本文首發(fā)在個人博客:http://muyunyun.cn/posts/7b9fdc87/
提到 Node.js, 我們腦海就會浮現(xiàn)異步、非阻塞、單線程等關鍵詞,進一步我們還會想到 buffer、模塊機制、事件循環(huán)、進程、V8、libuv 等知識點。本文起初旨在理順 Node.js 以上易混淆概念,然而一入異步深似海,本文嘗試基于 Node.js 的異步展開討論,其他的主題只能日后慢慢補上了。(附:亦可以把本文當作是樸靈老師所著的《深入淺出 Node.js》一書的小結)。
異步 I/0
Node.js 正是依靠構建了一套完善的高性能異步 I/0 框架,從而打破了 JavaScript 在服務器端止步不前的局面。
異步 I/0 VS 非阻塞 I/0
聽起來異步和非阻塞,同步和阻塞是相互對應的,從實際效果而言,異步和非阻塞都達到了我們并行 I/0 的目的,但是從計算機內核 I/0 而言,異步/同步和阻塞/非阻塞實際上是兩回事。
注意,操作系統(tǒng)內核對于 I/0 只有兩種方式:阻塞與非阻塞。
調用阻塞 I/0 的過程:
調用非阻塞 I/0 的過程:
在此先引人一個叫作輪詢
的技術。輪詢不同于回調,舉個生活例子,你有事去隔壁寢室找同學,發(fā)現(xiàn)人不在,你怎么辦呢?方法1,每隔幾分鐘再去趟隔壁寢室,看人在不;方法2,拜托與他同寢室的人,看到他回來時叫一下你;那么前者是輪詢,后者是回調。
再回到主題,阻塞 I/0 造成 CPU 等待浪費,非阻塞 I/0 帶來的麻煩卻是需要輪詢去確認是否完全完成數(shù)據(jù)獲取。從操作系統(tǒng)的這個層面上看,對于應用程序而言,不管是阻塞 I/0 亦或是 非阻塞 I/0,它們都只能是一種同步
,因為盡管使用了輪詢技術,應用程序仍然需要等待 I/0 完全返回。
Node 的異步 I/0
完成整個異步 I/O 環(huán)節(jié)的有事件循環(huán)、觀察者、請求對象以及 I/0 線程池。
事件循環(huán)
在進程啟動的時候,Node 會創(chuàng)建一個類似于 whlie(true) 的循環(huán),每一次執(zhí)行循環(huán)體的過程我們稱為 Tick。
每個 Tick 的過程就是查看是否有事件待處理,如果有,就取出事件及其相關的回調函數(shù)。如果存在相關的回調函數(shù),就執(zhí)行他們。然后進入下一個循環(huán),如果不再有事件處理,就退出進程。
偽代碼如下:
while(ture) {
const event = eventQueue.pop()
if (event && event.handler) {
event.handler.execute() // execute the callback in Javascript thread
} else {
sleep() // sleep some time to release the CPU do other stuff
}
}
觀察者
每個 Tick 的過程中,如何判斷是否有事件需要處理,這里就需要引入觀察者這個概念。
每個事件循環(huán)中有一個或多個觀察者,而判斷是否有事件需要處理的過程就是向這些觀察者詢問是否有要處理的事件。
在 Node 中,事件主要來源于網絡請求、文件 I/O 等,這些事件都有對應的觀察者。
請求對象
對于 Node 中的異步 I/O 而言,回調函數(shù)不由開發(fā)者來調用,在 JavaScript 發(fā)起調用到內核執(zhí)行完 id 操作的過渡過程中,存在一種中間產物,它叫作請求對象。
請求對象是異步 I/O 過程中的重要中間產物,所有狀態(tài)都保存在這個對象中,包括送入線程池等待執(zhí)行以及 I/O 操作完后的回調處理
以 fs.open()
為例:
fs.open = function(path, flags, mode, callback) {
bingding.open(
pathModule._makeLong(path),
stringToFlags(flags),
mode,
callback
)
}
fs.open
的作用就是根據(jù)指定路徑和參數(shù)去打開一個文件,從而得到一個文件描述符。
從前面的代碼中可以看到,JavaScript 層面的代碼通過調用 C++ 核心模塊進行下層的操作。
從 JavaScript 調用 Node 的核心模塊,核心模塊調用 C++ 內建模塊,內建模塊通過 libuv 進行系統(tǒng)調用,這是 Node 里經典的調用方式。
libuv 作為封裝層,有兩個平臺的實現(xiàn),實質上是調用了 uv_fs_open 方法,在 uv_fs_open 的調用過程中,會創(chuàng)建一個 FSReqWrap 請求對象,從 JavaScript 層傳入的參數(shù)和當前方法都被封裝在這個請求對象中。回調函數(shù)則被設置在這個對象的 oncomplete_sym 屬性上。
req_wrap -> object_ -> Set(oncomplete_sym, callback)
對象包裝完畢后,在 Windows 下,則調用 QueueUserWorkItem() 方法將這個 FSReqWrap 對象推人線程池中等待執(zhí)行。
至此,JavaScript 調用立即返回,由 JavaScript 層面發(fā)起的異步調用的第一階段就此結束(即上圖所注釋的異步 I/0 第一部分)。JavaScript 線程可以繼續(xù)執(zhí)行當前任務的后續(xù)操作,當前的 I/O 操作在線程池中等待執(zhí)行,不管它是否阻塞 I/O,都不會影響到 JavaScript 線程的后續(xù)操作,如此達到了異步的目的。
執(zhí)行回調
組裝好請求對象、送入 I/O 線程池等待執(zhí)行,實際上是完成了異步 I/O 的第一部分,回調通知是第二部分。
線程池中的 I/O 操作調用完畢之后,會將獲取的結果儲存在 req -> result
屬性上,然后調用 PostQueuedCompletionStatus()
通知 IOCP
,告知當前對象操作已經完成,并將線程歸還線程池。
在這個過程中,我們動用了事件循環(huán)的 I/O 觀察者,在每次 Tick
的執(zhí)行過程中,它會調用 IOCP
相關的 GetQueuedCompletionStatus
方法檢查線程池中是否有執(zhí)行完的請求,如果存在,會將請求對象加入到 I/O 觀察者的隊列中,然后將其當做事件處理。
I/O 觀察者回調函數(shù)的行為就是取出請求對象的 result
屬性作為參數(shù),取出 oncomplete_sym
屬性作為方法,然后調用執(zhí)行,以此達到調用 JavaScript 中傳入的回調函數(shù)的目的。
小結
通過介紹完整個異步 I/0 后,有個需要重視的觀點是 JavaScript 是單線程的,Node 本身其實是多線程的
,只是 I/0 線程使用的 CPU 比較少;還有個重要的觀點是,除了用戶的代碼無法并行執(zhí)行外,所有的 I/0 (磁盤 I/0 和網絡 I/0) 則是可以并行起來的。
異步編程
Node 是首個將異步大規(guī)模帶到應用層面的平臺。通過上文所述我們了解了 Node 如何通過事件循環(huán)實現(xiàn)異步 I/0,有異步 I/0 必然存在異步編程。異步編程的路經歷了太多坎坷,從回調函數(shù)、發(fā)布訂閱模式、Promise 對象,到 generator、asycn/await。趁著異步編程這個主題剛好把它們串起來理理。
異步 VS 回調
對于剛接觸異步的新人,很大幾率會混淆回調 (callback) 和異步 (asynchronous) 的概念。先來看看維基的 Callback 條目:
In computer programming, a callback is any executable code that is passed as an argument to other code
因此,回調本質上是一種設計模式,并且 jQuery (包括其他框架)的設計原則遵循了這個模式。
在 JavaScript 中,回調函數(shù)具體的定義為:函數(shù) A 作為參數(shù)(函數(shù)引用)傳遞到另一個函數(shù) B 中,并且這個函數(shù) B 執(zhí)行函數(shù) A。我們就說函數(shù) A 叫做回調函數(shù)。如果沒有名稱(函數(shù)表達式),就叫做匿名回調函數(shù)。
因此 callback 不一定用于異步,一般同步(阻塞)的場景下也經常用到回調,比如要求執(zhí)行某些操作后執(zhí)行回調函數(shù)。講了這么多讓我們來看下同步回調和異步回調的例子:
同步回調:
function f2() {
console.log('f2 finished')
}
function f1(cb) {
cb()
console.log('f1 finished')
}
f1(f2) // 得到的結果是 f2 finished, f1 finished
異步回調:
function f2() {
console.log('f2 finished')
}
function f1(cb) {
setTimeout(cb, 1000) // 通過 setTimeout() 來模擬耗時操作
console.log('f1 finished')
}
f1(f2) // 得到的結果是 f1 finished, f2 finished
小結:回調可以進行同步也可以異步調用,但是 Node.js 提供的 API 大多都是異步回調的,比如 buffer、http、cluster 等模塊。
發(fā)布/訂閱模式
事件發(fā)布/訂閱模式 (PubSub) 自身并無同步和異步調用的問題,但在 Node 的 events 模塊的調用中多半伴隨事件循環(huán)而異步觸發(fā)的,所以我們說事件發(fā)布/訂閱廣泛應用于異步編程。它的應用非常廣泛,可以在異步編程中幫助我們完成更松的解耦,甚至在 MVC、MVVC 的架構中以及設計模式中也少不了發(fā)布-訂閱模式的參與。
以 jQuery 事件監(jiān)聽為例
$('#btn').on('myEvent', function(e) { // 觸發(fā)事件
console.log('I am an Event')
})
$('#btn').trigger('myEvent') // 訂閱事件
可以看到,訂閱事件就是一個高階函數(shù)的應用。事件發(fā)布/訂閱模式可以實現(xiàn)一個事件與多個回調函數(shù)的關聯(lián),這些回調函數(shù)又稱為事件偵聽器。下面我們來看看發(fā)布/訂閱模式的簡易實現(xiàn)。
var PubSub = function() {
this.handlers = {}
}
PubSub.prototype.subscribe = function(eventType, handler) { // 注冊函數(shù)邏輯
if (!(eventType in this.handlers)) {
this.handlers[eventType] = []
}
this.handlers[eventType].push(handler) // 添加事件監(jiān)聽器
return this // 返回上下文環(huán)境以實現(xiàn)鏈式調用
}
PubSub.prototype.publish = function(eventType) { // 發(fā)布函數(shù)邏輯
var _args = Array.prototype.slice.call(arguments, 1)
for (var i = 0, _handlers = this.handlers[eventType]; i < _handlers.length; i++) { // 遍歷事件監(jiān)聽器
_handlers[i].apply(this, _args) // 調用事件監(jiān)聽器
}
}
var event = new PubSub // 構造 PubSub 實例
event.subscribe('name', function(msg) {
console.log('my name is ' + msg) // my name is muyy
})
event.publish('name', 'muyy')
至此,一個簡易的訂閱發(fā)布模式就實現(xiàn)了。然而發(fā)布/訂閱模式也存在一些缺點,創(chuàng)建訂閱本身會消耗一定的時間與內存,也許當你訂閱一個消息之后,之后可能就不會發(fā)生。發(fā)布-訂閱模式雖然它弱化了對象與對象之間的關系,但是如果過度使用,對象與對象的必要聯(lián)系就會被深埋,會導致程序難以跟蹤與維護。
Promise/Deferred 模式
想象一下,如果某個操作需要經過多個非阻塞的 IO 操作,每一個結果都是通過回調,程序有可能會看上去像這個樣子。這樣的代碼很難維護。這樣的情況更多的會發(fā)生在 server side 的情況下。代碼片段如下:
operation1(function(err, result1) {
operation2(result1, function(err, result2) {
operation3(result2, function(err, result3) {
operation4(result3, function(err, result4) {
callback(result4) // do something useful
})
})
})
})
這時候,Promise 出現(xiàn)了,其出現(xiàn)的目的就是為了解決所謂的回調地獄的問題。讓我們看下使用 Promise 后的代碼片段:
promise()
.then(operation1)
.then(operation2)
.then(operation3)
.then(operation4)
.then(function(value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done()
可以看到,使用了第二種編程模式后能極大地提高我們的編程體驗,接著就讓我們自己動手實現(xiàn)一個支持序列執(zhí)行的 Promise。(附:為了直觀的在瀏覽器上也能感受到 Promise,為此也寫了一段瀏覽器上的 Promise 用法示例)
在此之前,我們先要了解 Promise/A 提議中對單個異步操作所作的抽象定義,定義具體如下所示:
- Promise 操作只會處在 3 種狀態(tài)的一種:未完成態(tài)、完成態(tài)和失敗態(tài)。
- Promise 的狀態(tài)只會出現(xiàn)從未完成態(tài)向完成態(tài)或失敗態(tài)轉化,不能逆反。完成態(tài)和失敗態(tài)不能相互轉化。
- Promise 的狀態(tài)一旦轉化,將不能被更改。
Promise 的狀態(tài)轉化示意圖如下:
除此之外,Promise 對象的另一個關鍵就是需要具備 then() 方法,對于 then() 方法,有以下簡單的要求:
- 接受完成態(tài)、錯誤態(tài)的回調方法。在操作完成或出現(xiàn)錯誤時,將會調用對應方法。
- 可選地支持 progress 事件回調作為第三個方法。
- then() 方法只接受 function 對象,其余對象將被忽略。
- then() 方法繼續(xù)返回 Promise 對象,已實現(xiàn)鏈式調用。
then() 方法的定義如下:
then(fulfilledHandler, errorHandler, progressHandler)
有了這些核心知識,接著進入 Promise/Deferred 核心代碼環(huán)節(jié):
var Promise = function() { // 構建 Promise 對象
// 隊列用于存儲執(zhí)行的回調函數(shù)
this.queue = []
this.isPromise = true
}
Promise.prototype.then = function (fulfilledHandler, errorHandler, progressHandler) { // 構建 Progress 的 then 方法
var handler = {}
if (typeof fulfilledHandler === 'function') {
handler.fulfilled = fulfilledHandler
}
if (typeof errorHandler === 'function') {
handler.error = errorHandler
}
this.queue.push(handler)
return this
}
如上 Promise 的代碼就完成了,但是別忘了 Promise/Deferred 中的后者 Deferred,為了完成 Promise 的整個流程,我們還需要觸發(fā)執(zhí)行上述回調函數(shù)的地方,實現(xiàn)這些功能的對象就叫作 Deferred,即延遲對象。
Promise 和 Deferred 的整體關系如下圖所示,從中可知,Deferred 主要用于內部來維護異步模型的狀態(tài);而 Promise 則作用于外部,通過 then() 方法暴露給外部以添加自定義邏輯。
接著來看 Deferred 代碼部分的實現(xiàn):
var Deferred = function() {
this.promise = new Promise()
}
// 完成態(tài)
Deferred.prototype.resolve = function(obj) {
var promise = this.promise
var handler
while(handler = promise.queue.shift()) {
if (handler && handler.fulfilled) {
var ret = handler.fulfilled(obj)
if (ret && ret.isPromise) { // 這一行以及后面3行的意思是:一旦檢測到返回了新的 Promise 對象,停止執(zhí)行,然后將當前 Deferred 對象的 promise 引用改變?yōu)樾碌?Promise 對象,并將隊列中余下的回調轉交給它
ret.queue = promise.queue
this.promise = ret
return
}
}
}
}
// 失敗態(tài)
Deferred.prototype.reject = function(err) {
var promise = this.promise
var handler
while (handler = promise.queue.shift()) {
if (handler && handler.error) {
var ret = handler.error(err)
if (ret && ret.isPromise) {
ret.queue = promise.queue
this.promise = ret
return
}
}
}
}
// 生成回調函數(shù)
Deferred.prototype.callback = function() {
var that = this
return function(err, file) {
if(err) {
return that.reject(err)
}
that.resolve(file)
}
}
接著我們以兩次文件讀取作為例子,來驗證該設計的可行性。這里假設第二個文件讀取依賴于第一個文件中的內容,相關代碼如下:
var readFile1 = function(file, encoding) {
var deferred = new Deferred()
fs.readFile(file, encoding, deferred.callback())
return deferred.promise
}
var readFile2 = function(file, encoding) {
var deferred = new Deferred()
fs.readFile(file, encoding, deferred.callback())
return deferred.promise
}
readFile1('./file1.txt', 'utf8').then(function(file1) { // 這里通過 then 把兩個回調存進隊列中
return readFile2(file1, 'utf8')
}).then(function(file2) {
console.log(file2) // I am file2.
})
最后可以看到控制臺輸出 I am file2
,驗證成功~,這個案例的完整代碼可以點這里查看,并建議使用 node-inspector 進行斷點觀察,(這段代碼里面有些邏輯確實很繞,通過斷點調試就能較容易理解了)。
從 Promise 鏈式調用可以清晰地看到隊列(先進先出)的知識,其有如下兩個核心步驟:
- 將所有的回調都存到隊列中;
- Promise 完成時,逐個執(zhí)行回調,一旦檢測到返回了新的 Promise 對象,停止執(zhí)行,然后將當前 Deferred 對象的 promise 引用改變?yōu)樾碌?Promise 對象,并將隊列中余下的回調轉交給它;
至此,實現(xiàn)了 Promise/Deferred 的完整邏輯,Promise 的其他知識未來也會繼續(xù)探究。
Generator
盡管 Promise 一定程度解決了回調地獄的問題,但是對于喜歡簡潔的程序員來說,一大堆的模板代碼 .then(data => {...})
顯得不是很友好。所以愛折騰的開發(fā)者們在 ES6 中引人了 Generator 這種數(shù)據(jù)類型。仍然以讀取文件為例,先上一段非常簡潔的 Generator + co 的代碼:
co(function* () {
const file1 = yield readFile('./file1.txt')
const file2 = yield readFile('./file2.txt')
console.log(file1)
console.log(file2)
})
可以看到比 Promise 的寫法簡潔了許多。后文會給出 co 庫的實現(xiàn)原理。在此之前,先歸納下什么是 Generator。可以把 Generator 理解為一個可以遍歷的狀態(tài)機,調用 next 就可以切換到下一個狀態(tài),其最大特點就是可以交出函數(shù)的執(zhí)行權(即暫停執(zhí)行),讓我們看如下代碼:
function* gen(x) {
yield (function() {return 1})()
var y = yield x + 2
return y
}
// 調用方式一
var g = gen(1)
g.next() // { value: 1, done: false }
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
// 調用方式二
var g = gen(1)
g.next() // { value: 1, done: false }
g.next() // { value: 3, done: false }
g.next(10) // { value: 10, done: true }
由此我們歸納下 Generator 的基礎知識:
- Generator 生成迭代器后,等待迭代器的
next()
指令啟動。 - 啟動迭代器后,代碼會運行到
yield
處停止。并返回一個 {value: AnyType, done: Boolean} 對象,value 是這次執(zhí)行的結果,done 是迭代是否結束。并等待下一次的 next() 指令。 - next() 再次啟動,若 done 的屬性不為 true,則可以繼續(xù)從上一次停止的地方繼續(xù)迭代。
- 一直重復 2,3 步驟,直到 done 為 true。
- 通過調用方式二,我們可看到 next 方法可以帶一個參數(shù),該參數(shù)就會被當作
上一個 yield 語句的返回值
。
另外我們注意到,上述代碼中的第一種調用方式中的 y 值是 undefined,如果我們真想拿到 y 值,就需要通過 g.next(); g.next().value
這種方式取出。可以看出,Generator 函數(shù)將異步操作表示得很簡潔,但是流程管理卻不方便。這時候用于 Generator 函數(shù)的自動執(zhí)行的 co 函數(shù)庫 登場了。為什么 co 可以自動執(zhí)行 Generator 函數(shù)呢?我們知道,Generator 函數(shù)就是一個異步操作的容器。它的自動執(zhí)行需要一種機制,當異步操作有了結果,能夠自動交回執(zhí)行權。
兩種方法可以做到這一點:
- Thunk 函數(shù)。將異步操作包裝成 Thunk 函數(shù),在回調函數(shù)里面交回執(zhí)行權。
- Promise 對象。將異步操作包裝成 Promise 對象,用 then 方法交回執(zhí)行權。
co 函數(shù)庫其實就是將兩種自動自動執(zhí)行器(Thunk 函數(shù)和 Promise 對象),包裝成一個庫。使用 co 的前提條件是,Generator 函數(shù)的 yield 命令后面,只能是 Thunk 函數(shù)或者是 Promise 對象
。下面分別用以上兩種方法對 co 進行一個簡單的實現(xiàn)。
基于 Thunk 函數(shù)的自動執(zhí)行
在 JavaScript 中,Thunk 函數(shù)就是指將多參數(shù)函數(shù)替換成單參數(shù)的形式,并且其只接受回調函數(shù)作為參數(shù)的函數(shù)。Thunk 函數(shù)的例子如下:
// 正常版本的 readFile(多參數(shù))
fs.readFile(filename, 'utf8', callback)
// Thunk 版本的 readFile(單參數(shù))
function readFile(filename) {
return function(callback) {
fs.readFile(filename, 'utf8', callback);
};
}
在基于 Thunk 函數(shù)和 Generator 的知識上,接著我們來看看 co 基于 Thunk 函數(shù)的實現(xiàn)。(附:代碼參考自co最簡版實現(xiàn))
function co(generator) {
return function(fn) {
var gen = generator()
function next(err, result) {
if(err) {
return fn(err)
}
var step = gen.next(result)
if (!step.done) {
step.value(next) // 這里可以把它聯(lián)想成遞歸;將異步操作包裝成 Thunk 函數(shù),在回調函數(shù)里面交回執(zhí)行權。
} else {
fn(null, step.value)
}
}
next()
}
}
用法如下:
co(function* () { // 把 function*() 作為參數(shù) generator 傳入 co 函數(shù)
var file1 = yield readFile('./file1.txt')
var file2 = yield readFile('./file2.txt')
console.log(file1) // I'm file1
console.log(file2) // I'm file2
return 'done'
})(function(err, result) { // 這部分的 function 作為 co 函數(shù)內的 fn 的實參傳入
console.log(result) // done
})
上述部分關鍵代碼已進行注釋,下面對 co 函數(shù)里的幾個難點進行說明:
-
var step = gen.next(result)
, 前文提到的一句話在這里就很有用處了:next方法可以帶一個參數(shù),該參數(shù)就會被當作上一個yield語句的返回值
;在上述代碼的運行中一共會經過這個地方 3 次,result 的值第一次是空值,第二次是 file1.txt 的內容 I'm file1,第三次是 file2.txt 的內容 I'm file2。根據(jù)上述關鍵語句的提醒,所以第二次的內容會作為 file1 的值(當作上一個yield語句的返回值),同理第三次的內容會作為 file2 的值。 - 另一處是
step.value(next)
, step.value 就是前面提到的 thunk 函數(shù)返回的 function(callback) {}, next 就是傳入 thunk 函數(shù)的 callback。這句代碼是條遞歸語句,是這個簡易版 co 函數(shù)能自動調用 Generator 的關鍵語句。
建議親自跑一遍代碼,多打斷點,從而更好地理解,代碼已上傳github。
基于 Promise 對象的自動執(zhí)行
基于 Thunk 函數(shù)的自動執(zhí)行中,yield 后面需跟上 Thunk 函數(shù),在基于 Promise 對象的自動執(zhí)行中,yield 后面自然要跟 Promise 對象了,讓我們先構建一個 readFile 的
Promise 對象:
function readFile(fileName) {
return new Promise(function(resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) reject(error)
resolve(data)
})
})
}
在基于前文 Promise 對象和 Generator 的知識上,接著我們來看看 co 基于 Promise 函數(shù)的實現(xiàn):
function co(generator) {
var gen = generator()
function next(data) {
var result = gen.next(data) // 同上,經歷了 3 次,第一次是 undefined,第二次是 I'm file1,第三次是 I'm file2
if (result.done) return result.value
result.value.then(function(data) { // 將異步操作包裝成 Promise 對象,用 then 方法交回執(zhí)行權
next(data)
})
}
next()
}
用法如下:
co(function* generator() {
var file1 = yield readFile('./file1.txt')
var file2 = yield readFile('./file2.txt')
console.log(file1.toString()) // I'm file1
console.log(file2.toString()) // I'm file2
})
這一部分的代碼上傳在這里,通過觀察可以發(fā)現(xiàn)基于 Thunk 函數(shù)和基于 Promise 對象的自動執(zhí)行方案的 co 函數(shù)設計思路幾乎一致,也因此呼應了它們共同的本質 —— 當異步操作有了結果,自動交回執(zhí)行權。
async
看上去 Generator 已經足夠好用了,但是使用 Generator 處理異步必須得依賴 tj/co,于是 asycn 出來了。本質上 async 函數(shù)就是 Generator 函數(shù)的語法糖,這樣說是因為 async 函數(shù)的實現(xiàn),就是將 Generator 函數(shù)和自動執(zhí)行器,包裝進一個函數(shù)中。偽代碼如下,(注:其中 automatic 的實現(xiàn)可以參考 async 函數(shù)的含義和用法中的實現(xiàn))
async function fn(args){
// ...
}
// 等同于
function fn(args) {
return automatic(function*() { // automatic 函數(shù)就是自動執(zhí)行器,其的實現(xiàn)可以仿照 co 庫自動運行方案來實現(xiàn),這里就不展開了
// ...
})
}
接著仍然以上文的讀取文件為例,來比較 Generator 和 async 函數(shù)的寫法差異:
// Generator
var genReadFile = co(function*() {
var file1 = yield readFile('./file1.txt')
var file2 = yield readFile('./file2.txt')
})
// 改用 async 函數(shù)
var asyncReadFile = async function() {
var file1 = await readFile('./file1.txt')
var file2 = await 1 // 等同于同步操作(如果跟上原始類型的值)
}
總體來說 async/await 看上去和使用 co 庫后的 generator 看上去很相似,不過相較于 Generator,可以看到 Async 函數(shù)更優(yōu)秀的幾點:
- 內置執(zhí)行器。Generator 函數(shù)的執(zhí)行必須依靠執(zhí)行器,而 Aysnc 函數(shù)自帶執(zhí)行器,調用方式跟普通函數(shù)的調用一樣;
- 更好的語義。async 和 await 相較于 * 和 yield 更加語義化;
- 更廣的適用性。前文提到的 co 模塊約定,yield 命令后面只能是 Thunk 函數(shù)或 Promise 對象,而 async 函數(shù)的 await 命令后面則可以是 Promise 或者原始類型的值;
- 返回值是 Promise。async 函數(shù)返回值是 Promise 對象,比 Generator 函數(shù)返回的 Iterator 對象方便,因此可以直接使用 then() 方法進行調用;