异步函数是 ES8 新增的,旨在解决利用异步结构组织代码的问题。
异步函数是 ECMAScript 对函数的扩展,是 ES6 期约在函数中的应用。
异步函数主要用于异步执行复杂任务,即执行时间较长的任务。
主要参考资料:
- 《JavaScript 高级程序设计(第4版)》- P347(372/931)
定义异步函数
通过在函数定义前使用关键字 async ,定义异步函数。
示例:
- 定义异步函数
// 命名函数 async function asyncFunc_01() {} // 匿名函数 const asyncFunc_02 = async function() {} // 箭头函数 const asyncFunc_03 = async () => {}
调用异步函数
调用异步函数,异步函数的返回值始终是一个期约。
异步函数返回期约的状态:
- 待定
在异步函数中返回一个待定期约。
(根据 JavaScript 引擎的具体实现,或者还有其它情形)
- 解决
在异步函数中返回解决期约。
解决期约会被镜像生成一个新的解决期约,异步函数返回新的解决期约。
在异步函数中返回非期约值。
非期约值会被方法 Promise.reolve() 包装生成一个解决期约,异步函数返回这个解决期约。
没有在异步函数中返回值。
通过方法 Promise.reolve() 会包装一个 undefined 值生成一个解决期约,异步函数返回这个解决期约。
- 拒绝
在异步函数中抛出错误。
异步函数返回一个拒绝期约,拒绝期约的理由为抛出的错误。
在异步函数中返回拒绝期约。
拒绝期约会被镜像生成一个新的拒绝期约,异步函数返回新的拒绝期约。
异步函数期待在函数体内返回一个实现接口 Thenable 的对象(例如:期约)。
在异步函数中返回的 thenable 对象会被解包。
- 主要对象:
thenable 对象的方法 then(callback)
- 解包效果:
- 执行方法 then() 中除调用 callback() 之外的语句。
方法 then() 的返回值会被忽略
- 取出方法 then() 中第一次调用 callback() 时传入的第一个参数的值,作为异步函数的返回期约的值
thenable 对象方法 then() 中其它调用 callback() 的语句会被忽略。
解包是异步执行的。
示例:
-
调用异步函数,异步函数的返回值是一个期约。
async function asyncFunc() { return 'value' } const returnedValue = asyncFunc() // 调用异步函数 setTimeout(console.log, 0, 'returnedValue:', returnedValue) // 输出: // returnedValue: Promise {<fulfilled>: 'value'}
-
在异步函数中抛出错误。
async function asyncFunc() { throw 'error' // 抛出错误 } const returnedValue = asyncFunc() returnedValue.catch( (reason) => { console.log('rejected is resolved:', reason) } ) setTimeout(console.log, 0, 'returnedValue:', returnedValue) // 输出: // rejected is resolved: error // returnedValue: Promise {<rejected>: 'error'}
-
在异步函数中返回拒绝期约。
async function asyncFunc() { return Promise.reject('error') // 返回拒绝期约 } const returnedValue = asyncFunc() returnedValue.catch( (reason) => { console.log('rejected is resolved:', reason) } ) setTimeout(console.log, 0, 'returnedValue:', returnedValue) // 输出: // rejected is resolved: error // returnedValue: Promise {<rejected>: 'error'}
-
在异步函数中返回实现接口 Thenable 的对象。
async function asyncFunc() { const thenable = { then(callback) { // 方法 then(callback) console.log('statement_01') // 其它语句 callback('value_01') // 第一次调用 callback() console.log('statement_02') callback('value_02') return 'returned value' // 返回值 } } // 实现接口 Thenable 的对象 return thenable } const returnedValue = asyncFunc() returnedValue.then(console.log) setTimeout(console.log, 0, 'returnedValue:', returnedValue) // 输出: // statement_01 // statement_02 // value_01 // returnedValue: Promise {<fulfilled>: 'value_01'}
等待异步执行
在异步函数中,通过在某个表达式前面使用关键字 await ,使异步函数暂停被执行,等待异步恢复异步函数的执行。
代码执行在执行异步函数时遇到关键字 await ,会暂停执行异步函数。
然后把关键字 await 之后的异步函数的代码的后续执行作为一个任务进行排期。
最后绕过该异步函数的执行,继续执行后面的代码。
关键字 await 期待一个实现接口 Thenable 的对象(例如:期约)。
在关键字 await 后面的 thenable 对象会被解包:
- 主要对象:
thenable 对象的方法 then(callback)
- 解包效果:
- 执行方法 then() 中除调用 callback() 之外的语句。
方法 then() 的返回值会被忽略
- 取出方法 then() 中第一次调用 callback() 时传入的第一个参数的值,替换 thenable 对象。
thenable 对象方法 then() 中其它调用 callback() 的语句会被忽略。
解包是异步执行的。
使用关键字 await 等待期约。
在期约落定之前,异步函数将一直处于暂停状态。
期约落定为解决期约,取出解决期约的值,替换期约。
期约落定为拒绝期约,即在异步函数中抛出(异步)错误,异步函数返回一个拒绝期约。
关键字 await 只能在异步函数的函数块中使用,不能在普通函数的函数块中使用。
示例:
-
在异步函数中等待实现接口 Thenable 的对象。
async function asyncFunc() { const thenable = { then(callback) { console.log('statement_01') callback('value_01') console.log('statement_02') callback('value_02') return 'return' } } console.log('await:', await thenable) // 等待实现接口 Thenable 的对象 console.log('asyncFunc continue execute') } const returnedValue = asyncFunc() console.log('before await') // 输出: // before await // statement_01 // statement_02 // await: value_01 // asyncFunc continue execute
-
异步函数等待实现接口 Thenable 的对象,并返回 thenable 对象。
async function asyncFunc() { const thenable_01 = { then(callback) { console.log('thenable_01->statement_01') callback('thenable_01->value_01') console.log('thenable_01->statement_02') callback('thenable_01->value_02') return 'thenable_01->return' } } const thenable_02 = { then(callback) { console.log('thenable_02->statement_01') callback('thenable_02->value_01') console.log('thenable_02->statement_02') callback('thenable_02->value_02') return 'thenable_02->return' } } console.log('await:', await thenable_01) // 等待 thenable 对象 thenable_01 return thenable_02 // 返回 thenable 对象 thenable_02 } const returnedValue = asyncFunc() setTimeout(console.log, 0, 'returnedValue:', returnedValue) // 输出: // thenable_01->statement_01 // thenable_01->statement_02 // await: thenable_01->value_01 // thenable_02->statement_01 // thenable_02->statement_02 // returnedValue: Promise {<fulfilled>: 'thenable_02: value_01'}
-
使用关键字 await 等待一个最终落定为解决的待定期约。
async function asyncFunc() { console.log( 'await:', await new Promise( (resolve) => { setTimeout(resolve, 1000, 'value') } ) // 等待 1s 后落定为解决的期约 ) console.log('asyncFunc continue execute') } asyncFunc() // 输出: // await: value // asyncFunc continue execute
-
使用关键字 await 等待一个拒绝期约。
async function asyncFunc() { console.log('await:', await Promise.reject('error')) // 等待拒绝期约,即抛出异步错误 console.log('never execute') // 不会执行 } const returnedValue = asyncFunc() returnedValue.catch( (reason) => { console.log('rejected is resolved:', reason) } ) setTimeout(console.log, 0, 'returnedValue:', returnedValue) // 输出: // rejected is resolved: error // returnedValue: Promise {<rejected>: 'error'}
异步函数与期约
异步函数与期约的功能有相当程度的重叠,但它们在内存中的表现差别很大。
在创建期约时,JavaScript 引擎会维护一个存储调用信息的调用栈,显然的,这个调用栈会占用内存。在抛出错误时,错误处理系统会获取调用栈生成栈追踪信息。
在函数嵌套调用时,JavaScript 引擎会在嵌套函数中存储指向包含函数的指针,这些指针也会占用内存。在抛出错误时,错误处理系统会获取函数嵌套指针生成栈追踪信息。
可以通过抛出错误打印调用栈或函数嵌套指针的信息,从而查看期约调用栈和异步函数嵌套指针的内容。
-
打印期约调用栈。
function promiseExecutor(resolve, reject) { setTimeout(reject, 1000, 'rejected') } function createPromise() { return new Promise(promiseExecutor) } createPromise() // 输出 // Uncaught (in promise) rejected //-- setTimeout (async) //-- promiseExecutor //-- createPromise //-- (anonymous)
-
打印异步函数嵌套指针。
function promiseExecutor(resolve, reject) { setTimeout(reject, 1000, 'rejected') } async function createPromise() { return await new Promise(promiseExecutor) } createPromise() // 输出 // Uncaught (in promise) rejected //-- createPromise //-- await in createPromise (async) //-- (anonymous)
可以看出,对于同样的行为,期约比异步函数维护了更多的调用信息。
即期约调用栈比异步函数嵌套指针额外保留了已经成功返回的函数的调用信息(如上面示例中的 setTimeout()、promiseExecutor() )。
所以 JavaScript 引擎会为期约维护一个尽可能地保留完整的调用信息的调用栈,这会带来一些计算和存储成本。
因此在重视性能的应用开发中,应优先考虑使用异步函数。