异步函数
异步函数,也称为 async/await (语法关键字),是 ES8 规范新增的,是 ES6 中 Promise 在 ECMAScript 函数 中的应用。
为什么需要 async/await ?
ES8 的 async/await 主要是为了 解决利用异步结构组织代码的问题.
举个最简单的例子,下面的 Promise 超时后会进行 resolve,提供了 4 种写法:
// 写法 1 new Promise((resolve)=>{ setTimeout(()=>{ resolve('解决了'); },1000); }).then((value)=>{ console.log(value); }); // 写法 2 new Promise((resolve)=>{ setTimeout(resolve, 1000, '解决了'); }).then((value)=>{ console.log(value); }); // 写法 3 function handler(value) { console.log(value); } new Promise((resolve)=> setTimeout(resolve, 1000, '解决了')).then(handler); // 写法 4 function handler(value) { console.log(value); } let p = new Promise((resolve)=> setTimeout(resolve, 1000, '解决了')); p.then(handler); 复制代码
上述的 4 种写法,后一种都是前一种的优化改进,但是仔细观察发现,其实每一种写法的改进其实都不大. 因为任何需要访问这个 Promise 所产生值的代码,都需要以处理程序 (then + cllback) 的形式来接收这个值. 而上面的只是一个简单的需求,还没有其他多余的逻辑,仍旧显得比较复杂,可想而知如果你的使用场景很复杂,堆积组合的代码将会变得非常不直观.
相比于上面,下面的写法显然要更容易、更直观的被理解:
async function handler() { let rs = await new Promise((resolve) => setTimeout(resolve, 1000, '解决了')); console.log('inner = ',rs); } handler(); 复制代码
async 关键字
async 关键字用于 声明异步函数,可以用在 函数声明、函数表达式、箭头函数 和 方法 上.
async function example() {} // 函数声明 let example = async function() {}; // 函数表达式 let example = async () => {}; // 箭头函数 class example { async exampleHandler() {} // 方法 } 复制代码
- async 关键字可以让函数具有 异步特征,但总体上其代码仍然是同步求值的.
- 异步函数的 返回值(没有显示的 return 就会返回 undefined) 默认是一个被 Promise.resolve() 包裹的 Promise 对象,当然直接返回一个 Promise 对象也是一样的.
// 下面的数字代表输出顺序 async function example() { console.log("example"); // 1. example } let rs = example(); console.log('main'); // 2. main console.log(rs); // 3. Promise {<fulfilled>: undefined} rs.then(console.log); // 4. undefined 复制代码
- 异步函数的 返回值 期待一个实现 thenable 接口的对象,但这并不是严格要求的。如果返回的是 实现 thenable 接口 的对象,则这个对象可以由提供给 then() 的处理程序 “解包”。如果不是,则返回值就被当作状态为 fulfilled 的 Promise.
PS:实现 thenable 接口的对象,简单理解就是一个包含 then 方法的对象。“解包” 这里可以理解为把实现了 thenable 接口的对象中的 then 方法,当作在实例化 Promise 时要传入的 executor,如:new Promise(executor).
// 1. 返回原始值 async function example() { return 'example'; } example().then(console.log); // example // 2. 返回复杂类型(且没有实现 thenable 接口) async function example() { return ['example']; } example().then(console.log); // ['example'] // 3. 返回一个实现了 thenable 接口的非 Promise 对象 let obj = { then(resolve, reject) { console.log("obj.then run..."); resolve('解决了'); } } async function example() { return obj; } example().then(console.log); // 输出顺序:obj.then run... 解决了 复制代码
- 与在 Promise 中一样,在 异步函数 中 抛出错误 会返回状态为 rejected 的 Promise. 同样的,外部的 try/catch 无法进行捕获.
// 1. 普通函数 throw Error function example() { console.log('example run ...'); // 1. example run ... throw Error('出错了'); } try { example(); } catch (error) { console.log('error = ', error); // 2. error = Error: 出错了 // at example (index.html:41) // at index.html:45 } // 2. 异步函数 throw Error async function example() { console.log('example run ...'); // 1. example run ... throw Error('出错了'); } try { // 用 Promise 上的 then 方法 example().then(undefined, (reason) => { console.log('then onReject = ', reason); // 2. then onReject = Error: 出错了 // at example (index.html:41) // at index.html:45 }); // 用 Promise 上的 catch 方法 // example().catch(console.log); } catch (error) { // 和 Promise 一样,内部抛出的异常会被 reject 处理,并不会被 try/catch 捕获 console.log('error = ', error); // 不会被执行 } 复制代码
await 关键字
因为 异步函数 主要针对不会马上完成的任务,所以需要一种 暂停 和 恢复 执行的能力。使用 await 关键字可以暂停异步函数代码的执行,等待 Promise 进入 settled 状态。
settled 状态,即 Promise 状态变更为 fulfilled 或 rejected.
- await 关键字会暂停执行 异步函数 后面的代码,让出 JavaScript 运行时 的执行线程,这一点与 生成器函数(generator function) 中的 yield 关键字是一致的.
- await 关键字会尝试 “解包” 对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行.
“解包” 这里可以理解为把 await 后面的 Promise 在 settled 状态下的 value 或 reason 进行返回.
async function handler() { let rs = await new Promise((resolve) => setTimeout(resolve, 1000, '解决了')); console.log(rs); // 大约 1s 后输出:'解决了' } handler(); 复制代码
- await 关键字期望一个实现 thenable 接口的对象,这和 async 的返回值一样不是严格要求。如果是实现 thenable 接口的对象,则这个对象可以由 await 来 “解包”。如果不是,则这个值就被包装成状态为 fulfilled 状态的 Promise.
// 1. await + 原始值 async function example() { console.log(await 'example'); // example } example(); // 2. await + 返回复杂类型(且没有实现 thenable 接口) async function example() { console.log(await ['example']); // ['example'] } example(); // 3. await + 实现了 thenable 接口的非 Promise 对象 let obj = { then(resolve, reject) { resolve('解决了'); } } async function example() { console.log(await obj); // 解决了 } example(); // 4. await + Promise 对象 async function example() { console.log(await Promise.resolve('解决了')); // 解决了 } example(); 复制代码
- await 关键字在等待会 抛出错误 的 同步操作,会返回状态为 rejected 的 Promise. 和 async 关键字一样,外部的 try/catch 无法进行捕获.
// 定义函数 async function example() { console.log('start throw error'); // 1. start throw error await (() => { throw Error('抛出异常了'); })(); console.log('end throw error'); // 不会输出,因为 await 后抛出了异常,直接向外返回了 rejected 状态的 Promise } // 执行函数 try { example().catch(error => { console.log("promise catch = ",error); // 2. promise catch = Error: 抛出异常了 }); } catch (error) { console.log("try catch = ", error); // 不会输出,因为无法捕获 } 复制代码
- await 关键字必须在 异步函数 中使用,不能在顶级上下文如:
<script>
标签或 模块 中使用.
async/await 暂停和恢复执行
async/await 中真正起作用的是 await,可以把 async 关键字简单的当作一个 标识符. 因为如果 异步函数 中不使用 await 关键字,其执行基本上跟普通函数没有什么区别,但是对于 异步函数 的返回值还是会和 普通函数 有区别,这一点在上面有说明.
async function example1() { console.log(await new Promise((resolve, _) => { setTimeout(resolve('example1'),0); })); } async function example2() { console.log(await 'example2'); } async function example3() { console.log('example3'); } example1(); example2(); example3(); // 输出顺序:example3 example1 example2 复制代码
简单理解上面的输出顺序:
- JavaScript 运行时在碰到 await 关键字时,会记录在哪里暂停执行.
- 等到 await 右边的值可用了,JavaScript 运行时会向 消息队列 中推送一个任务,这个任务会 恢复异步函数的执行.
- 并且会按在 消息队列 中的顺序,依次恢复执行.
因此,即使 await 后面跟着一个 立即可用的值,函数的其余部分也会被 异步求值.
下面举个例子,详细介绍一下具体的过程:
async function async1() { console.log(2); console.log(await Promise.resolve(8)); console.log(9); } async function async2() { console.log(4); console.log(await 6); console.log(7); } console.log(1); async1(); console.log(3); async2(); console.log(5); // 输出顺序: 1 2 3 4 5 8 9 6 7 复制代码
一起来分析下执行过程:
(1) 执行 console.log(1),输出 1
(2) 执行 async1() 异步函数:
- (2.1) 执行 console.log(2),输出 2
- (2.2) 遇到 console.log(await Promise.resolve(8)),此时碰到了 await 关键字先暂停执行,向消息队列中添加一个 Promise 在 settle 之后且值为 8 的任务 (3) 此时 async1() 函数先退出执行
(4) 执行 console.log(3),输出 3
(5) 执行 async2() 异步函数:
- (5.1) 执行 console.log(4),输出 4
- (5.2) 遇到 console.log(await 6),从上面对于 await 的介绍中可以知道,它等价于 console.log(await Promise.resolve(6)),此时碰到了 await 关键字先暂停执行,向消息队列中添加立即可用值为 6 的任务
(6) 此时 async2() 函数先退出执行
(7) 执行 console.log(5),输出 5
(8) 到这,主线程已经执行完毕
(9) JavaScript 运行时从消息队列中取出解决 await 后面 Promise 的处理程序
(10) JavaScript 运行时从消息队列中取出恢复执行 async1()的任务及值 8
- (10.1) 此时 console.log(await Promise.resolve(8)) 等价于 console.log(8),输出 8
- (10.2) 执行 console.log(9),输出 9
- (10.3) 此时 async1 执行完成并返回
(11) JavaScript 运行时从消息队列中取出恢复执行 async2()的任务及值 6
- (11.1) 此时 console.log(await 6) 等价于 console.log(6),输出 6
- (11.2) 执行 console.log(7),输出 7
- (11.3) 此时 async2 执行完成并返回 其实也可以和 事件循环 结合在一起看,下面给出了简单的图解: