0长10时间没有好好的总结点知识了,最近过的真的太颓废了,看来是要好好的约束一下自己了。
由于前段时间在写公司的机器人程序(没办法,产品用户少,只能是做机器人大军来稳定一下局面不至于太难堪)用的基本上都是异步,各种异步请求等等。采用promise+async解决,现在来总结一下异步的方法。
异步是所有编程语言中都存在的一种流程处理方式,所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
解决异步的方法大概分为以下几种:
- 回调函数
- 事件监听
- 订阅、发布模式
- promise
- generator
- async
回调函数这个很简单,订阅发布模式在之前有篇文章中记录过,promise的使用也在之前的一片文章中简单介绍过。
这篇就介绍一下generator 和 async 。
async 和 generator 算是同源,async是generator的语法糖,用法基本相似。但是我还是偏爱async的,写法比较易懂。
还记得第一遍看es6的文档的时候看到generator就懵逼了,看不懂了。尤其是co 什么的,但是这次看的时候就明了很多了。
generator
generator函数可以看做一个状态机,内部有多个状态,执行 generator 函数会返回一个遍历器对象{value:xxx,done:xxx}
generato函数是普通函数,但是有两个特征:
function
和 方法名之间有个*
用来标示这是一个generator函数- 在方法的内部使用
yield
来定义不同的状态。
function* helloGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloGenerator();
该函数有三个状态:hello,world 和 return 语句(结束执行)。
虽然最后一行调用了此方法,但是不会去执行,这是generator特殊的地方,想要执行必须调用遍历器对象的next
方法,使得指针移向下一个状态。也就是说,每次调用next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield
表达式(或return
语句)为止。而且每次返回的也不是函数的值而是遍历器对象{value:xxx,done:true/false}
。 所以说generator 函数是分段执行的,yield
表达式是暂停执行的标记,而next
方法可以恢复执行。
hw.next() // { value: 'hello', done: false } hw.next() // { value: 'world', done: false } hw.next() // { value: 'ending', done: true } hw.next() // { value: undefined, done: true }
value
属性就是当前yield
表达式的值xxx
,done
属性的值false/true
,表示遍历是否结束。
总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的
next
方法,就会返回一个有着value
和done
两个属性的对象。value
属性表示当前的内部状态的值,是yield
表达式后面那个表达式的值;done
属性是一个布尔值,表示是否遍历结束。
yield 表达式
generator函数可以看做是一种可以暂定的函数,yield
表达式就是暂停标志。
遍历器对象的next
方法的运行逻辑如下。
- 遇到
yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。 - 下一次调用
next
方法时,再继续往下执行,直到遇到下一个yield
表达式。 - 如果没有再遇到新的
yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值。 - 如果该函数没有
return
语句,则返回的对象的value
属性值为undefined
。
需要注意的是,yield
表达式后面的表达式,只有当调用next
方法、内部指针指向该语句时才会执行。
for...of 循环
for...of
循环可以自动遍历 Generator 函数时生成的Iterator
对象,且此时不再需要调用next
方法。
function *foo() { yield 1; yield 2; yield 3; yield 4; yield 5; return 6; } for (let v of foo()) { console.log(v); } // 1 2 3 4 5
上面代码使用for...of
循环,依次显示5个yield
表达式的值。这里需要注意,一旦next
方法的返回对象的done
属性为true
,for...of
循环就会中止,且不包含该返回对象,所以上面代码的return
语句返回的6
,不包括在for...of
循环之中。
利用for...of
循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用for...of
循环,通过 Generator 函数为它加上这个接口,就可以用了。
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);// 返回一个包含所有自身属性(不包含继承属性)的数组。
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
for (let [key, value] of objectEntries(jane)) {
console.log(`{key}:{value}`);
}
// first: Jane
// last: Doe
generator 异步Demo
var fetch = require('node-fetch'); function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio); } var g = gen(); var result = g.next(); result.value.then(function(data){ return data.json(); }).then(function(data){ g.next(data); });
上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了yield
命令。
首先执行 Generator 函数,获取遍历器对象,然后使用next
方法(第二行),执行异步任务的第一阶段。由于Fetch
模块返回的是一个 Promise 对象,因此要用then
方法调用下一个next
方法。
可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。
co 模块
解决generator自动执行的方法有很多,最著名的 就是CO模块
了。 使用co 的话不需要我们自己去写generator函数的执行器,只要generator 函数传入co中,就会自动执行。
var co = require('co'); var gen = function* () { var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; co(gen);
co
函数返回一个Promise
对象,因此可以用then
方法添加回调函数。
co(gen).then(function (){ console.log('Generator 函数执行完成'); });
处理并发的异步操作
co 支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下一步。
这时,要把并发的操作都放在数组或对象里面,跟在yield
语句后面。
// 数组的写法 co(function* () { var res = yield [ Promise.resolve(1), Promise.resolve(2) ]; console.log(res); }).catch(onerror); // 对象的写法 co(function* () { var res = yield { 1: Promise.resolve(1), 2: Promise.resolve(2), }; console.log(res); }).catch(onerror);
下面是另一个例子。
co(function* () { var values = [n1, n2, n3]; yield values.map(somethingAsync); }); function* somethingAsync(x) { // do something async return y }
上面的代码允许并发三个somethingAsync
异步操作,等到它们全部完成,才会进行下一步。
async 函数
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
var fs = require('fs'); var readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function(error, data) { if (error) reject(error); resolve(data); }); }); }; var gen = function* () { var f1 = yield readFile('/etc/fstab'); var f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; //写成async函数,就是下面这样。 var asyncReadFile = async function () { var f1 = await readFile('/etc/fstab'); var f2 = await readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); };
一比较就会发现,async
函数就是将 Generator 函数的星号(*
)替换成async
,将yield
替换成await
,仅此而已。
async
函数对 Generator 函数的改进,体现在以下四点。
(1)内置执行器。
Generator 函数的执行必须靠执行器,所以才有了co
模块,而async
函数自带执行器。也就是说,async
函数的执行,与普通函数一模一样,只要一行。
var result = asyncReadFile();
上面的代码调用了asyncReadFile
函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next
方法,或者用co
模块,才能真正执行,得到最后结果。
(2)更好的语义。
async
和await
,比起星号和yield
,语义更清楚了。async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。
co
模块约定,yield
命令后面只能是 Thunk 函数或 Promise 对象,而async
函数的await
命令后面,可以是Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
(4)返回值是 Promise。
async
函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then
方法指定下一步的操作。
进一步说,async
函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await
命令就是内部then
命令的语法糖。
基本用法
async
函数返回一个 Promise 对象,可以使用then
方法添加回调函数。当函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
//demo async function getStockPriceByName(name) { var symbol = await getStockSymbol(name); var stockPrice = await getStockPrice(symbol); return stockPrice; } getStockPriceByName('goog').then(function (result) { console.log(result); });
async
函数内部抛出错误,会导致返回的 Promise 对象变为reject
状态。抛出的错误对象会被catch
方法回调函数接收到。
async function f() { throw new Error('出错了'); } f().then( v => console.log(v), e => console.log(e) ) // Error: 出错了
async
函数返回的 Promise 对象,必须等到内部所有await
命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return
语句或者抛出错误。也就是说,只有async
函数内部的异步操作执行完,才会执行then
方法指定的回调函数。
async function getTitle(url) { let response = await fetch(url); let html = await response.text(); return html.match(/<title>([\s\S]+)<\/title>/i)[1]; } getTitle('https://tc39.github.io/ecma262/').then(console.log) // "ECMAScript 2017 Language Specification"
上面代码中,函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then方法里面的console.log。
await 命令
正常情况下,await
命令后面是一个 Promise 对象。如果不是,会被转成一个立即resolve
的 Promise 对象。
async function f() { return await 123; } f().then(v => console.log(v)) // 123
上面代码中,await
命令的参数是数值123
,它被转成 Promise 对象,并立即resolve
。
await
命令后面的 Promise 对象如果变为reject
状态,则reject
的参数会被catch
方法的回调函数接收到。
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await
放在try...catch
结构里面,这样不管这个异步操作是否成功,第二个await
都会执行。
async function f() { try { await Promise.reject('出错了'); } catch(e) { } return await Promise.resolve('hello world'); } f() .then(v => console.log(v)) // hello world
另一种方法是await
后面的 Promise 对象再跟一个catch
方法,处理前面可能出现的错误。
async function f() { await Promise.reject('出错了') .catch(e => console.log(e)); return await Promise.resolve('hello world'); } f() .then(v => console.log(v)) // 出错了 // hello world
如果有多个await
命令,可以统一放在try...catch
结构中。
async function main() { try { var val1 = await firstStep(); var val2 = await secondStep(val1); var val3 = await thirdStep(val1, val2); console.log('Final: ', val3); } catch (err) { console.error(err); } }
使用注意点
第一点,前面已经说过,await
命令后面的Promise
对象,运行结果可能是rejected
,所以最好把await
命令放在try...catch
代码块中。
async function myFunction() { try { await somethingThatReturnsAPromise(); } catch (err) { console.log(err); } } // 另一种写法 async function myFunction() { await somethingThatReturnsAPromise() .catch(function (err) { console.log(err); }; }
第二点,多个await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
let foo = await getFoo(); let bar = await getBar();
上面代码中,getFoo
和getBar
是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo
完成以后,才会执行getBar
,完全可以让它们同时触发。
// 写法一 let [foo, bar] = await Promise.all([getFoo(), getBar()]); // 写法二 let fooPromise = getFoo(); let barPromise = getBar(); let foo = await fooPromise; let bar = await barPromise;
上面两种写法,getFoo
和getBar
都是同时触发,这样就会缩短程序的执行时间。
第三点,await
命令只能用在async
函数之中,如果用在普通函数,就会报错。
第四点,采用for循环不要采用forEach 循环,
如果确实希望多个请求并发执行,可以使用Promise.all
方法。
async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = await Promise.all(promises); console.log(results); } // 或者使用下面的写法 async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = []; for (let promise of promises) { results.push(await promise); } console.log(results); }
项目中的应用可以参见机器人批量注册和获取token的代码。
async function build() { let len = userList.length - 1; console.log(userList.length) for (i = 0; i < userList.length; i++) { let item = userList[i]; var parmans = { account: item.account, password: item.password, }; await fetch('https://api.xxxxx.cn/login', { method: 'POST', body: JSON.stringify(parmans), headers: {'Content-Type': 'application/json'}, }) .then(function (res) { return res.json() }) .then(function (data) { console.log(data) }).catch(function (err) { console.log(err) }) } } build();
总结、摘自 http://es6.ruanyifeng.com/
文章评论