摘要:的異步完成整個(gè)異步環(huán)節(jié)的有事件循環(huán)觀(guān)察者請(qǐng)求對(duì)象以及線(xiàn)程池。執(zhí)行回調(diào)組裝好請(qǐng)求對(duì)象送入線(xiàn)程池等待執(zhí)行,實(shí)際上是完成了異步的第一部分,回調(diào)通知是第二部分。異步編程是首個(gè)將異步大規(guī)模帶到應(yīng)用層面的平臺(tái)。
本文首發(fā)在個(gè)人博客:http://muyunyun.cn/posts/7b9fdc87/
提到 Node.js, 我們腦海就會(huì)浮現(xiàn)異步、非阻塞、單線(xiàn)程等關(guān)鍵詞,進(jìn)一步我們還會(huì)想到 buffer、模塊機(jī)制、事件循環(huán)、進(jìn)程、V8、libuv 等知識(shí)點(diǎn)。本文起初旨在理順 Node.js 以上易混淆概念,然而一入異步深似海,本文嘗試基于 Node.js 的異步展開(kāi)討論,其他的主題只能日后慢慢補(bǔ)上了。(附:亦可以把本文當(dāng)作是樸靈老師所著的《深入淺出 Node.js》一書(shū)的小結(jié))。
異步 I/0Node.js 正是依靠構(gòu)建了一套完善的高性能異步 I/0 框架,從而打破了 JavaScript 在服務(wù)器端止步不前的局面。
異步 I/0 VS 非阻塞 I/0聽(tīng)起來(lái)異步和非阻塞,同步和阻塞是相互對(duì)應(yīng)的,從實(shí)際效果而言,異步和非阻塞都達(dá)到了我們并行 I/0 的目的,但是從計(jì)算機(jī)內(nèi)核 I/0 而言,異步/同步和阻塞/非阻塞實(shí)際上是兩回事。
注意,操作系統(tǒng)內(nèi)核對(duì)于 I/0 只有兩種方式:阻塞與非阻塞。
調(diào)用阻塞 I/0 的過(guò)程:
調(diào)用非阻塞 I/0 的過(guò)程:
在此先引人一個(gè)叫作輪詢(xún)的技術(shù)。輪詢(xún)不同于回調(diào),舉個(gè)生活例子,你有事去隔壁寢室找同學(xué),發(fā)現(xiàn)人不在,你怎么辦呢?方法1,每隔幾分鐘再去趟隔壁寢室,看人在不;方法2,拜托與他同寢室的人,看到他回來(lái)時(shí)叫一下你;那么前者是輪詢(xún),后者是回調(diào)。
再回到主題,阻塞 I/0 造成 CPU 等待浪費(fèi),非阻塞 I/0 帶來(lái)的麻煩卻是需要輪詢(xún)?nèi)ゴ_認(rèn)是否完全完成數(shù)據(jù)獲取。從操作系統(tǒng)的這個(gè)層面上看,對(duì)于應(yīng)用程序而言,不管是阻塞 I/0 亦或是 非阻塞 I/0,它們都只能是一種同步,因?yàn)楸M管使用了輪詢(xún)技術(shù),應(yīng)用程序仍然需要等待 I/0 完全返回。
Node 的異步 I/0完成整個(gè)異步 I/O 環(huán)節(jié)的有事件循環(huán)、觀(guān)察者、請(qǐng)求對(duì)象以及 I/0 線(xiàn)程池。
事件循環(huán)在進(jìn)程啟動(dòng)的時(shí)候,Node 會(huì)創(chuàng)建一個(gè)類(lèi)似于 whlie(true) 的循環(huán),每一次執(zhí)行循環(huán)體的過(guò)程我們稱(chēng)為 Tick。
每個(gè) Tick 的過(guò)程就是查看是否有事件待處理,如果有,就取出事件及其相關(guān)的回調(diào)函數(shù)。如果存在相關(guān)的回調(diào)函數(shù),就執(zhí)行他們。然后進(jìn)入下一個(gè)循環(huán),如果不再有事件處理,就退出進(jì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 } }觀(guān)察者
每個(gè) Tick 的過(guò)程中,如何判斷是否有事件需要處理,這里就需要引入觀(guān)察者這個(gè)概念。
每個(gè)事件循環(huán)中有一個(gè)或多個(gè)觀(guān)察者,而判斷是否有事件需要處理的過(guò)程就是向這些觀(guān)察者詢(xún)問(wèn)是否有要處理的事件。
在 Node 中,事件主要來(lái)源于網(wǎng)絡(luò)請(qǐng)求、文件 I/O 等,這些事件都有對(duì)應(yīng)的觀(guān)察者。
請(qǐng)求對(duì)象對(duì)于 Node 中的異步 I/O 而言,回調(diào)函數(shù)不由開(kāi)發(fā)者來(lái)調(diào)用,在 JavaScript 發(fā)起調(diào)用到內(nèi)核執(zhí)行完 id 操作的過(guò)渡過(guò)程中,存在一種中間產(chǎn)物,它叫作請(qǐng)求對(duì)象。
請(qǐng)求對(duì)象是異步 I/O 過(guò)程中的重要中間產(chǎn)物,所有狀態(tài)都保存在這個(gè)對(duì)象中,包括送入線(xiàn)程池等待執(zhí)行以及 I/O 操作完后的回調(diào)處理
以 fs.open() 為例:
fs.open = function(path, flags, mode, callback) { bingding.open( pathModule._makeLong(path), stringToFlags(flags), mode, callback ) }
fs.open 的作用就是根據(jù)指定路徑和參數(shù)去打開(kāi)一個(gè)文件,從而得到一個(gè)文件描述符。
從前面的代碼中可以看到,JavaScript 層面的代碼通過(guò)調(diào)用 C++ 核心模塊進(jìn)行下層的操作。
從 JavaScript 調(diào)用 Node 的核心模塊,核心模塊調(diào)用 C++ 內(nèi)建模塊,內(nèi)建模塊通過(guò) libuv 進(jìn)行系統(tǒng)調(diào)用,這是 Node 里經(jīng)典的調(diào)用方式。
libuv 作為封裝層,有兩個(gè)平臺(tái)的實(shí)現(xiàn),實(shí)質(zhì)上是調(diào)用了 uv_fs_open 方法,在 uv_fs_open 的調(diào)用過(guò)程中,會(huì)創(chuàng)建一個(gè) FSReqWrap 請(qǐng)求對(duì)象,從 JavaScript 層傳入的參數(shù)和當(dāng)前方法都被封裝在這個(gè)請(qǐng)求對(duì)象中?;卣{(diào)函數(shù)則被設(shè)置在這個(gè)對(duì)象的 oncomplete_sym 屬性上。
req_wrap -> object_ -> Set(oncomplete_sym, callback)
對(duì)象包裝完畢后,在 Windows 下,則調(diào)用 QueueUserWorkItem() 方法將這個(gè) FSReqWrap 對(duì)象推人線(xiàn)程池中等待執(zhí)行。
至此,JavaScript 調(diào)用立即返回,由 JavaScript 層面發(fā)起的異步調(diào)用的第一階段就此結(jié)束(即上圖所注釋的異步 I/0 第一部分)。JavaScript 線(xiàn)程可以繼續(xù)執(zhí)行當(dāng)前任務(wù)的后續(xù)操作,當(dāng)前的 I/O 操作在線(xiàn)程池中等待執(zhí)行,不管它是否阻塞 I/O,都不會(huì)影響到 JavaScript 線(xiàn)程的后續(xù)操作,如此達(dá)到了異步的目的。
執(zhí)行回調(diào)組裝好請(qǐng)求對(duì)象、送入 I/O 線(xiàn)程池等待執(zhí)行,實(shí)際上是完成了異步 I/O 的第一部分,回調(diào)通知是第二部分。
線(xiàn)程池中的 I/O 操作調(diào)用完畢之后,會(huì)將獲取的結(jié)果儲(chǔ)存在 req -> result 屬性上,然后調(diào)用 PostQueuedCompletionStatus() 通知 IOCP,告知當(dāng)前對(duì)象操作已經(jīng)完成,并將線(xiàn)程歸還線(xiàn)程池。
在這個(gè)過(guò)程中,我們動(dòng)用了事件循環(huán)的 I/O 觀(guān)察者,在每次 Tick 的執(zhí)行過(guò)程中,它會(huì)調(diào)用 IOCP 相關(guān)的 GetQueuedCompletionStatus 方法檢查線(xiàn)程池中是否有執(zhí)行完的請(qǐng)求,如果存在,會(huì)將請(qǐng)求對(duì)象加入到 I/O 觀(guān)察者的隊(duì)列中,然后將其當(dāng)做事件處理。
I/O 觀(guān)察者回調(diào)函數(shù)的行為就是取出請(qǐng)求對(duì)象的 result 屬性作為參數(shù),取出 oncomplete_sym 屬性作為方法,然后調(diào)用執(zhí)行,以此達(dá)到調(diào)用 JavaScript 中傳入的回調(diào)函數(shù)的目的。
小結(jié)通過(guò)介紹完整個(gè)異步 I/0 后,有個(gè)需要重視的觀(guān)點(diǎn)是 JavaScript 是單線(xiàn)程的,Node 本身其實(shí)是多線(xiàn)程的,只是 I/0 線(xiàn)程使用的 CPU 比較少;還有個(gè)重要的觀(guān)點(diǎn)是,除了用戶(hù)的代碼無(wú)法并行執(zhí)行外,所有的 I/0 (磁盤(pán) I/0 和網(wǎng)絡(luò) I/0) 則是可以并行起來(lái)的。
異步編程Node 是首個(gè)將異步大規(guī)模帶到應(yīng)用層面的平臺(tái)。通過(guò)上文所述我們了解了 Node 如何通過(guò)事件循環(huán)實(shí)現(xiàn)異步 I/0,有異步 I/0 必然存在異步編程。異步編程的路經(jīng)歷了太多坎坷,從回調(diào)函數(shù)、發(fā)布訂閱模式、Promise 對(duì)象,到 generator、asycn/await。趁著異步編程這個(gè)主題剛好把它們串起來(lái)理理。
異步 VS 回調(diào)對(duì)于剛接觸異步的新人,很大幾率會(huì)混淆回調(diào) (callback) 和異步 (asynchronous) 的概念。先來(lái)看看維基的 Callback) 條目:
In computer programming, a callback is any executable code that is passed as an argument to other code
因此,回調(diào)本質(zhì)上是一種設(shè)計(jì)模式,并且 jQuery (包括其他框架)的設(shè)計(jì)原則遵循了這個(gè)模式。
在 JavaScript 中,回調(diào)函數(shù)具體的定義為:函數(shù) A 作為參數(shù)(函數(shù)引用)傳遞到另一個(gè)函數(shù) B 中,并且這個(gè)函數(shù) B 執(zhí)行函數(shù) A。我們就說(shuō)函數(shù) A 叫做回調(diào)函數(shù)。如果沒(méi)有名稱(chēng)(函數(shù)表達(dá)式),就叫做匿名回調(diào)函數(shù)。
因此 callback 不一定用于異步,一般同步(阻塞)的場(chǎng)景下也經(jīng)常用到回調(diào),比如要求執(zhí)行某些操作后執(zhí)行回調(diào)函數(shù)。講了這么多讓我們來(lái)看下同步回調(diào)和異步回調(diào)的例子:
同步回調(diào):
function f2() { console.log("f2 finished") } function f1(cb) { cb() console.log("f1 finished") } f1(f2) // 得到的結(jié)果是 f2 finished, f1 finished
異步回調(diào):
function f2() { console.log("f2 finished") } function f1(cb) { setTimeout(cb, 1000) // 通過(guò) setTimeout() 來(lái)模擬耗時(shí)操作 console.log("f1 finished") } f1(f2) // 得到的結(jié)果是 f1 finished, f2 finished
小結(jié):回調(diào)可以進(jìn)行同步也可以異步調(diào)用,但是 Node.js 提供的 API 大多都是異步回調(diào)的,比如 buffer、http、cluster 等模塊。
發(fā)布/訂閱模式事件發(fā)布/訂閱模式 (PubSub) 自身并無(wú)同步和異步調(diào)用的問(wèn)題,但在 Node 的 events 模塊的調(diào)用中多半伴隨事件循環(huán)而異步觸發(fā)的,所以我們說(shuō)事件發(fā)布/訂閱廣泛應(yīng)用于異步編程。它的應(yīng)用非常廣泛,可以在異步編程中幫助我們完成更松的解耦,甚至在 MVC、MVVC 的架構(gòu)中以及設(shè)計(jì)模式中也少不了發(fā)布-訂閱模式的參與。
以 jQuery 事件監(jiān)聽(tīng)為例
$("#btn").on("myEvent", function(e) { // 觸發(fā)事件 console.log("I am an Event") }) $("#btn").trigger("myEvent") // 訂閱事件
可以看到,訂閱事件就是一個(gè)高階函數(shù)的應(yīng)用。事件發(fā)布/訂閱模式可以實(shí)現(xiàn)一個(gè)事件與多個(gè)回調(diào)函數(shù)的關(guān)聯(lián),這些回調(diào)函數(shù)又稱(chēng)為事件偵聽(tīng)器。下面我們來(lái)看看發(fā)布/訂閱模式的簡(jiǎn)易實(shí)現(xiàn)。
var PubSub = function() { this.handlers = {} } PubSub.prototype.subscribe = function(eventType, handler) { // 注冊(cè)函數(shù)邏輯 if (!(eventType in this.handlers)) { this.handlers[eventType] = [] } this.handlers[eventType].push(handler) // 添加事件監(jiān)聽(tīng)器 return this // 返回上下文環(huán)境以實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用 } 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)聽(tīng)器 _handlers[i].apply(this, _args) // 調(diào)用事件監(jiān)聽(tīng)器 } } var event = new PubSub // 構(gòu)造 PubSub 實(shí)例 event.subscribe("name", function(msg) { console.log("my name is " + msg) // my name is muyy }) event.publish("name", "muyy")
至此,一個(gè)簡(jiǎn)易的訂閱發(fā)布模式就實(shí)現(xiàn)了。然而發(fā)布/訂閱模式也存在一些缺點(diǎn),創(chuàng)建訂閱本身會(huì)消耗一定的時(shí)間與內(nèi)存,也許當(dāng)你訂閱一個(gè)消息之后,之后可能就不會(huì)發(fā)生。發(fā)布-訂閱模式雖然它弱化了對(duì)象與對(duì)象之間的關(guān)系,但是如果過(guò)度使用,對(duì)象與對(duì)象的必要聯(lián)系就會(huì)被深埋,會(huì)導(dǎo)致程序難以跟蹤與維護(hù)。
Promise/Deferred 模式想象一下,如果某個(gè)操作需要經(jīng)過(guò)多個(gè)非阻塞的 IO 操作,每一個(gè)結(jié)果都是通過(guò)回調(diào),程序有可能會(huì)看上去像這個(gè)樣子。這樣的代碼很難維護(hù)。這樣的情況更多的會(huì)發(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 }) }) }) })
這時(shí)候,Promise 出現(xiàn)了,其出現(xiàn)的目的就是為了解決所謂的回調(diào)地獄的問(wè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()
可以看到,使用了第二種編程模式后能極大地提高我們的編程體驗(yàn),接著就讓我們自己動(dòng)手實(shí)現(xiàn)一個(gè)支持序列執(zhí)行的 Promise。(附:為了直觀(guān)的在瀏覽器上也能感受到 Promise,為此也寫(xiě)了一段瀏覽器上的 Promise 用法示例)
在此之前,我們先要了解 Promise/A 提議中對(duì)單個(gè)異步操作所作的抽象定義,定義具體如下所示:
Promise 操作只會(huì)處在 3 種狀態(tài)的一種:未完成態(tài)、完成態(tài)和失敗態(tài)。
Promise 的狀態(tài)只會(huì)出現(xiàn)從未完成態(tài)向完成態(tài)或失敗態(tài)轉(zhuǎn)化,不能逆反。完成態(tài)和失敗態(tài)不能相互轉(zhuǎn)化。
Promise 的狀態(tài)一旦轉(zhuǎn)化,將不能被更改。
Promise 的狀態(tài)轉(zhuǎn)化示意圖如下:
除此之外,Promise 對(duì)象的另一個(gè)關(guān)鍵就是需要具備 then() 方法,對(duì)于 then() 方法,有以下簡(jiǎn)單的要求:
接受完成態(tài)、錯(cuò)誤態(tài)的回調(diào)方法。在操作完成或出現(xiàn)錯(cuò)誤時(shí),將會(huì)調(diào)用對(duì)應(yīng)方法。
可選地支持 progress 事件回調(diào)作為第三個(gè)方法。
then() 方法只接受 function 對(duì)象,其余對(duì)象將被忽略。
then() 方法繼續(xù)返回 Promise 對(duì)象,已實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用。
then() 方法的定義如下:
then(fulfilledHandler, errorHandler, progressHandler)
有了這些核心知識(shí),接著進(jìn)入 Promise/Deferred 核心代碼環(huán)節(jié):
var Promise = function() { // 構(gòu)建 Promise 對(duì)象 // 隊(duì)列用于存儲(chǔ)執(zhí)行的回調(diào)函數(shù) this.queue = [] this.isPromise = true } Promise.prototype.then = function (fulfilledHandler, errorHandler, progressHandler) { // 構(gòu)建 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 的整個(gè)流程,我們還需要觸發(fā)執(zhí)行上述回調(diào)函數(shù)的地方,實(shí)現(xiàn)這些功能的對(duì)象就叫作 Deferred,即延遲對(duì)象。
Promise 和 Deferred 的整體關(guān)系如下圖所示,從中可知,Deferred 主要用于內(nèi)部來(lái)維護(hù)異步模型的狀態(tài);而 Promise 則作用于外部,通過(guò) then() 方法暴露給外部以添加自定義邏輯。
接著來(lái)看 Deferred 代碼部分的實(shí)現(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行的意思是:一旦檢測(cè)到返回了新的 Promise 對(duì)象,停止執(zhí)行,然后將當(dāng)前 Deferred 對(duì)象的 promise 引用改變?yōu)樾碌?Promise 對(duì)象,并將隊(duì)列中余下的回調(diào)轉(zhuǎn)交給它 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 } } } } // 生成回調(diào)函數(shù) Deferred.prototype.callback = function() { var that = this return function(err, file) { if(err) { return that.reject(err) } that.resolve(file) } }
接著我們以?xún)纱挝募x取作為例子,來(lái)驗(yàn)證該設(shè)計(jì)的可行性。這里假設(shè)第二個(gè)文件讀取依賴(lài)于第一個(gè)文件中的內(nèi)容,相關(guān)代碼如下:
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) { // 這里通過(guò) then 把兩個(gè)回調(diào)存進(jìn)隊(duì)列中 return readFile2(file1, "utf8") }).then(function(file2) { console.log(file2) // I am file2. })
最后可以看到控制臺(tái)輸出 I am file2,驗(yàn)證成功~,這個(gè)案例的完整代碼可以點(diǎn)這里查看,并建議使用 node-inspector 進(jìn)行斷點(diǎn)觀(guān)察,(這段代碼里面有些邏輯確實(shí)很繞,通過(guò)斷點(diǎn)調(diào)試就能較容易理解了)。
從 Promise 鏈?zhǔn)秸{(diào)用可以清晰地看到隊(duì)列(先進(jìn)先出)的知識(shí),其有如下兩個(gè)核心步驟:
將所有的回調(diào)都存到隊(duì)列中;
Promise 完成時(shí),逐個(gè)執(zhí)行回調(diào),一旦檢測(cè)到返回了新的 Promise 對(duì)象,停止執(zhí)行,然后將當(dāng)前 Deferred 對(duì)象的 promise 引用改變?yōu)樾碌?Promise 對(duì)象,并將隊(duì)列中余下的回調(diào)轉(zhuǎn)交給它;
至此,實(shí)現(xiàn)了 Promise/Deferred 的完整邏輯,Promise 的其他知識(shí)未來(lái)也會(huì)繼續(xù)探究。
Generator盡管 Promise 一定程度解決了回調(diào)地獄的問(wèn)題,但是對(duì)于喜歡簡(jiǎn)潔的程序員來(lái)說(shuō),一大堆的模板代碼 .then(data => {...}) 顯得不是很友好。所以愛(ài)折騰的開(kāi)發(fā)者們?cè)?ES6 中引人了 Generator 這種數(shù)據(jù)類(lèi)型。仍然以讀取文件為例,先上一段非常簡(jiǎn)潔的 Generator + co 的代碼:
co(function* () { const file1 = yield readFile("./file1.txt") const file2 = yield readFile("./file2.txt") console.log(file1) console.log(file2) })
可以看到比 Promise 的寫(xiě)法簡(jiǎn)潔了許多。后文會(huì)給出 co 庫(kù)的實(shí)現(xiàn)原理。在此之前,先歸納下什么是 Generator。可以把 Generator 理解為一個(gè)可以遍歷的狀態(tài)機(jī),調(diào)用 next 就可以切換到下一個(gè)狀態(tài),其最大特點(diǎn)就是可以交出函數(shù)的執(zhí)行權(quán)(即暫停執(zhí)行),讓我們看如下代碼:
function* gen(x) { yield (function() {return 1})() var y = yield x + 2 return y } // 調(diào)用方式一 var g = gen(1) g.next() // { value: 1, done: false } g.next() // { value: 3, done: false } g.next() // { value: undefined, done: true } // 調(diào)用方式二 var g = gen(1) g.next() // { value: 1, done: false } g.next() // { value: 3, done: false } g.next(10) // { value: 10, done: true }
由此我們歸納下 Generator 的基礎(chǔ)知識(shí):
Generator 生成迭代器后,等待迭代器的 next() 指令啟動(dòng)。
啟動(dòng)迭代器后,代碼會(huì)運(yùn)行到 yield 處停止。并返回一個(gè) {value: AnyType, done: Boolean} 對(duì)象,value 是這次執(zhí)行的結(jié)果,done 是迭代是否結(jié)束。并等待下一次的 next() 指令。
next() 再次啟動(dòng),若 done 的屬性不為 true,則可以繼續(xù)從上一次停止的地方繼續(xù)迭代。
一直重復(fù) 2,3 步驟,直到 done 為 true。
通過(guò)調(diào)用方式二,我們可看到 next 方法可以帶一個(gè)參數(shù),該參數(shù)就會(huì)被當(dāng)作上一個(gè) yield 語(yǔ)句的返回值。
另外我們注意到,上述代碼中的第一種調(diào)用方式中的 y 值是 undefined,如果我們真想拿到 y 值,就需要通過(guò) g.next(); g.next().value 這種方式取出。可以看出,Generator 函數(shù)將異步操作表示得很簡(jiǎn)潔,但是流程管理卻不方便。這時(shí)候用于 Generator 函數(shù)的自動(dòng)執(zhí)行的 co 函數(shù)庫(kù) 登場(chǎng)了。為什么 co 可以自動(dòng)執(zhí)行 Generator 函數(shù)呢?我們知道,Generator 函數(shù)就是一個(gè)異步操作的容器。它的自動(dòng)執(zhí)行需要一種機(jī)制,當(dāng)異步操作有了結(jié)果,能夠自動(dòng)交回執(zhí)行權(quán)。
兩種方法可以做到這一點(diǎn):
Thunk 函數(shù)。將異步操作包裝成 Thunk 函數(shù),在回調(diào)函數(shù)里面交回執(zhí)行權(quán)。
Promise 對(duì)象。將異步操作包裝成 Promise 對(duì)象,用 then 方法交回執(zhí)行權(quán)。
co 函數(shù)庫(kù)其實(shí)就是將兩種自動(dòng)自動(dòng)執(zhí)行器(Thunk 函數(shù)和 Promise 對(duì)象),包裝成一個(gè)庫(kù)。使用 co 的前提條件是,Generator 函數(shù)的 yield 命令后面,只能是 Thunk 函數(shù)或者是 Promise 對(duì)象。下面分別用以上兩種方法對(duì) co 進(jìn)行一個(gè)簡(jiǎn)單的實(shí)現(xiàn)。
基于 Thunk 函數(shù)的自動(dòng)執(zhí)行在 JavaScript 中,Thunk 函數(shù)就是指將多參數(shù)函數(shù)替換成單參數(shù)的形式,并且其只接受回調(diào)函數(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 的知識(shí)上,接著我們來(lái)看看 co 基于 Thunk 函數(shù)的實(shí)現(xiàn)。(附:代碼參考自co最簡(jiǎn)版實(shí)現(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ù),在回調(diào)函數(shù)里面交回執(zhí)行權(quán)。 } 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ù)內(nèi)的 fn 的實(shí)參傳入 console.log(result) // done })
上述部分關(guān)鍵代碼已進(jìn)行注釋?zhuān)旅鎸?duì) co 函數(shù)里的幾個(gè)難點(diǎn)進(jìn)行說(shuō)明:
var step = gen.next(result), 前文提到的一句話(huà)在這里就很有用處了:next方法可以帶一個(gè)參數(shù),該參數(shù)就會(huì)被當(dāng)作上一個(gè)yield語(yǔ)句的返回值;在上述代碼的運(yùn)行中一共會(huì)經(jīng)過(guò)這個(gè)地方 3 次,result 的值第一次是空值,第二次是 file1.txt 的內(nèi)容 I"m file1,第三次是 file2.txt 的內(nèi)容 I"m file2。根據(jù)上述關(guān)鍵語(yǔ)句的提醒,所以第二次的內(nèi)容會(huì)作為 file1 的值(當(dāng)作上一個(gè)yield語(yǔ)句的返回值),同理第三次的內(nèi)容會(huì)作為 file2 的值。
另一處是 step.value(next), step.value 就是前面提到的 thunk 函數(shù)返回的 function(callback) {}, next 就是傳入 thunk 函數(shù)的 callback。這句代碼是條遞歸語(yǔ)句,是這個(gè)簡(jiǎn)易版 co 函數(shù)能自動(dòng)調(diào)用 Generator 的關(guān)鍵語(yǔ)句。
建議親自跑一遍代碼,多打斷點(diǎn),從而更好地理解,代碼已上傳github。
基于 Promise 對(duì)象的自動(dòng)執(zhí)行基于 Thunk 函數(shù)的自動(dòng)執(zhí)行中,yield 后面需跟上 Thunk 函數(shù),在基于 Promise 對(duì)象的自動(dòng)執(zhí)行中,yield 后面自然要跟 Promise 對(duì)象了,讓我們先構(gòu)建一個(gè) readFile 的
Promise 對(duì)象:
function readFile(fileName) { return new Promise(function(resolve, reject) { fs.readFile(fileName, function(error, data) { if (error) reject(error) resolve(data) }) }) }
在基于前文 Promise 對(duì)象和 Generator 的知識(shí)上,接著我們來(lái)看看 co 基于 Promise 函數(shù)的實(shí)現(xiàn):
function co(generator) { var gen = generator() function next(data) { var result = gen.next(data) // 同上,經(jīng)歷了 3 次,第一次是 undefined,第二次是 I"m file1,第三次是 I"m file2 if (result.done) return result.value result.value.then(function(data) { // 將異步操作包裝成 Promise 對(duì)象,用 then 方法交回執(zhí)行權(quán) 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 })
這一部分的代碼上傳在這里,通過(guò)觀(guān)察可以發(fā)現(xiàn)基于 Thunk 函數(shù)和基于 Promise 對(duì)象的自動(dòng)執(zhí)行方案的 co 函數(shù)設(shè)計(jì)思路幾乎一致,也因此呼應(yīng)了它們共同的本質(zhì) —— 當(dāng)異步操作有了結(jié)果,自動(dòng)交回執(zhí)行權(quán)。
async看上去 Generator 已經(jīng)足夠好用了,但是使用 Generator 處理異步必須得依賴(lài) tj/co,于是 asycn 出來(lái)了。本質(zhì)上 async 函數(shù)就是 Generator 函數(shù)的語(yǔ)法糖,這樣說(shuō)是因?yàn)?async 函數(shù)的實(shí)現(xiàn),就是將 Generator 函數(shù)和自動(dòng)執(zhí)行器,包裝進(jìn)一個(gè)函數(shù)中。偽代碼如下,(注:其中 automatic 的實(shí)現(xiàn)可以參考 async 函數(shù)的含義和用法中的實(shí)現(xiàn))
async function fn(args){ // ... } // 等同于 function fn(args) { return automatic(function*() { // automatic 函數(shù)就是自動(dòng)執(zhí)行器,其的實(shí)現(xiàn)可以仿照 co 庫(kù)自動(dòng)運(yùn)行方案來(lái)實(shí)現(xiàn),這里就不展開(kāi)了 // ... }) }
接著仍然以上文的讀取文件為例,來(lái)比較 Generator 和 async 函數(shù)的寫(xiě)法差異:
// 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 // 等同于同步操作(如果跟上原始類(lèi)型的值) }
總體來(lái)說(shuō) async/await 看上去和使用 co 庫(kù)后的 generator 看上去很相似,不過(guò)相較于 Generator,可以看到 Async 函數(shù)更優(yōu)秀的幾點(diǎn):
內(nèi)置執(zhí)行器。Generator 函數(shù)的執(zhí)行必須依靠執(zhí)行器,而 Aysnc 函數(shù)自帶執(zhí)行器,調(diào)用方式跟普通函數(shù)的調(diào)用一樣;
更好的語(yǔ)義。async 和 await 相較于 * 和 yield 更加語(yǔ)義化;
更廣的適用性。前文提到的 co 模塊約定,yield 命令后面只能是 Thunk 函數(shù)或 Promise 對(duì)象,而 async 函數(shù)的 await 命令后面則可以是 Promise 或者原始類(lèi)型的值;
返回值是 Promise。async 函數(shù)返回值是 Promise 對(duì)象,比 Generator 函數(shù)返回的 Iterator 對(duì)象方便,因此可以直接使用 then() 方法進(jìn)行調(diào)用;
參考資料深入淺出 Node.js
理解回調(diào)函數(shù)
JavaScript之異步編程簡(jiǎn)述
理解co執(zhí)行邏輯
co 函數(shù)庫(kù)的含義和用法
async 函數(shù)的含義和用法
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/88588.html
摘要:前端日?qǐng)?bào)精選在中的元素種類(lèi)及性能優(yōu)化譯異步遞歸回調(diào)譯定位一個(gè)頁(yè)面阻塞問(wèn)題的排查過(guò)程前端分享之的使用及單點(diǎn)登錄中文視頻如何用做好一個(gè)大型應(yīng)用云際個(gè)實(shí)用技巧眾成翻譯年一定不要錯(cuò)過(guò)的五本編程書(shū)籍年前端領(lǐng)域有哪些探索和實(shí)踐實(shí)現(xiàn)一個(gè)時(shí)光網(wǎng)掘金 2017-09-22 前端日?qǐng)?bào) 精選 JavaScript 在 V8 中的元素種類(lèi)及性能優(yōu)化【譯】異步遞歸:回調(diào)、Promise、Async[譯]HTML...
摘要:以下,請(qǐng)求兩個(gè),當(dāng)兩個(gè)異步請(qǐng)求返還結(jié)果后,再請(qǐng)求第三個(gè)此處為調(diào)用后的結(jié)果的數(shù)組對(duì)于來(lái)說(shuō),只要參數(shù)數(shù)組有一個(gè)元素變?yōu)闆Q定態(tài),便返回新的。 showImg(https://segmentfault.com/img/remote/1460000015444020); Promise 札記 研究 Promise 的動(dòng)機(jī)大體有以下幾點(diǎn): 對(duì)其 api 的不熟悉以及對(duì)實(shí)現(xiàn)機(jī)制的好奇; 很多庫(kù)(比...
摘要:瀑布流布局中的圖片有一個(gè)核心特點(diǎn)等寬不定等高,瀑布流布局在國(guó)內(nèi)網(wǎng)網(wǎng)站都有一定規(guī)模的使用,比如花瓣網(wǎng)等等。那么接下來(lái)就基于這個(gè)特點(diǎn)開(kāi)始瀑布流探索之旅。 showImg(https://segmentfault.com/img/remote/1460000013059759?w=640&h=280); 瀑布流布局中的圖片有一個(gè)核心特點(diǎn) —— 等寬不定等高,瀑布流布局在國(guó)內(nèi)網(wǎng)網(wǎng)站都有一定規(guī)模...
摘要:瀑布流布局中的圖片有一個(gè)核心特點(diǎn)等寬不定等高,瀑布流布局在國(guó)內(nèi)網(wǎng)網(wǎng)站都有一定規(guī)模的使用,比如花瓣網(wǎng)等等。那么接下來(lái)就基于這個(gè)特點(diǎn)開(kāi)始瀑布流探索之旅。 showImg(https://segmentfault.com/img/remote/1460000013059759?w=640&h=280); 瀑布流布局中的圖片有一個(gè)核心特點(diǎn) —— 等寬不定等高,瀑布流布局在國(guó)內(nèi)網(wǎng)網(wǎng)站都有一定規(guī)模...
摘要:瀑布流布局中的圖片有一個(gè)核心特點(diǎn)等寬不定等高,瀑布流布局在國(guó)內(nèi)網(wǎng)網(wǎng)站都有一定規(guī)模的使用,比如花瓣網(wǎng)等等。那么接下來(lái)就基于這個(gè)特點(diǎn)開(kāi)始瀑布流探索之旅。 showImg(https://segmentfault.com/img/remote/1460000013059759?w=640&h=280); 瀑布流布局中的圖片有一個(gè)核心特點(diǎn) —— 等寬不定等高,瀑布流布局在國(guó)內(nèi)網(wǎng)網(wǎng)站都有一定規(guī)模...
閱讀 2047·2021-11-22 09:34
閱讀 3391·2021-09-28 09:35
閱讀 13925·2021-09-09 11:34
閱讀 3696·2019-08-29 16:25
閱讀 2899·2019-08-29 15:23
閱讀 2103·2019-08-28 17:55
閱讀 2501·2019-08-26 17:04
閱讀 3102·2019-08-26 12:21