理解 JavaScript 的 async/await

242

随着 Node 7 的发布,越来越多的人开始研究据说是异步编程终级解决方案的 async/await。我第一次看到这组关键字并不是在 JavaScript 语言里,而是在 c# 5.0 的语法中。C# 的 async/await 需要在 .NET Framework 4.5 以上的版本中使用,因此我还很悲伤了一阵——为了要兼容 XP 系统,我们开发的软件不能使用高于 4.0 版本的 .NET Framework。

我之前在《闲谈异步调用“扁平”化》 中就谈到了这个问题。无论是在 C# 还是 JavaScript 中,async/await 都是非常棒的特性,它们也都是非常甜的语法糖。C# 的 async/await 实现离不开 Task 或 Task<Result> 类,而 JavaScript 的 async/await 实现,也离不开 Promise

现在抛开 C# 和 .NET Framework,专心研究下 JavaScript 的 async/await。

async 和 await 在干什么

任意一个名称都是有意义的,先从字面意思来理解。async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

另外还有一个很有意思的语法规定,await 只能出现在 async 函数中。然后细心的朋友会产生一个疑问,如果 await 只能出现在 async 函数中,那这个 async 函数应该怎么调用?

如果需要通过 await 来调用一个 async 函数,那这个调用的外面必须得再包一个 async 函数,然后……进入死循环,永无出头之日……

如果 async 函数不需要 await 来调用,那 async 到底起个啥作用?

async 起什么作用

这个问题的关键在于,async 函数是怎么处理它的返回值的!

我们当然希望它能直接通过 return 语句返回我们想要的值,但是如果真是这样,似乎就没 await 什么事了。所以,写段代码来试试,看它到底会返回什么:

async function testAsync() {
    return "hello async";
}

const result = testAsync();
console.log(result);

看到输出就恍然大悟了——输出的是一个 Promise 对象。

c:\var\test> node --harmony_async_await .
Promise { 'hello async' }

所以,async 函数返回的是一个 Promise 对象。从文档中也可以得到这个信息。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样

testAsync().then(v => {
    console.log(v);    // 输出 hello async
});

现在回过头来想下,如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

那么下一个关键点就在于 await 关键字了。

await 到底在等啥

一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行

function getSomething() {
    return "something";
}

async function testAsync() {
    return Promise.resolve("hello async");
}

async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}

test();

await 等到了要等的,然后呢

await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?我不得不先说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

看到上面的阻塞一词,心慌了吧……放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。

async/await 帮我们干了啥

作个简单的比较

上面已经说明了 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

takeLongTime().then(v => {
    console.log("got", v);
});

如果改用 async/await 呢,会是这样

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

async function test() {
    const v = await takeLongTime();
    console.log(v);
}

test();

眼尖的同学已经发现 takeLongTime() 没有申明为 async。实际上,takeLongTime() 本身就是返回的 Promise 对象,加不加 async 结果都一样,如果没明白,请回过头再去看看上面的“async 起什么作用”。

又一个疑问产生了,这两段代码,两种方式对异步调用的处理(实际就是对 Promise 对象的处理)差别并不明显,甚至使用 async/await 还需要多写一些代码,那它的优势到底在哪?

async/await 的优势在于处理 then 链

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

输出结果 resultstep3() 的参数 700 + 200 = 900doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

如果用 async/await 来实现呢,会是这样

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样

还有更酷的

现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(m, n) {
    console.log(`step2 with ${m} and ${n}`);
    return takeLongTime(m + n);
}

function step3(k, m, n) {
    console.log(`step3 with ${k}, ${m} and ${n}`);
    return takeLongTime(k + m + n);
}

这回先用 async/await 来写:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2907.387ms

除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => {
            return step2(time1, time2)
                .then(time3 => [time1, time2, time3]);
        })
        .then(times => {
            const [time1, time2, time3] = times;
            return step3(time1, time2, time3);
        })
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

有没有感觉有点复杂的样子?那一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,看着就晕!

洗洗睡吧

就目前来说,已经理解 async/await 了吧?但其实还有一些事情没提及——Promise 有可能 reject 啊,怎么处理呢?如果需要并行处理3个步骤,再等待所有结果,又该怎么处理呢?

阮一峰老师已经说过了,我就懒得说了。


如果觉得我的文章对你有用,请随意赞赏
已赞赏
40 条评论
pc813 · 2017年03月06日

边城 你好,我把此文章放到了 linkedinfo.co ,我的一个业余小项目。如您觉得不妥,请随时联系我将之撤下。

linkedinfo.co 是一个集合了各类优秀技术文章的站点,不展现全文,所有条目都附上作者与原文链接,读者最终都会进入到作者原文的站点。做 linkedinfo 的初衷是想方便自己且方便其他想学习的朋友,能更方便地找到自己感兴趣的技术文章。同时这也是一个我自己练习的过程,比如将正在学习的 Semantic Web 技术应用到 linkedinfo 来更好地连接不同的知识。

各位感兴趣的朋友可以去 https://linkedinfo.co 看看,非常希望能得到你们的意见与建议 (可在about 页面留言 https://linkedinfo.co/about )。

+3 回复

纪风夜影 · 6月15日

之前一直疑惑,直到这一句话

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

点赞+关注

+3 回复

陈浩 · 2017年09月27日

最后的例子加个外层变量,也很清晰的,当然,await更清晰,只是promise没有那么不堪而已 :)

// promise 写法
var p1 = 1, p2, p3;
step1(p1)
.then((param) => step2(p1, p2=param))
.then((param) => step3(p1, p2, p3=param))
.then((param) => {console.log('result is', param);});

PS:文章很不错,受教了

+2 回复

0

对,这样做也是可以的,举例举得稍微极端一些

边城 作者 · 2017年09月27日
Surmon · 2016年12月06日

很赞

回复

Evan_Wing · 2017年07月24日

文章很不错,简单易懂,受教了

回复

encorehwang · 2017年09月07日

你好, 我可以把你的文章转载我的博客吗, 会表明作者和出处

回复

0

可以的。欢迎在标记作者和出处的情况下转载。

边城 作者 · 2017年09月07日
我是包子脸 · 2017年12月21日

赞啊

回复

Miss_Ye · 1月18日

文章很好,深入浅出! 已收藏到自己的文章中,且标明作者及出处,谢谢分享!

回复

0

感谢!

边城 作者 · 1月18日
木杉树 · 1月24日

写的特别好~

回复

董继强 · 3月15日

感谢感谢, 写的太好了

回复

diermengdierme · 3月27日

您写的比阮老师写的好 ,反正我看他写的楞是没看懂.

回复

木子喵 · 3月28日

很不错

回复

执着的技术学员 · 4月2日

可以理解成async 和Promise的作用一样?都是非阻塞,await的作用就是吧一个异步及执行的代码变成同步执行?那加上await会不会阻塞呢?

回复

0

await 代码看起来像是阻塞的,实际看后面的代码是异步代码还是同步代码,如果是同步代码就是阻塞的,异步代码就不是。

边城 作者 · 4月3日
0

我也对这个不是明白,感觉用了await之后异步的代码就变成了阻塞式的,必须等待完成。

Alex · 6月6日
0

@Alex 因为举的例子是方法彼此同步的,必然有同步等待的

flcwl · 6月15日
YiHzo · 4月12日

我觉得对我最有用的是,解释了async和await的作用是干嘛的,这也是很多文章没讲到的,对我非常有用,万分感谢!!!

回复

lawpachi · 4月19日

请教下作者,async/await 是不是属于microtasks。

回复

0

async/await 是属于语法层面的东西,microtask 是属于实现层面的东西,两者有关系,但可能不存在从属关系。我对于 ES 规范并没有深入研究,这个问题可能 @justjavac 会更清楚。 @justjavac ,如果您方便的话,麻烦来回答一下

边城 作者 · 4月20日
0

@边城 这个在 Promise 规范里面定义的,属于 microtasks。setTimeout 是属于 macrotasks

justjavac · 4月20日
0

回复错人了

justjavac · 4月20日
龚国涛 · 5月4日

async 到底起什么作用还是没说啊

回复

1

async 声明一个函数是异步函数。异步函数会自动(由解释器)使用 Promise 封装返回值,同时,其内部可以使用 await 关键字。最终由 async/await/Promise 配合由解释器将类似同步方式编写的代码以正确的异步逻辑来调用

边城 作者 · 5月4日
vaeer · 6月3日

async和await能不能理解为生成器里function* 和yield呢

回复

0

从语法糖的角度,你要这么理解也没什么不对,反正就是用一两个关键字或者简单的语法结构来代替大量代码。但是原理上还是有一些区别,建议你不要拿他们两个去类比。

边城 作者 · 6月4日
Alex · 6月6日

你好,如果在doIt函数中结尾添加上console.log("over");,那么用await的写法会发现over是在最后打印出来的,但是如果用then则over在中间就打印出来了,那么给我的感觉就是await是阻塞的。

回复

0

没明白你的写法……不过 await 在异步操作的整个流程中是阻塞形式,因为它要等待后面的异步操作完成才执行下一句代码,但本身运行不是阻塞的,因为在整个异步流程之外(也就是往外找直到某个 async 函数调用未被 await),并未等待。

边城 作者 · 6月7日
flcwl · 6月15日

function takeLongTime(n) {

return new Promise(resolve => {
    setTimeout(() => resolve(n + 200), n);
});

}
const result = await step3(time3);
您好,我是小白,请教 resolves()是回调函数,awiat是直接获取这个Promise回调传递的参数n+200吗?

回复

0

你可以这么理解,但实际的处理过程是一个很复杂的机制。你也可以这样理解:resolve(n + 200) 会触发 Promise 对象的一个事件(也就是 then(...) 啦,而这个事件会得到 n + 200 这个值,继续后面的处理。而 await 把这些代码简化了,它简单地把 await 下面的语句封装成上述 then(...) 中注入的事件处理函数。所以 resolve() 触发事件,也就等于是继续处理 await 下面的语句。

边城 作者 · 6月16日
犬犬七 · 6月22日

您好!想把您的文章转到我们的微信公众号您看可以吗?会注明作者和出处,谢谢您!

回复

0

可以啊,欢迎转载!

边城 作者 · 6月22日
载入中...