含义
ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async
函数是什么?一句话,它就是 Generator 函数的语法糖。一比较就会发现,async
函数就是将 Generator 函数的星号(*)替换成async
,将yield
替换成await
,仅此而已。
async
函数对 Generator 函数的改进,体现在以下四点。
- 内置执行器。
Generator 函数的执行必须靠执行器,所以才有了co
模块,而async
函数自带执行器。也就是说,async
函数的执行,与普通函数一模一样,只要一行。- 更好的语义。
async
和await
,比起星号和yield
,语义更清楚了。async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。- 更广的适用性。
co
模块约定,yield
命令后面只能是 Thunk 函数或 Promise 对象,而async
函数的await
命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即resolved
的 Promise 对象)。- 返回值是 Promise。
async
函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then
方法指定下一步的操作。换一句话说,
async
函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await
命令就是内部then
命令的语法糖。
基本用法
async
函数返回一个 Promise 对象,可以使用then
方法添加回调函数。当函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
下面是一个例子,指定多少毫秒后输出一个值。
- 由于
async
函数返回的是 Promise 对象,可以作为await
命令的参数。所以,上面的例子也可以写成下面的形式。
async 函数有多种使用形式。
语法
async
函数的语法规则总体上比较简单,难点是错误处理机制。
- 返回 Promise 对象
async
函数返回一个 Promise 对象。
async
函数内部return
语句返回的值,会成为then
方法回调函数的参数。
async
函数内部抛出错误,会导致返回的 Promise 对象变为reject
状态。抛出的错误对象会被catch
方法回调函数接收到。
- Promise 对象的状态变化
async
函数返回的 Promise 对象,必须等到内部所有await
命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return
语句或者抛出错误。也就是说,只有async
函数内部的异步操作执行完,才会执行then
方法指定的回调函数。
- await 命令
正常情况下,await
命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。
另一种情况是,await
命令后面是一个thenable
对象(即定义then
方法的对象),那么await
会将其等同于 Promise 对象。
这个例子还演示了如何实现休眠效果。JavaScript 一直没有休眠的语法,但是借助await
命令就可以让程序停顿指定的时间。
await
命令后面的 Promise 对象如果变为reject
状态,则reject
的参数会被catch
方法的回调函数接收到。
任何一个await
语句后面的 Promise 对象变为reject
状态,那么整个async
函数都会中断执行。
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await
放在try...catch
结构里面,这样不管这个异步操作是否成功,第二个await
都会执行。
另一种方法是await
后面的 Promise 对象再跟一个catch
方法,处理前面可能出现的错误。
- 错误处理
如果await
后面的异步操作出错,那么等同于async
函数返回的 Promise 对象被reject
。
防止出错的方法,也是将其放在try...catch代码块之中。
如果有多个await命令,可以统一放在try...catch结构中。
下面的例子使用try...catch结构,实现多次重复解析文本并取出标题。
- 使用注意事项
第一点,前面已经说过,await
命令后面的Promise
对象,运行结果可能是rejected
,所以最好把await
命令放在try...catch
代码块中。
第二点,多个await
命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
第三点,await
命令只能用在async
函数之中,如果用在普通函数,就会报错。
第四点,async
函数可以保留运行堆栈。
async 函数的实现原理
async
函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
所有的async
函数都可以写成上面的第二种形式,其中的spawn
函数就是自动执行器。
- 下面给出
spawn
函数的实现。function spawn(genF) { return new Promise(function(resolve, reject) { const gen = genF(); function step(nextF) { let next; try { next = nextF(); } catch(e) { return reject(e); } if(next.done) { return resolve(next.value); } Promise.resolve(next.value).then(function(v) { step(function() { return gen.next(v); }); }, function(e) { step(function() { return gen.throw(e); }); }); } step(function() { return gen.next(undefined); }); }); }
与其他异步处理方法的比较
我们通过一个例子,来看
async
函数与 Promise、Generator 函数的比较。
假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。
- 首先是 Promise 的写法。
function chainAnimationsPromise(elem, animations) { // 变量ret用来保存上一个动画的返回值 let ret = null; // 新建一个空的Promise let p = Promise.resolve(); // 使用then方法,添加所有动画 for(let anim of animations) { p = p.then(function(val) { ret = val; return anim(elem); }); } // 返回一个部署了错误捕捉机制的Promise return p.catch(function(e) { /* 忽略错误,继续执行 */ }).then(function() { return ret; }); }
虽然 Promise 的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是 Promise 的 API(
then
、catch
等等),操作本身的语义反而不容易看出来。
- 接着是 Generator 函数的写法。
function chainAnimationsGenerator(elem, animations) { return spawn(function*() { let ret = null; try { for(let anim of animations) { ret = yield anim(elem); } } catch(e) { /* 忽略错误,继续执行 */ } return ret; }); }
上面代码使用 Generator 函数遍历了每个动画,语义比 Promise 写法更清晰,用户定义的操作全部都出现在
spawn
函数的内部。这个写法的问题在于,必须有一个任务运行器,自动执行 Generator 函数,上面代码的spawn
函数就是自动执行器,它返回一个 Promise 对象,而且必须保证yield
语句后面的表达式,必须返回一个 Promise。
- 最后是 async 函数的写法。
async function chainAnimationsAsync(elem, animations) { let ret = null; try { for(let anim of animations) { ret = await anim(elem); } } catch(e) { /* 忽略错误,继续执行 */ } return ret; }
可以看到 Async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将 Generator 写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。如果使用 Generator 写法,自动执行器需要用户自己提供。