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