浏览器中的 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)
并未得到执行。