隨著 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
現在拋開 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 起什麼作用
這個問題的關鍵在於,async 函數是怎麼處理它的回傳值的!我們當然希望它能直接透過return 語句傳回我們想要的值,但是如果真是這樣,似乎就沒await 什麼事了。所以,寫段程式碼來試試,看看它到底會回傳什麼:async function testAsync() { return "hello async"; } const result = testAsync(); console.log(result);
c:\var\test> node --harmony_async_await . Promise { 'hello async' }
testAsync().then(v => { console.log(v); // 输出 hello async });
聯想一下 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();
如果它等到的是一個 Promise 對象,await 就忙起來了,它會阻塞後面的程式碼,等著 Promise 物件 resolve,然後得到 resolve 的值,作為 await 表達式的運算結果。
看到上面的阻塞一詞,心慌了吧…放心,這就是 await 必須用在 async 函數中的原因。 async 函數呼叫不會造成阻塞,它內部所有的阻塞都被封裝在一個 Promise 物件中非同步執行。async/await 幫我們乾了啥
🎜🎜作個簡單的比較🎜🎜上面已經說明了async 會將其後的函數(函數表達式或Lambda)的傳回值封裝成一個Promise 物件,而Pawait會等待這個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
输出结果 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 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样
还有更酷的
现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。
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个步骤,再等待所有结果,又该怎么处理呢?