[NodeJS] async 和 await 的本質
絕大多數nodejs程式設計師都會使用 async 和 await 關鍵字,但是極少有人能真正弄明白 async 和 await 的原理。這篇文章將從零“構建”出 async 和 await 關鍵字,從而幫助理清 async 和 await 的本質。
先用一句話概括:async 和 await 是內建了執行器的 generator 函式。
什麼是 generator 函式?顧名思義,generator 函式就是一個生成器。生成的是一個可以多次通過 .next() 迭代的物件,例如,定義一個 generator 函式如下:
let g = function* () { yield 1 yield 2 return 3 }
其中,yield 關鍵字定義每次迭代的返回值,最後一個返回值用 return。
然後,就可以用它來生成一個可迭代的物件:
let iter = g() console.log(iter.next()) console.log(iter.next()) console.log(iter.next()) console.log(iter.next())
以上程式碼執行的結果是:
{ value: 1, done: false } { value: 2, done: false } { value: 3, done: true } { value: undefined, done: true }
generator 函式也可以接收引數:
let g = function* (a, b) { yield a yield b return a + b } let iter = g(1, 2) console.log(iter.next()) console.log(iter.next()) console.log(iter.next()) console.log(iter.next())
執行結果:
{ value: 1, done: false } { value: 2, done: false } { value: 3, done: true } { value: undefined, done: true }
接下來是一個關鍵點:yield 關鍵字的值和 .next() 的引數的關係:
let g = function* () { let ret = yield 1 return ret } let iter = g() console.log(iter.next()) console.log(iter.next(2))
以上程式碼的執行結果是:
{ value: 1, done: false } { value: 2, done: true }
可以看出,第二次呼叫 .next() 的時候,傳入了引數2,這個 2 被賦值給了 ret。也就是說,
let ret = yield 1
這行程式碼其實是被拆成兩段執行的。第一次呼叫 .next() 的時候,g 裡面的程式碼開始執行,執行到了 yield 1 這裡,就暫停並返回了。這時列印 .next() 的返回值是 { value: 1, done: false }。然後,執行 .next(2) 的時候,又回到了 g 裡面的程式碼,從 let ret = 2 開始執行。
理清楚這一執行過程非常重要。因為,這意味著:
如果我在 g 裡面 yield 一個 Promise 出去,在外面等 Promise 執行完之後,再通過 .next() 的引數把結果傳進來,會怎樣呢?
let asyncSum = function(a, b) { return new Promise(resolve => { setTimeout(() => { resolve(a + b) }, 1000) }) } let g = function* () { let ret = yield asyncSum(1, 2) console.log(ret) return ret } let iter = g() let p = iter.next().value p.then(sum => { iter.next(sum) })
執行結果就是等待一秒之後打印出3:
// 這裡掛起了一秒鐘 3
請細細品味上面程式碼裡面的 g 函式:
let g = function* () { let ret = yield asyncSum(1, 2) console.log(ret) return ret }
將其與下面程式碼進行對比:
let g = async function () { let ret = await asyncSum(1, 2) console.log(ret) return ret }
發現了吧?事實上 async 函式就是 generator 函式。
讀者會問了,不對啊,我們呼叫 async 函式,都是直接呼叫,返回一個 Promise ,而不用像上面呼叫 g 那麼麻煩的。
沒錯。上面呼叫 g 的程式碼:
let iter = g() let p = iter.next().value p.then(sum => { iter.next(sum) })
叫做 g 的執行器。我們可以把它封裝起來:
let executor = function() { return new Promise(resolve => { let iter = g() let p = iter.next().value p.then(sum => { let ret = iter.next(sum) resolve(ret.value) }) }) } executor().then(ret => { console.log(ret) })
執行結果:
// 掛起一秒鐘 3 // g 裡面的 console.log(ret) 3 // .then 裡面的 console.log(ret)
實際上,node的執行引擎悄悄地幫我們做了上面的事情,當我們直接呼叫一個 async 函式時,其實是在呼叫它的執行器。
原理講到這裡就完了。下面是擴充套件部分。
上面的 executor 函式是僅僅針對這個例子裡面的 g 寫的。那我們是否可能寫一個通用的執行器函式,適用於任何 generator 函式呢?不管 generator 函式裡面有多少個 yield ,這個執行器是否都可以自動全部處理完?
答案當然是肯定的,用到了遞迴,請看完整程式碼:
let asyncSum = function(a, b) { return new Promise(resolve => { setTimeout(() => { resolve(a + b) }, 1000) }) } let asyncMul = function(a, b) { return new Promise(resolve => { setTimeout(() => { resolve(a * b) }, 1000) }) } let g = function* (a, b) { let sum = yield asyncSum(1, 2) let ret = yield asyncMul(sum, 2) return ret } function executor(generator, ...args) { let iter = generator.apply(this, args) let n = iter.next() if (n.done) { return new Promise(resolve => resolve(n.value)) } else { return new Promise(resolve => { n.value.then(ret => { _r(iter, ret, resolve) }); }); } } function _r(iter, ret, resolve) { let n = iter.next(ret) if (n.done) { resolve(n.value) } else { n.value.then(ret => { _r(iter, ret, resolve) }) } } executor(g, 1, 2).then(ret => { console.log(ret) })
執行結果:
// 這裡掛起了兩秒鐘 6
不過上面這個 executor 是個不完善的版本,因為沒有考慮錯誤的情況。其實早在 async 和 await 還沒有出現的 2013 年,著名程式設計師 TJ Holowaychuk 就寫了一個完善的 generator 執行器。專案地址:https://github.com/tj/co 。其名字叫 co。典型用法就是:
co(function* () { var result = yield Promise.resolve(true); return result; }).then(function (value) { console.log(value); }, function (err) { console.error(err.stack); });
關於 async 和 await 的本質,到這裡就結束啦。文章最後請細心的讀者思考一個問題:為什麼 TJ Holowaychuk 的這個模組名字要叫做 co?
&n