浏览器中的 JavaScript 程序是典型的事件驱动型程序,即它们会等待用户触发后才真正的执行,而基于的 JavaScript 的服务器通常要等待客户端通过网络发送请求,然后才能执行。这种异步编程在 JavaScript 是很常见的,下面就来介绍几个异步编程的重要特性,它们可以使编写异步代码更容易。
本文将按照异步编程方式的出现时间来归纳整理:

2. 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(); // something hello async
await 表达式的运算结果取决于它等的是什么:
- 如果它等到的不是一个 Promise 对象,那
await表达式的运算结果就是它等到的内容; - 如果它等到的是一个 Promise 对象,
await就就会阻塞后面的代码,等着 Promise 对象resolve,然后将得到的值作为await表达式的运算结果。
下面来看一个例子:
function testAsy(x){
return new Promise(resolve=>{setTimeout(() => {
resolve(x);
}, 3000)
}
)
}
async function testAwt(){
let result = await testAsy('hello world');
console.log(result); // 3 秒钟之后出现 hello world
console.log('cuger') // 3 秒钟之后出现 cug
}
testAwt();
console.log('cug') //立即输出 cug
这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。await暂停当前async的执行,所以’cug’最先输出,hello world’和 cuger 是 3 秒钟后同时出现的。
3. async/await 的优势
单一的 Promise 链并不能凸显 async/await 的优势。但是,如果处理流程比较复杂,那么整段代码将充斥着 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:vartest>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms
输出结果 result 是 step3() 的参数 700 + 200 = 900。doIt() 顺序执行了三个步骤,一共用了 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 实现是一样的,但是这个代码看起来会清晰得多,几乎和同步代码一样。
async/await对比 Promise 的优势就显而易见了:
- 代码读起来更加同步,Promise 虽然摆脱了回调地狱,但是
then的链式调⽤也会带来额外的理解负担; - Promise 传递中间值很麻烦,⽽
async/await⼏乎是同步的写法,⾮常优雅; - 错误处理友好,
async/await可以⽤成熟的try/catch,Promise 的错误捕获比较冗余; - 调试友好,Promise 的调试很差,由于没有代码块,不能在⼀个返回表达式的箭头函数中设置断点,如果在⼀个.then 代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的
.then代码块,因为调试器只能跟踪同步代码的每⼀步。
4. async/await 的异常处理
利用 async/await 的语法糖,可以像处理同步代码的异常一样,来处理异步代码,这里还用上面的示例:
const exe = (flag) => () => new Promise((resolve, reject) => {
console.log(flag);
setTimeout(() => {
flag ? resolve("yes") : reject("no");
}, 1000);
});
const run = async () => {
try {
await exe(false)();
await exe(true)();
} catch (e) {
console.log(e);
}
}
run();
这里定义一个异步方法 run,由于 await 后面需要直接跟 Promise 对象,因此通过额外的一个方法调用符号 () 把原有的 exe 方法内部的 Thunk 包装拆掉,即执行 exe(false)() 或 exe(true)() 返回的就是 Promise 对象。在 try 块之后,使用 catch 来捕捉。运行代码会得到这样的输出:
false no
这个 false 就是 exe 方法对入参的输出,而这个 no 就是 setTimeout 方法 reject 的回调返回,它通过异常捕获并最终在 catch 块中输出。就像我们所认识的同步代码一样,第四行的 exe(true) 并未得到执行。