关于 Async/await,看这一篇就够了

18,402 阅读6分钟

这篇文章主要关注 async/await 的执行原理。虽然以前也有用过 async/await,但场景都比较简单,对它也只是有个大致的理解,近期做了一个大量使用 async/await 的 node 服务工程,在使用的细节上产生了很多疑惑。比如:

关于异常处理的疑惑

await Promise 通过在外面 try catch 和给 Promise 加 .catch() 都可以处理异常,那我们应该用哪种?

下面的 async 函数被执行时,如果没有声明 await 为什么就不能 catch 到 _run 方法内部的异常了?

async run () {
  try {
    await _run()
  } catch (err) {} 
}

async 函数里的 try catch 能捕获哪些异常?为什么说它比 Promise 的异常处理好?

关于 async 函数的疑惑

async 函数如果没有用到 await 会怎么样,或者说如果我把一个普通函数给加上 async 定义,还能像从前一样用它吗?

async 函数如果直接 return 值 'great',就会相当于 return 一个 resolve 值是 great 的 Promise,如果直接 return 一个 resolve 值是 great 的 Promise,实际 await 到的值依然是 great,而不是这个 Promise。为什么?如果 return 的是一个还没有 resolve 的 Promise,会怎么样?这里的原理是怎样的?

Async/await 实现原理

带着这些疑问,经过一番查找,我逐渐明确了它的实现原理,明白了为什么人们常说它只是 Promise 的语法糖,而实际上它是借助 generator 来更清晰地表达 Promise 的异步流程,这篇回答 可以说明这一点。而 这篇 tc39 的草案 是我认为能够比较清晰地解释 async/await 执行原理的材料,贴一下代码:

function spawn(genF, self) {
    return new Promise(function(resolve, reject) {
        var gen = genF.call(self);
        function step(nextF) {
            var next;
            try {
                next = nextF();
            } catch(e) {
                // finished with failure, reject the promise
                reject(e);
                return;
            }
            if(next.done) {
                // finished with success, resolve the promise
                resolve(next.value);
                return;
            }
            // not finished, chain off the yielded promise and `step` again
            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); });
    });
}

既然是借助了 generator,那想要理解这段代码,就要先熟悉 generator 的特性,这里就不详细介绍了,有不熟悉的朋友建议先弄清楚。这个函数的作用就是将 async 函数转化成一个会 return Promise 的新函数,Promise 的执行过程会将原函数作为 generator 自动循环执行,执行的三个关键点是:

  1. 每一次执行 next 都会包裹上 try catch,以保证在有异常时能够抛出。
  2. 在 next.done 也就是函数结束时将整体 Promise 用 generator 函数 return 的值 resolve 掉。
  3. 将 await 的值通过 Promise.resolve 转化为 Promise,并为它挂接回调,成功时继续执行 generator,失败时让 generator throw 异常。这也是最关键的部分。

建议大家先理解代码,尝试自己回答一下开篇的几个问题,然后再继续往下看我的解释。

关于异常处理的:

  • 第一个问题,.catch 是 Promise 本身就支持的写法,我们用它处理也没有问题,只是既然都用了 async/await 就最好保持风格一致,都用 try catch 来处理比较好。

  • 第二个问题,如果没有声明 await 为什么就不能 catch 到 _run 方法内部的异常了?我们可以带入一下上面的原理代码,如果没有加 await 直接就会走到 next.done 结束掉,而带上 await 则会走到 Promise.resolve(next.value).then 的部分,这个 next.value 就是 _run() 返回的 Promise,这样 _run() 被 reject 时就会进入到 step(function() { return gen.throw(e); }) 因而捕捉到异常。

  • 第三个问题,为什么说它比 Promise 的异常处理好?通过上面的原型代码,我们可以看到 async 函数可以通过 try { next = nextF(); } 捕获各层次代码中的语法错误等普通异常,也可以通过 Promise.resolve(next.value).then 的 onRejected 回调中的 gen.throw(e) 来捕捉 await 的 Promise 被 reject 的情况,异常的结果都是导致 async 函数作为 Promise 的整体被 reject。而对 Promise 包裹 try catch 是不能捕获 then 回调链中的普通异常的。

关于 async 函数的:

  • 第一个问题,如果我把一个普通函数给加上 async 定义,还能像从前一样用它吗?看了上面的代码后,答案已经比较明确了,加上了 async 之后它就变了,返回的值会是一个 Promise,至于能不能像以前一样用它,就得看情况了,如果它以前就是返回了一个 Promise,那你很可能还是可以保持它的使用方式的。

  • 第二个问题是关于 async 函数的 return 值,看上面代码 —— if (next.done) { resolve(next.value) } ,这其实就只是一个 Promise 的原理问题,是当 Promise a 被 resolve(x) 时,x 如果是 Promise,那 a 的回调就会被挂到 x 上,我们 await a 也就相当于 await x。

关键还是在于 Promise

要理解上面的内容,关键还是要先理解 Promise 的原理,如果有不清楚,建议再仔细看看 Promise 规范,特别是 定义 then 的部分The Promise Resolution Procedure。Then 的规范定义了 then 方法必须 return 一个 promise,而 await 后面的语句其实就是 then 方法中的 onFullfilled,因而 async 函数会 return 一个新的 promise,这个 promise 的行为也是遵循这个规范的。而 The Promise Resolution Procedure 是对如何处理 resolve 值的要求,这跟很多细节有关,比如上面提到的 async 函数 return 值,以及下面即将提到的 v8 团队对 async/await 的改造。在弄清了这两部分规范之后 async/await 也就容易理解了。

V8 关于 async/await 的 Blog

网上关于 async/await 比较有价值的一篇文章是 v8 团队对 async/await 的改进,主要是介绍了在 2018 年对 async/await 实现方式的改进,减少了一个对 Promise 的包装和传递过程, 这里 有对此比较详细的解释,只是把 new Promise(res => res(p)) 改成了 Promise.resolve(p) 就节省了两个 microTick。除此之外改进还减少了一个新建 throw away Promise 的过程,也就是在 then 的时候不返回新的 Promise,节省创建这个 Promise 的时间,不过这个并没有减少 microTick。在这次改良后 await 在执行顺序上就比较符合人们直观的预期了。

我的一点想法

从几年前的 Async/Await 完胜 Promise 的六个理由如何避免 async/await 地狱,关于 Async/await 的讨论还在继续。个人感觉 Async/await 是一个更好的工具,它让多层嵌套的复杂异步逻辑更直观、清晰,但是它对写代码的人提出了更高的要求,如果没有足够的了解,可能更容易写出 Bug。