异步函数
异步函数,也称为“async/await”(语法关键字),是 ES6 期约模式在 ECMAScript 函数中的应用。
这个特性从行为和语法上都增强了 JavaScript,让以同步方式写的代码能够异步执行。
下面来看一个最简单的例子:
1
| let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
|
这个期约在1000 毫秒之后解决为数值 3。如果程序中的其他代码要在这个值可用时访问它,则需写一个解决处理程序,这其实是很不方便的,
1 2
| let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3)); p.then((x) => console.log(x));
|
因为其他代码都必须塞到期约处理程序中。不过可以把处理程序定义为一个函数:
1 2 3
| function handler(x) { console.log(x); } let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3)); p.then(handler);
|
这个改进其实也不大。这是因为任何需要访问这个期约所产生值的代码,都需要以处理程序的形式 来接收这个值。也就是说,代码照样还是要放到处理程序里。
ES8 为此提供了 async/await 关键字。
async 和 await
async
1.async 关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上
1 2 3 4 5 6
| async function foo() {} let bar = async function() {}; let baz = async () => {}; class Qux { async qux() {} }
|
2.使用 async 关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。
而在参数或闭包方面,异步函数仍然具有普通 JavaScript 函数的正常行为。
3.异步函数的返回值期待(但实际上并不要求)一个实现 thenable 接口的对象,但常规的值也可以。 如果返回的是实现 thenable 接口的对象,则这个对象可以由提供给 then()的处理程序“解包”。如果不是,则返回值就被当作已经解决的期约。下面的代码演示了这些情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| async function foo() { return 'foo'; } foo().then(console.log);
async function bar() { return ['bar']; } bar().then(console.log);
async function baz() { const thenable = { then(callback) { callback('baz'); } }; return thenable; } baz().then(console.log);
async function qux() { return Promise.resolve('qux'); } qux().then(console.log);
|
4.与在期约处理程序中一样,在异步函数中抛出错误会返回拒绝的期约:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| async function foo() { console.log(1); throw 3; }
foo().catch(console.log); console.log(2);
不过,拒绝期约的错误不会被异步函数捕获: async function foo() { console.log(1); Promise.reject(3); }
foo().catch(console.log); console.log(2);
|
await
1.异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用 await 关键字可以暂停异步函数代码的执行,等待期约解决。
1 2 3 4 5 6 7 8 9 10
| let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3)); p.then((x) => console.log(x)); 使用 async/await 可以写成这样: async function foo() { let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3)); console.log(await p); } foo();
|
2.await 关键字会暂停执行异步函数后面的代码,让出 JavaScript 运行时的执行线程。这个行 为与生成器函数中的 yield 关键字是一样的。
3.await 关键字的用法与 JavaScript 的一元操作一样。它可以单独使用,也可以在表达式中使用,如下面的例子所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| async function foo() { console.log(await Promise.resolve('foo')); } foo();
async function bar() { return await Promise.resolve('bar'); } bar().then(console.log);
async function baz() { await new Promise((resolve, reject) => setTimeout(resolve, 1000)); console.log('baz'); } baz();
|
4.await 关键字期待(但实际上并不要求)一个实现 thenable 接口的对象,但常规的值也可以。如 果是实现 thenable 接口的对象,则这个对象可以由 await 来“解包”。如果不是,则这个值就被当作已经解决的期约。
await 的限制
1.await 关键字必须在异步函数中使用,不能在顶级上下文如<script>
标签或模块中使用。不过,定义并立即调用异步函数是没问题的。
1 2 3 4 5 6 7 8 9 10
| async function foo() { console.log(await Promise.resolve(3)); } foo();
(async function() { console.log(await Promise.resolve(3)); })();
|
2.异步函数的特质不会扩展到嵌套函数。因此,await 关键字也只能直接出现在异步函数的定 义中。在同步函数内部使用 await 会抛出 SyntaxError。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| function foo() { const syncFn = () => { return await Promise.resolve('foo'); }; console.log(syncFn()); }
function bar() { function syncFn() { return await Promise.resolve('bar'); } console.log(syncFn()); }
function baz() { const syncFn = function() { return await Promise.resolve('baz'); }; console.log(syncFn()); }
function qux() { (function () { console.log(await Promise.resolve('qux')); })(); (() => console.log(await Promise.resolve('qux')))(); }
|
3.要完全理解 await 关键字,必须知道它并非只是等待一个值可用那么简单。JavaScript 运行时在碰 到 await 关键字时,会记录在哪里暂停执行。等到 await 右边的值可用了,JavaScript 运行时会向消息 队列中推送一个任务,这个任务会恢复异步函数的执行。
因此,即使 await 后面跟着一个立即可用的值,函数的其余部分也会被异步求值。下面的例子演 示了这一点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| async function foo() { console.log(2); await null; console.log(4); } console.log(1); foo(); console.log(3);
控制台中输出结果的顺序很好地解释了运行时的工作过程: (1) 打印 1; (2) 调用异步函数 foo(); (3)(在 foo()中)打印 2; (4)(在 foo()中)await 关键字暂停执行,为立即可用的值 null 向消息队列中添加一个任务; (5) foo()退出; (6) 打印 3; (7) 同步线程的代码执行完毕; (8) JavaScript 运行时从消息队列中取出任务,恢复异步函数执行; (9)(在 foo()中)恢复执行,await 取得 null 值(这里并没有使用); (10)(在 foo()中)打印 4; (11) foo()返回。 如果 await 后面是一个期约,则问题会稍微复杂一些。此时,为了执行异步函数,实际上会有两个 任务被添加到消息队列并被异步求值。下面的例子虽然看起来很反直觉,但它演示了真正的执行顺序: async function foo() { console.log(2); console.log(await Promise.resolve(8)); console.log(9); } async function bar() { console.log(4); console.log(await 6); console.log(7); } console.log(1); foo(); console.log(3); bar(); console.log(5);
运行时会像这样执行上面的例子: (1) 打印 1; (2) 调用异步函数 foo(); (3)(在 foo()中)打印 2; (4)(在 foo()中)await 关键字暂停执行,向消息队列中添加一个期约在落定之后执行的任务; (5) 期约立即落定,把给 await 提供值的任务添加到消息队列; (6) foo()退出; (7) 打印 3; (8) 调用异步函数 bar(); (9)(在 bar()中)打印 4; (10)(在 bar()中)await 关键字暂停执行,为立即可用的值 6 向消息队列中添加一个任务; (11) bar()退出; (12) 打印 5; (13) 顶级线程执行完毕; (14) JavaScript 运行时从消息队列中取出解决 await 期约的处理程序,并将解决的值 8 提供给它; (15) JavaScript 运行时向消息队列中添加一个恢复执行 foo()函数的任务; (16) JavaScript 运行时从消息队列中取出恢复执行 bar()的任务及值 6; (17)(在 bar()中)恢复执行,await 取得值 6; (18)(在 bar()中)打印 6; (19)(在 bar()中)打印 7; (20) bar()返回; (21) 异步任务完成,JavaScript 从消息队列中取出恢复执行 foo()的任务及值 8; (22)(在 foo()中)打印 8; (23)(在 foo()中)打印 9; (24) foo()返回。
|
异步函数策略
因为简单实用,所以异步函数很快成为 JavaScript 项目使用最广泛的特性之一。
实现 sleep()
1 2 3 4 5 6 7 8 9 10
| async function sleep(delay) { return new Promise((resolve) => setTimeout(resolve, delay)); } async function foo() { const t0 = Date.now(); await sleep(1500); console.log(Date.now() - t0); } foo();
|
利用平行执行
串行执行期约
使用 async/await,期约连锁会变得很简单:
1 2 3 4 5 6 7 8 9 10
| function addTwo(x) {return x + 2;} function addThree(x) {return x + 3;} function addFive(x) {return x + 5;} async function addTen(x) { for (const fn of [addTwo, addThree, addFive]) { x = await fn(x); } return x; } addTen(9).then(console.log);
|
这里,await 直接传递了每个函数的返回值,结果通过迭代产生。当然,这个例子并没有使用期约, 如果要使用期约,则可以把所有函数都改成异步函数。这样它们就都返回期约了:
1 2 3 4 5 6 7 8 9 10
| async function addTwo(x) {return x + 2;} async function addThree(x) {return x + 3;} async function addFive(x) {return x + 5;} async function addTen(x) { for (const fn of [addTwo, addThree, addFive]) { x = await fn(x); } return x; } addTen(9).then(console.log);
|
栈追踪与内存管理