摘要:以下展示它是如何工作的函數(shù)使用構(gòu)造函數(shù)創(chuàng)建一個(gè)新的對象,并立即將其返回給調(diào)用者。在傳遞給構(gòu)造函數(shù)的函數(shù)中,我們確保傳遞給,這是一個(gè)特殊的回調(diào)函數(shù)。
本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書筆記,在GitHub連載更新,同步翻譯版鏈接。
歡迎關(guān)注我的專欄,之后的博文將在專欄同步:
Encounter的掘金專欄
知乎專欄 Encounter的編程思考
segmentfault專欄 前端小站
Asynchronous Control Flow Patterns with ES2015 and Beyond在上一章中,我們學(xué)習(xí)了如何使用回調(diào)處理異步代碼,以及如何解決如回調(diào)地獄代碼等異步問題。回調(diào)是JavaScript和Node.js中的異步編程的基礎(chǔ),但是現(xiàn)在,其他替代方案已經(jīng)出現(xiàn)。這些替代方案更復(fù)雜,以便能夠以更方便的方式處理異步代碼。
在本章中,我們將探討一些代表性的替代方案,Promise和Generator。以及async await,這是一種創(chuàng)新的語法,可在高版本的JavaScript中提供,其也作為ECMAScript 2017發(fā)行版的一部分。
我們將看到這些替代方案如何簡化處理異步控制流的方式。最后,我們將比較所有這些方法,以了解所有這些方法的所有優(yōu)點(diǎn)和缺點(diǎn),并能夠明智地選擇最適合我們下一個(gè)Node.js項(xiàng)目要求的方法。
Promise我們在前面的章節(jié)中提到,CPS風(fēng)格不是編寫異步代碼的唯一方法。事實(shí)上,JavaScript生態(tài)系統(tǒng)為傳統(tǒng)的回調(diào)模式提供了有趣的替代方案。最著名的選擇之一是Promise,特別是現(xiàn)在它是ECMAScript 2015的一部分,并且現(xiàn)在可以在Node.js中可用。
什么是Promise?Promise是一種抽象的對象,我們通常允許函數(shù)返回一個(gè)名為Promise的對象,它表示異步操作的最終結(jié)果。通常情況下,我們說當(dāng)異步操作尚未完成時(shí),我們說Promise對象處于pending狀態(tài),當(dāng)操作成功完成時(shí),我們說Promise對象處于resolve狀態(tài),當(dāng)操作錯(cuò)誤終止時(shí),我們說Promise對象處于reject狀態(tài)。一旦Promise處于resolve或reject,我們認(rèn)為當(dāng)前異步操作結(jié)束。
為了接收到異步操作的正確結(jié)果或錯(cuò)誤捕獲,我們可以使用Promise的then方法:
promise.then([onFulfilled], [onRejected])
在前面的代碼中,onFulfilled()是一個(gè)函數(shù),最終會(huì)收到Promise的正確結(jié)果,而onRejected()是另一個(gè)函數(shù),它將接收產(chǎn)生異常的原因(如果有的話)。兩個(gè)參數(shù)都是可選的。
要了解Promise如何轉(zhuǎn)換我們的代碼,讓我們考慮以下幾點(diǎn):
asyncOperation(arg, (err, result) => { if (err) { // 錯(cuò)誤處理 } // 正常結(jié)果處理 });
Promise允許我們將這個(gè)典型的CPS代碼轉(zhuǎn)換成更好的結(jié)構(gòu)化和更優(yōu)雅的代碼,如下所示:
asyncOperation(arg) .then(result => { // 錯(cuò)誤處理 }, err => { // 正常結(jié)果處理 });
then()方法的一個(gè)關(guān)鍵特征是它同步地返回另一個(gè)Promise對象。如果onFulfilled()或onRejected()函數(shù)中的任何一個(gè)函數(shù)返回x,則then()方法返回的Promise對象將如下所示:
如果x是一個(gè)值,則這個(gè)Promise對象會(huì)正確處理(resolve)x
如果x是一個(gè)Promise對象或thenable,則會(huì)正確處理(resolve)x
如果x是一個(gè)異常,則會(huì)捕獲異常(reject)x
注:thenable是一個(gè)具有then方法的類似于Promise的對象(Promise-like)。
這個(gè)特點(diǎn)使我們能夠鏈?zhǔn)綐?gòu)建Promise,允許輕松排列組合我們的異步操作。另外,如果我們沒有指定一個(gè)onFulfilled()或onRejected()處理程序,則正確結(jié)果或異常捕獲將自動(dòng)轉(zhuǎn)發(fā)到Promise鏈的下一個(gè)Promise。例如,這允許我們在整個(gè)鏈中自動(dòng)傳播錯(cuò)誤,直到被onRejected()處理程序捕獲。隨著Promise鏈,任務(wù)的順序執(zhí)行突然變成簡單多了:
asyncOperation(arg) .then(result1 => { // 返回另一個(gè)Promise return asyncOperation(arg2); }) .then(result2 => { // 返回一個(gè)值 return "done"; }) .then(undefined, err => { // 捕獲Promise鏈中的異常 });
下圖展示了鏈?zhǔn)?b>Promise如何工作:
Promise的另一個(gè)重要特性是onFulfilled()和onRejected()函數(shù)是異步調(diào)用的,如同上述的例子,在最后那個(gè)then函數(shù)resolve一個(gè)同步的Promise,它也是同步的。這種模式避免了Zalgo(參見Chapter2-Node.js Essential Patterns),使我們的異步代碼更加一致和穩(wěn)健。
如果在onFulfilled()或onRejected()處理程序中拋出異常(使用throw語句),則then()方法返回的Promise將被自動(dòng)地reject,拋出異常作為reject的原因。這相對于CPS來說是一個(gè)巨大的優(yōu)勢,因?yàn)樗馕吨辛?b>Promise,異常將在整個(gè)鏈中自動(dòng)傳播,并且throw語句終于可以使用。
在以前,許多不同的庫實(shí)現(xiàn)了Promise,大多數(shù)時(shí)候它們之間不兼容,這意味著不可能在使用不同Promise庫的thenable鏈?zhǔn)絺鞑ュe(cuò)誤。
JavaScript社區(qū)非常努力地解決了這個(gè)限制,這些努力導(dǎo)致了Promises / A +規(guī)范的創(chuàng)建。該規(guī)范詳細(xì)描述了then方法的行為,提供了一個(gè)可互兼容的基礎(chǔ),這使得來自不同庫的Promise對象能夠彼此兼容,開箱即用。
有關(guān)Promises / A +規(guī)范的詳細(xì)說明,可以參考Promises / A + 官方網(wǎng)站。
Promise / A + 的實(shí)施在JavaScript中以及Node.js中,有幾個(gè)實(shí)現(xiàn)Promises / A +規(guī)范的庫。以下是最受歡迎的:
Bluebird
Q
RSVP
Vow
When.js
ES2015 promises
真正區(qū)別他們的是在Promises / A +標(biāo)準(zhǔn)之上提供的額外功能。正如我們上述所說的那樣,該標(biāo)準(zhǔn)定義了then()方法和Promise解析過程的行為,但它沒有指定其他功能,例如,如何從基于回調(diào)的異步函數(shù)創(chuàng)建Promise。
在我們的示例中,我們將使用由ES2015的Promise,因?yàn)?b>Promise對象自Node.js 4后即可使用,而不需要任何庫來實(shí)現(xiàn)。
作為參考,以下是ES2015的Promise提供的API:
constructor(new Promise(function(resolve, reject){})):創(chuàng)建了一個(gè)新的Promise,它基于作為傳遞兩個(gè)類型為函數(shù)的參數(shù)來決定resolve或reject。構(gòu)造函數(shù)的參數(shù)解釋如下:
resolve(obj) :resolve一個(gè)Promise,并帶上一個(gè)參數(shù)obj,如果obj是一個(gè)值,這個(gè)值就是傳遞的異步操作成功的結(jié)果。如果obj是一個(gè)Promise或一個(gè)thenable,則會(huì)進(jìn)行正確處理。
reject(err):reject一個(gè)Promise,并帶上一個(gè)參數(shù)err。它是Error對象的一個(gè)實(shí)例。
Promise對象的靜態(tài)方法Promise.resolve(obj): 將會(huì)創(chuàng)建一個(gè)resolve的Promise實(shí)例
Promise.reject(err): 將會(huì)創(chuàng)建一個(gè)reject的Promise實(shí)例
Promise.all(iterable):返回一個(gè)新的Promise實(shí)例,并且在iterable中所
有Promise狀態(tài)為reject時(shí), 返回的Promise實(shí)例的狀態(tài)會(huì)被置為reject,如果iterable中至少有一個(gè)Promise狀態(tài)為reject時(shí), 返回的Promise實(shí)例狀態(tài)也會(huì)被置為reject,并且reject的原因是第一個(gè)被reject的Promise對象的reject原因。
Promise.race(iterable):返回一個(gè)Promise實(shí)例,當(dāng)iterable中任何一個(gè)Promise被resolve或被reject時(shí), 返回的Promise實(shí)例以同樣的原因resolve或reject。
Promise實(shí)例方法Promise.then(onFulfilled, onRejected):這是Promise的基本方法。它的行為與我們之前描述的Promises / A +標(biāo)準(zhǔn)兼容。
Promise.catch(onRejected):這只是Promise.then(undefined,onRejected)的語法糖。
Promisifying一個(gè)Node.js回調(diào)風(fēng)格的函數(shù)值得一提的是,一些Promise實(shí)現(xiàn)提供了另一種機(jī)制來創(chuàng)建新的Promise,稱為deferreds。我們不會(huì)在這里描述,因?yàn)樗皇荅S2015標(biāo)準(zhǔn)的一部分,但是如果您想了解更多信息,可以閱讀Q文檔 (https://github.com/kriskowal/... 或When.js文檔 (https://github.com/cujojs/whe... 。
在JavaScript中,并不是所有的異步函數(shù)和庫都支持開箱即用的Promise。大多數(shù)情況下,我們必須將一個(gè)典型的基于回調(diào)的函數(shù)轉(zhuǎn)換成一個(gè)返回Promise的函數(shù),這個(gè)過程也被稱為promisification。
幸運(yùn)的是,Node.js中使用的回調(diào)約定允許我們創(chuàng)建一個(gè)可重用的函數(shù),我們通過使用Promise對象的構(gòu)造函數(shù)來簡化任何Node.js風(fēng)格的API。讓我們創(chuàng)建一個(gè)名為promisify()的新函數(shù),并將其包含到utilities.js模塊中(以便稍后在我們的Web爬蟲應(yīng)用程序中使用它):
module.exports.promisify = function(callbackBasedApi) { return function promisified() { const args = [].slice.call(arguments); return new Promise((resolve, reject) => { args.push((err, result) => { if (err) { return reject(err); } if (arguments.length <= 2) { resolve(result); } else { resolve([].slice.call(arguments, 1)); } }); callbackBasedApi.apply(null, args); }); } };
前面的函數(shù)返回另一個(gè)名為promisified()的函數(shù),它表示輸入中給出的callbackBasedApi的promisified版本。以下展示它是如何工作的:
promisified()函數(shù)使用Promise構(gòu)造函數(shù)創(chuàng)建一個(gè)新的Promise對象,并立即將其返回給調(diào)用者。
在傳遞給Promise構(gòu)造函數(shù)的函數(shù)中,我們確保傳遞給callbackBasedApi,這是一個(gè)特殊的回調(diào)函數(shù)。由于我們知道回調(diào)總是最后調(diào)用的,我們只需將回調(diào)函數(shù)附加到提供給promisified()函數(shù)的參數(shù)列表里(args)。
在特殊的回調(diào)中,如果我們收到錯(cuò)誤,我們立即reject這個(gè)Promise。
如果沒有收到錯(cuò)誤,我們使用一個(gè)值或一個(gè)數(shù)組值來resolve這個(gè)Promise,具體取決于傳遞給回調(diào)的結(jié)果數(shù)量。
最后,我們只需使用我們構(gòu)建的參數(shù)列表調(diào)用callbackBasedApi。
順序執(zhí)行大部分的Promise已經(jīng)提供了一個(gè)開箱即用的接口來將一個(gè)Node.js風(fēng)格的API轉(zhuǎn)換成一個(gè)返回Promise的API。例如,Q有Q.denodeify()和Q.nbind(),Bluebird有Promise.promisify(),而When.js有node.lift()。
在一些必要的理論之后,我們現(xiàn)在準(zhǔn)備將我們的Web爬蟲應(yīng)用程序轉(zhuǎn)換為使用Promise的形式。讓我們直接從版本2開始,直接下載一個(gè)Web網(wǎng)頁的鏈接。
在spider.js模塊中,第一步是加載我們的Promise實(shí)現(xiàn)(我們稍后會(huì)使用它)和Promisifying我們打算使用的基于回調(diào)的函數(shù):
const utilities = require("./utilities"); const request = utilities.promisify(require("request")); const mkdirp = utilities.promisify(require("mkdirp")); const fs = require("fs"); const readFile = utilities.promisify(fs.readFile); const writeFile = utilities.promisify(fs.writeFile);
現(xiàn)在,我們開始更改我們的download函數(shù):
function download(url, filename) { console.log(`Downloading ${url}`); let body; return request(url) .then(response => { body = response.body; return mkdirp(path.dirname(filename)); }) .then(() => writeFile(filename, body)) .then(() => { console.log(`Downloaded and saved: ${url}`); return body; }); }
這里要注意的到的最重要的是我們也為readFile()返回的Promise注冊
一個(gè)onRejected()函數(shù),用來處理一個(gè)網(wǎng)頁沒有被下載的情況(或文件不存在)。 還有,看我們?nèi)绾问褂?b>throw來傳遞onRejected()函數(shù)中的錯(cuò)誤的。
既然我們已經(jīng)更改我們的spider()函數(shù),我們這么修改它的調(diào)用方式:
spider(process.argv[2], 1) .then(() => console.log("Download complete")) .catch(err => console.log(err));
注意我們是如何第一次使用Promise的語法糖catch來處理源自spider()函數(shù)的任何錯(cuò)誤情況。如果我們再看看迄今為止我們所寫的所有代碼,那么我們會(huì)驚喜的發(fā)現(xiàn),我們沒有包含任何錯(cuò)誤傳播邏輯,因?yàn)槲覀冊谑褂没卣{(diào)函數(shù)時(shí)會(huì)被迫做這樣的事情。這顯然是一個(gè)巨大的優(yōu)勢,因?yàn)樗鼧O大地減少了我們代碼中的樣板文件以及丟失任何異步錯(cuò)誤的機(jī)會(huì)。
現(xiàn)在,完成我們唯一缺失的Web爬蟲應(yīng)用程序的第二版的spiderLinks()函數(shù),我們將在稍后實(shí)現(xiàn)它。
順序迭代到目前為止,Web爬蟲應(yīng)用程序代碼庫主要是對Promise是什么以及如何使用的概述,展示了使用Promise實(shí)現(xiàn)順序執(zhí)行流程的簡單性和優(yōu)雅性。但是,我們現(xiàn)在考慮的代碼只涉及到一組已知的異步操作的執(zhí)行。所以,完成我們對順序執(zhí)行流程的探索的缺失部分是看我們?nèi)绾问褂?b>Promise來實(shí)現(xiàn)迭代。同樣,網(wǎng)絡(luò)蜘蛛第二版的spiderLinks()函數(shù)也是一個(gè)很好的例子。
讓我們添加缺少的這一塊:
function spiderLinks(currentUrl, body, nesting) { let promise = Promise.resolve(); if (nesting === 0) { return promise; } const links = utilities.getPageLinks(currentUrl, body); links.forEach(link => { promise = promise.then(() => spider(link, nesting - 1)); }); return promise; }
為了異步迭代一個(gè)網(wǎng)頁的全部鏈接,我們必須動(dòng)態(tài)創(chuàng)建一個(gè)Promise的迭代鏈。
首先,我們定義一個(gè)空的Promise,resolve為undefined。這個(gè)Promise只是用來作為Promise的迭代鏈的起始點(diǎn)。
然后,我們通過在循環(huán)中調(diào)用鏈中前一個(gè)Promise的then()方法獲得的新的Promise來更新Promise變量。這就是我們使用Promise的異步迭代模式。
這樣,循環(huán)的結(jié)束,promise變量會(huì)包含循環(huán)中最后一個(gè)then()返回的Promise對象,所以它只有當(dāng)Promise的迭代鏈中全部Promise對象被resolve后才能被resolve。
注:在最后調(diào)用了這個(gè)then方法來resolve這個(gè)Promise對象
通過這個(gè),我們已使用Promise對象重寫了我們的Web爬蟲應(yīng)用程序。我們現(xiàn)在應(yīng)該可以運(yùn)行它了。
順序迭代模式為了總結(jié)這個(gè)順序執(zhí)行的部分,讓我們提取一個(gè)模式來依次遍歷一組Promise:
let tasks = [ /* ... */ ] let promise = Promise.resolve(); tasks.forEach(task => { promise = promise.then(() => { return task(); }); }); promise.then(() => { // 所有任務(wù)都完成 });
使用reduce()方法來替代forEach()方法,允許我們寫出更為簡潔的代碼:
let tasks = [ /* ... */ ] let promise = tasks.reduce((prev, task) => { return prev.then(() => { return task(); }); }, Promise.resolve()); promise.then(() => { //All tasks completed });
與往常一樣,通過對這種模式的簡單調(diào)整,我們可以將所有任務(wù)的結(jié)果收集到一個(gè)數(shù)組中,我們可以實(shí)現(xiàn)一個(gè)mapping算法,或者構(gòu)建一個(gè)filter等等。
并行執(zhí)行上述這個(gè)模式使用循環(huán)動(dòng)態(tài)地建立一個(gè)鏈?zhǔn)降腜romise。
另一個(gè)適合用Promise的執(zhí)行流程是并行執(zhí)行流程。實(shí)際上,我們需要做的就是使用內(nèi)置的Promise.all()。這個(gè)方法創(chuàng)造了另一個(gè)Promise對象,只有在輸入中的所有Promise都resolve時(shí)才能resolve。這是一個(gè)并行執(zhí)行,因?yàn)樵谄鋮?shù)Promise對象的之間沒有執(zhí)行順序可言。
為了演示這一點(diǎn),我們來看我們的Web爬蟲應(yīng)用程序的第三版,它將頁面中的所有鏈接并行下載。讓我們再次使用Promise更新spiderLinks()函數(shù)來實(shí)現(xiàn)并行流程:
function spiderLinks(currentUrl, body, nesting) { if (nesting === 0) { return Promise.resolve(); } const links = utilities.getPageLinks(currentUrl, body); const promises = links.map(link => spider(link, nesting - 1)); return Promise.all(promises); }
這里的模式在elements.map()迭代中產(chǎn)生一個(gè)數(shù)組,存放所有異步任務(wù),之后便于同時(shí)啟動(dòng)spider()任務(wù)。這一次,在循環(huán)中,我們不等待以前的下載完成,然后開始一個(gè)新的下載任務(wù):所有的下載任務(wù)在一個(gè)循環(huán)中一個(gè)接一個(gè)地開始。之后,我們利用Promise.all()方法,它返回一個(gè)新的Promise對象,當(dāng)數(shù)組中的所有Promise對象都被resolve時(shí),這個(gè)Promise對象將被resolve。換句話說,所有的下載任務(wù)完成,這正是我們想要的。
限制并行執(zhí)行不幸的是,ES2015的Promise API并沒有提供一種原生的方式來限制并發(fā)任務(wù)的數(shù)量,但是我們總是可以依靠我們所學(xué)到的有關(guān)用普通JavaScript來限制并發(fā)。事實(shí)上,我們在TaskQueue類中實(shí)現(xiàn)的模式可以很容易地被調(diào)整來支持返回承諾的任務(wù)。這很容易通過修改next()方法來完成:
class TaskQueue { constructor(concurrency) { this.concurrency = concurrency; this.running = 0; this.queue = []; } pushTask(task) { this.queue.push(task); this.next(); } next() { while (this.running < this.concurrency && this.queue.length) { const task = this.queue.shift(); task().then(() => { this.running--; this.next(); }); this.running++; } } }
不同于使用一個(gè)回調(diào)函數(shù)來處理任務(wù),我們簡單地調(diào)用Promise的then()。
讓我們回到spider.js模塊,并修改它以支持我們的新版本的TaskQueue類。首先,我們確保定義一個(gè)TaskQueue的新實(shí)例:
const TaskQueue = require("./taskQueue"); const downloadQueue = new TaskQueue(2);
然后,是我們的spiderLinks()函數(shù)。這里的修改也是很簡單:
function spiderLinks(currentUrl, body, nesting) { if (nesting === 0) { return Promise.resolve(); } const links = utilities.getPageLinks(currentUrl, body); // 我們需要如下代碼,用于創(chuàng)建Promise對象 // 如果沒有下列代碼,當(dāng)任務(wù)數(shù)量為0時(shí),將永遠(yuǎn)不會(huì)resolve if (links.length === 0) { return Promise.resolve(); } return new Promise((resolve, reject) => { let completed = 0; let errored = false; links.forEach(link => { let task = () => { return spider(link, nesting - 1) .then(() => { if (++completed === links.length) { resolve(); } }) .catch(() => { if (!errored) { errored = true; reject(); } }); }; downloadQueue.pushTask(task); }); }); }
在上述代碼中有幾點(diǎn)值得我們注意的:
首先,我們需要返回使用Promise構(gòu)造函數(shù)創(chuàng)建的新的Promise對象。正如我們將看到的,這使我們能夠在隊(duì)列中的所有任務(wù)完成時(shí)手動(dòng)resolve我們的Promise對象。
然后,我們應(yīng)該看看我們?nèi)绾味x任務(wù)。我們所做的是將一個(gè)onFulfilled()回調(diào)函數(shù)的調(diào)用添加到由spider()返回的Promise對象中,所以我們可以計(jì)算完成的下載任務(wù)的數(shù)量。當(dāng)完成的下載量與當(dāng)前頁面中鏈接的數(shù)量相同時(shí),我們知道任務(wù)已經(jīng)處理完畢,所以我們可以調(diào)用外部Promise的resolve()函數(shù)。
Promises / A +規(guī)范規(guī)定,then()方法的onFulfilled()和onRejected()回調(diào)函數(shù)只能調(diào)用一次(僅調(diào)用onFulfilled()和onRejected())。Promise接口的實(shí)現(xiàn)確保即使我們多次手動(dòng)調(diào)用resolve或reject,Promise也僅可以被resolve或reject一次。
現(xiàn)在,使用Promise的Web爬蟲應(yīng)用程序的第4版應(yīng)該已經(jīng)準(zhǔn)備好了。我們可能再次注意到下載任務(wù)如何并行運(yùn)行,并發(fā)數(shù)量限制為2。
在公有API中暴露回調(diào)函數(shù)和Promise正如我們在前面所學(xué)到的,Promise可以被用作回調(diào)函數(shù)的一個(gè)很好的替代品。它們使我們的代碼更具可讀性和易于理解。雖然Promise帶來了許多優(yōu)點(diǎn),但也要求開發(fā)人員理解許多不易于理解的概念,以便正確和熟練地使用。由于這個(gè)原因和其他原因,在某些情況下,比起Promise來說,很多開發(fā)者更偏向于回調(diào)函數(shù)。
現(xiàn)在讓我們想象一下,我們想要構(gòu)建一個(gè)執(zhí)行異步操作的公共庫。我們需要做什么?我們是創(chuàng)建了一個(gè)基于回調(diào)函數(shù)的API還是一個(gè)面向Promise的API?還是兩者均有?
這是許多知名的庫所面臨的問題,至少有兩種方法值得一提,使我們能夠提供一個(gè)多功能的API。
像request,redis和mysql這樣的庫所使用的第一種方法是提供一個(gè)簡單的基于回調(diào)函數(shù)的API,如果需要,開發(fā)人員可以選擇公開函數(shù)。其中一些庫提供工具函數(shù)來Promise化異步回調(diào),但開發(fā)人員仍然需要以某種方式將暴露的API轉(zhuǎn)換為能夠使用Promise對象。
第二種方法更透明。它還提供了一個(gè)面向回調(diào)的API,但它使回調(diào)參數(shù)可選。每當(dāng)回調(diào)作為參數(shù)傳遞時(shí),函數(shù)將正常運(yùn)行,在完成時(shí)或失敗時(shí)執(zhí)行回調(diào)。當(dāng)回調(diào)未被傳遞時(shí),函數(shù)將立即返回一個(gè)Promise對象。這種方法有效地結(jié)合了回調(diào)函數(shù)和Promise,使得開發(fā)者可以在調(diào)用時(shí)選擇采用什么接口,而不需要提前進(jìn)行Promise化。許多庫,如mongoose和sequelize,都支持這種方法。
我們來看一個(gè)簡單的例子。假設(shè)我們要實(shí)現(xiàn)一個(gè)異步執(zhí)行除法的模塊:
module.exports = function asyncDivision(dividend, divisor, cb) { return new Promise((resolve, reject) => { // [1] process.nextTick(() => { const result = dividend / divisor; if (isNaN(result) || !Number.isFinite(result)) { const error = new Error("Invalid operands"); if (cb) { cb(error); // [2] } return reject(error); } if (cb) { cb(null, result); // [3] } resolve(result); }); }); };
該模塊的代碼非常簡單,但是有一些值得強(qiáng)調(diào)的細(xì)節(jié):
首先,返回使用Promise的構(gòu)造函數(shù)創(chuàng)建的新承諾。我們在構(gòu)造函數(shù)參數(shù)函數(shù)內(nèi)定義全部邏輯。
在發(fā)生錯(cuò)誤的情況下,我們reject這個(gè)Promise,但如果回調(diào)函數(shù)在被調(diào)用時(shí)作為參數(shù)傳遞,我們也執(zhí)行回調(diào)來進(jìn)行錯(cuò)誤傳播。
在計(jì)算結(jié)果之后,我們resolve了這個(gè)Promise,但是如果有回調(diào)函數(shù),我們也會(huì)將結(jié)果傳播給回調(diào)函數(shù)。
我們現(xiàn)在看如何用回調(diào)函數(shù)和Promise來使用這個(gè)模塊:
// 回調(diào)函數(shù)的方式 asyncDivision(10, 2, (error, result) => { if (error) { return console.error(error); } console.log(result); }); // Promise化的調(diào)用方式 asyncDivision(22, 11) .then(result => console.log(result)) .catch(error => console.error(error));
應(yīng)該很清楚的是,即將開始使用類似于上述的新模塊的開發(fā)人員將很容易地選擇最適合自己需求的風(fēng)格,而無需在希望利用Promise時(shí)引入外部promisification功能。
GeneratorsES2015規(guī)范引入了另外一種機(jī)制,除了其他新功能外,還可以用來簡化Node.js應(yīng)用程序的異步控制流程。我們正在談?wù)?b>Generator,也被稱為semi-coroutines。它們是子程序的一般化,可以有不同的入口點(diǎn)。在一個(gè)正常的函數(shù)中,實(shí)際上我們只能有一個(gè)入口點(diǎn),這個(gè)入口點(diǎn)對應(yīng)著函數(shù)本身的調(diào)用。Generator與一般函數(shù)類似,但是可以暫停(使用yield語句),然后在稍后繼續(xù)執(zhí)行。在實(shí)現(xiàn)迭代器時(shí),Generator特別有用,因?yàn)槲覀円呀?jīng)討論了如何使用迭代器來實(shí)現(xiàn)重要的異步控制流模式,如順序執(zhí)行和限制并行執(zhí)行。
Generators基礎(chǔ)在我們探索使用Generator來實(shí)現(xiàn)異步控制流程之前,學(xué)習(xí)一些基本概念是很重要的。我們從語法開始吧。可以通過在函數(shù)關(guān)鍵字之后附加*(星號(hào))運(yùn)算符來聲明Generator函數(shù):
function* makeGenerator() { // body }
在makeGenerator()函數(shù)內(nèi)部,我們可以使用關(guān)鍵字yield暫停執(zhí)行并返回給調(diào)用者傳遞給它的值:
function* makeGenerator() { yield "Hello World"; console.log("Re-entered"); }
在前面的代碼中,Generator通過yield一個(gè)字符串Hello World暫停當(dāng)前函數(shù)的執(zhí)行。當(dāng)Generator恢復(fù)時(shí),執(zhí)行將從下列語句開始:
console.log("Re-entered");
makeGenerator()函數(shù)本質(zhì)上是一個(gè)工廠,它在被調(diào)用時(shí)返回一個(gè)新的Generator對象:
const gen = makeGenerator();
生成器對象的最重要的方法是next(),它用于啟動(dòng)/恢復(fù)Generator的執(zhí)行,并返回如下形式的對象:
{ value:done: }
這個(gè)對象包含Generator yield的值和一個(gè)指示Generator是否已經(jīng)完成執(zhí)行的符號(hào)。
一個(gè)簡單的例子為了演示Generator,我們來創(chuàng)建一個(gè)名為fruitGenerator.js的新模塊:
function* fruitGenerator() { yield "apple"; yield "orange"; return "watermelon"; } const newFruitGenerator = fruitGenerator(); console.log(newFruitGenerator.next()); // [1] console.log(newFruitGenerator.next()); // [2] console.log(newFruitGenerator.next()); // [3]
前面的代碼將打印下面的輸出:
{ value: "apple", done: false } { value: "orange", done: false } { value: "watermelon", done: true }
我們可以這么解釋上述現(xiàn)象:
第一次調(diào)用newFruitGenerator.next()時(shí),Generator函數(shù)開始執(zhí)行,直到達(dá)到第一個(gè)yield語句為止,該命令暫停Generator函數(shù)執(zhí)行,并將值apple返回給調(diào)用者。
在第二次調(diào)用newFruitGenerator.next()時(shí),Generator函數(shù)恢復(fù)執(zhí)行,從第二個(gè)yield語句開始,這又使得執(zhí)行暫停,同時(shí)將orange返回給調(diào)用者。
newFruitGenerator.next()的最后一次調(diào)用導(dǎo)致Generator函數(shù)的執(zhí)行從其最后的yield恢復(fù),一個(gè)返回語句,它終止Generator函數(shù),返回watermelon,并將結(jié)果對象中的done屬性設(shè)置為true。
Generators作為迭代器為了更好地理解為什么Generator函數(shù)對實(shí)現(xiàn)迭代器非常有用,我們來構(gòu)建一個(gè)例子。在我們將調(diào)用iteratorGenerator.js的新模塊中,我們編寫下面的代碼:
function* iteratorGenerator(arr) { for (let i = 0; i < arr.length; i++) { yield arr[i]; } } const iterator = iteratorGenerator(["apple", "orange", "watermelon"]); let currentItem = iterator.next(); while (!currentItem.done) { console.log(currentItem.value); currentItem = iterator.next(); }
此代碼應(yīng)按如下所示打印數(shù)組中的元素:
apple orange watermelon
在這個(gè)例子中,每次我們調(diào)用iterator.next()時(shí),我們都會(huì)恢復(fù)Generator函數(shù)的for循環(huán),通過yield數(shù)組中的下一個(gè)項(xiàng)來運(yùn)行另一個(gè)循環(huán)。這演示了如何在函數(shù)調(diào)用過程中維護(hù)Generator的狀態(tài)。當(dāng)繼續(xù)執(zhí)行時(shí),循環(huán)和所有變量的值與Generator函數(shù)執(zhí)行暫停時(shí)的狀態(tài)完全相同。
傳值給Generators現(xiàn)在我們繼續(xù)研究Generator的基本功能,首先學(xué)習(xí)如何將值傳遞回Generator函數(shù)。這其實(shí)很簡單,我們需要做的只是為next()方法提供一個(gè)參數(shù),并且該值將作為Generator函數(shù)內(nèi)的yield語句的返回值提供。
為了展示這一點(diǎn),我們來創(chuàng)建一個(gè)新的簡單模塊:
function* twoWayGenerator() { const what = yield null; console.log("Hello " + what); } const twoWay = twoWayGenerator(); twoWay.next(); twoWay.next("world");
當(dāng)執(zhí)行時(shí),前面的代碼會(huì)輸出Hello world。我們做如下的解釋:
第一次調(diào)用next()方法時(shí),Generator函數(shù)到達(dá)第一個(gè)yield語句,然后暫停。
當(dāng)next("world")被調(diào)用時(shí),Generator函數(shù)從上次停止的位置,也就是上次的yield語句點(diǎn)恢復(fù),但是這次我們有一個(gè)值傳遞到Generator函數(shù)。這個(gè)值將被賦值到what變量。生成器然后執(zhí)行console.log()指令并終止。
用類似的方式,我們可以強(qiáng)制Generator函數(shù)拋出異常。這可以通過使用Generator函數(shù)的throw方法來實(shí)現(xiàn),如下例所示:
const twoWay = twoWayGenerator(); twoWay.next(); twoWay.throw(new Error());
在這個(gè)最后這段代碼,twoWayGenerator()函數(shù)將在yield函數(shù)返回的時(shí)候拋出異常。這就好像從Generator函數(shù)內(nèi)部拋出了一個(gè)異常一樣,這意味著它可以像使用try ... catch塊一樣進(jìn)行捕獲和處理異常。
Generator實(shí)現(xiàn)異步控制流你一定想知道Generator函數(shù)如何幫助我們處理異步操作。我們可以通過創(chuàng)建一個(gè)接受Generator函數(shù)作為參數(shù)的特殊函數(shù)來演示這一點(diǎn),并允許我們在Generator函數(shù)內(nèi)部使用異步代碼。這個(gè)函數(shù)在異步操作完成時(shí)要注意恢復(fù)Generator函數(shù)的執(zhí)行。我們將調(diào)用這個(gè)函數(shù)asyncFlow():
function asyncFlow(generatorFunction) { function callback(err) { if (err) { return generator.throw(err); } const results = [].slice.call(arguments, 1); generator.next(results.length > 1 ? results : results[0]); } const generator = generatorFunction(callback); generator.next(); }
前面的函數(shù)取一個(gè)Generator函數(shù)作為輸入,然后立即調(diào)用:
const generator = generatorFunction(callback); generator.next();
generatorFunction()接受一個(gè)特殊的回調(diào)函數(shù)作為參數(shù),當(dāng)generator.throw()如果接收到一個(gè)錯(cuò)誤,便立即返回。另外,通過將在回調(diào)函數(shù)中接收的results傳值回Generator函數(shù)繼續(xù)Generator函數(shù)的執(zhí)行:
if (err) { return generator.throw(err); } const results = [].slice.call(arguments, 1); generator.next(results.length > 1 ? results : results[0]);
為了說明這個(gè)簡單的輔助函數(shù)的強(qiáng)大,我們創(chuàng)建一個(gè)叫做clone.js的新模塊,這個(gè)模塊只是創(chuàng)建它本身的克隆。粘貼我們剛才創(chuàng)建的asyncFlow()函數(shù),核心代碼如下:
const fs = require("fs"); const path = require("path"); asyncFlow(function*(callback) { const fileName = path.basename(__filename); const myself = yield fs.readFile(fileName, "utf8", callback); yield fs.writeFile(`clone_of_${filename}`, myself, callback); console.log("Clone created"); });
明顯地,有了asyncFlow()函數(shù)的幫助,我們可以像我們書寫同步阻塞函數(shù)一樣用同步的方式來書寫異步代碼了。并且這個(gè)結(jié)果背后的原理顯得很清楚。一旦異步操作結(jié)束,傳遞給每個(gè)異步函數(shù)的回調(diào)函數(shù)將繼續(xù)Generator函數(shù)的執(zhí)行。沒有什么復(fù)雜的,但是結(jié)果確實(shí)很令人意外。
這個(gè)技術(shù)有其他兩個(gè)變化,一個(gè)是Promise的使用,另外一個(gè)則是thunks。
在基于Generator的控制流中使用的thunk只是一個(gè)簡單的函數(shù),它除了回調(diào)之外,部分地應(yīng)用了原始函數(shù)的所有參數(shù)。返回值是另一個(gè)只接受回調(diào)作為參數(shù)的函數(shù)。例如,fs.readFile()的thunkified版本如下所示:
function readFileThunk(filename, options) { return function(callback) { fs.readFile(filename, options, callback); } }
thunk和Promise都允許我們創(chuàng)建不需要回調(diào)的Generator函數(shù)作為參數(shù)傳遞,例如,使用thunk的asyncFlow()版本如下:
function asyncFlowWithThunks(generatorFunction) { function callback(err) { if (err) { return generator.throw(err); } const results = [].slice.call(arguments, 1); const thunk = generator.next(results.length > 1 ? results : results[0]).value; thunk && thunk(callback); } const generator = generatorFunction(); const thunk = generator.next().value; thunk && thunk(callback); }
這個(gè)技巧是讀取generator.next()的返回值,返回值中包含thunk。下一步是通過注入特殊的回調(diào)函數(shù)調(diào)用thunk本身。這允許我們寫下面的代碼:
asyncFlowWithThunk(function*() { const fileName = path.basename(__filename); const myself = yield readFileThunk(__filename, "utf8"); yield writeFileThunk(`clone_of_${fileName}`, myself); console.log("Clone created") });使用co的基于Gernator的控制流
你應(yīng)該已經(jīng)猜到了,Node.js生態(tài)系統(tǒng)會(huì)借助Generator函數(shù)來提供一些處理異步控制流的解決方案,例如,suspend是其中一個(gè)最老的支持Promise、thunks和Node.js風(fēng)格回調(diào)函數(shù)和正常風(fēng)格的回調(diào)函數(shù)的 庫。還有,大部分我們之前分析的Promise庫都提供工具函數(shù)使得Generator和Promise可以一起使用。
我們選擇co作為本章節(jié)的例子。它支持很多類型的yieldables,其中一些是:
Thunks
Promises
Arrays(并行執(zhí)行)
Objects(并行執(zhí)行)
Generators(委托)
Generator函數(shù)(委托)
還有很多框架或庫是基于co生態(tài)系統(tǒng)的,包括以下一些:
Web框架,最流行的是koa
實(shí)現(xiàn)特定控制流模式的庫
包裝流行的API兼容co的庫
我們使用co重新實(shí)現(xiàn)我們的Generator版本的Web爬蟲應(yīng)用程序。
為了將Node.js風(fēng)格的函數(shù)轉(zhuǎn)換成thunks,我們將會(huì)使用一個(gè)叫做thunkify的庫。
順序執(zhí)行讓我們通過修改Web爬蟲應(yīng)用程序的版本2開始我們對Generator函數(shù)和co的實(shí)際探索。我們要做的第一件事就是加載我們的依賴包,并生成我們要使用的函數(shù)的thunkified版本。這些將在spider.js模塊的最開始進(jìn)行:
const thunkify = require("thunkify"); const co = require("co"); const request = thunkify(require("request")); const fs = require("fs"); const mkdirp = thunkify(require("mkdirp")); const readFile = thunkify(fs.readFile); const writeFile = thunkify(fs.writeFile); const nextTick = thunkify(process.nextTick);
看上述代碼,我們可以注意到與本章前面promisify化的API的代碼的一些相似之處。在這一點(diǎn)上,有意思的是,如果我們使用我們的promisified版本的函數(shù)來代替thunkified的版本,代碼將保持完全一樣,這要?dú)w功于co支持thunk和Promise對象作為yieldable對象。事實(shí)上,如果我們想,甚至可以在同一個(gè)應(yīng)用程序中使用thunk和Promise,即使在同一個(gè)Generator函數(shù)中。就靈活性而言,這是一個(gè)巨大的優(yōu)勢,因?yàn)樗刮覀兡軌蚴褂没?b>Generator函數(shù)的控制流來解決我們應(yīng)用程序中的問題。
好的,現(xiàn)在讓我們開始將download()函數(shù)轉(zhuǎn)換為一個(gè)Generator函數(shù):
function* download(url, filename) { console.log(`Downloading ${url}`); const response = yield request(url); const body = response[1]; yield mkdirp(path.dirname(filename)); yield writeFile(filename, body); console.log(`Downloaded and saved ${url}`); return body; }
通過使用Generator和co,我們的download()函數(shù)變得簡單多了。當(dāng)我們需要做異步操作的時(shí)候,我們使用異步的Generator函數(shù)作為thunk來把之前的內(nèi)容轉(zhuǎn)化到Generator函數(shù),并使用yield子句。
然后我們開始實(shí)現(xiàn)我們的spider()函數(shù):
function* spider(url, nesting) { cost filename = utilities.urlToFilename(url); let body; try { body = yield readFile(filename, "utf8"); } catch (err) { if (err.code !== "ENOENT") { throw err; } body = yield download(url, filename); } yield spiderLinks(url, body, nesting); }
從上述代碼中一個(gè)有趣的細(xì)節(jié)是我們可以使用try...catch語句塊來處理異常。我們還可以使用throw來傳播異常。另外一個(gè)細(xì)節(jié)是我們yield我們的download()函數(shù),而這個(gè)函數(shù)既不是一個(gè)thunk,也不是一個(gè)promisified函數(shù),只是另外的一個(gè)Generator函數(shù)。這也毫無問題,由于co也支持其他Generators作為yieldables。
最后轉(zhuǎn)換spiderLinks(),在這個(gè)函數(shù)中,我們遞歸下載一個(gè)網(wǎng)頁的鏈接。在這個(gè)函數(shù)中使用Generators,顯得簡單多了:
function* spiderLinks(currentUrl, body, nesting) { if (nesting === 0) { return nextTick(); } const links = utilities.getPageLinks(currentUrl, body); for (let i = 0; i < links.length; i++) { yield spider(links[i], nesting - 1); } }
看上述代碼。雖然順序迭代沒有什么模式可以展示。Generator和co輔助我們做了很多,方便了我們可以使用同步方式開書寫異步代碼。
看最重要的部分,程序的入口:
co(function*() { try { yield spider(process.argv[2], 1); console.log(`Download complete`); } catch (err) { console.log(err); } });
這是唯一一處需要調(diào)用co(...)來封裝的一個(gè)Generator。實(shí)際上,一旦我們這么做,co會(huì)自動(dòng)封裝我們傳遞給yield語句的任何Generator函數(shù),并且這個(gè)過程是遞歸的,所以程序的剩余部分與我們是否使用co是完全無關(guān)的,雖然是被co封裝在里面。
現(xiàn)在應(yīng)該可以運(yùn)行使用Generator函數(shù)改寫的Web爬蟲應(yīng)用程序了。
并行執(zhí)行不幸的是,雖然Generator很方便地進(jìn)行順序執(zhí)行,但是不能直接用來并行化執(zhí)行一組任務(wù),至少不能僅僅使用yield和Generator。之前,在種情況下我們使用的模式只是簡單地依賴于一個(gè)基于回調(diào)或者Promise的函數(shù),但使用了Generator函數(shù)后,一切會(huì)顯得更簡單。
幸運(yùn)的是,如果不限制并發(fā)數(shù)的并行執(zhí)行,co已經(jīng)可以通過yield一個(gè)Promise對象、thunk、Generator函數(shù),甚至包含Generator函數(shù)的數(shù)組來實(shí)現(xiàn)。
考慮到這一點(diǎn),我們的Web爬蟲應(yīng)用程序第三版可以通過重寫spiderLinks()函數(shù)來做如下改動(dòng):
function* spiderLinks(currentUrl, body, nesting) { if (nesting === 0) { return nextTick(); } const links = utilities.getPageLinks(currentUrl, body); const tasks = links.map(link => spider(link, nesting - 1)); yield tasks; }
但是上述函數(shù)所做的只是拿到所有的任務(wù),這些任務(wù)本質(zhì)上都是通過Generator函數(shù)來實(shí)現(xiàn)異步的,如果在co的thunk內(nèi)對一個(gè)包含Generator函數(shù)的數(shù)組使用yield,這些任務(wù)都會(huì)并行執(zhí)行。外層的Generator函數(shù)會(huì)等到yield子句的所有異步任務(wù)并行執(zhí)行后再繼續(xù)執(zhí)行。
接下來我們看怎么用一個(gè)基于回調(diào)函數(shù)的方式來解決相同的并行流。我們用這種方式重寫spiderLinks()函數(shù):
function spiderLinks(currentUrl, body, nesting) { if (nesting === 0) { return nextTick(); } // 返回一個(gè)thunk return callback => { let completed = 0, hasErrors = false; const links = utilities.getPageLinks(currentUrl, body); if (links.length === 0) { return process.nextTick(callback); } function done(err, result) { if (err && !hasErrors) { hasErrors = true; return callback(err); } if (++completed === links.length && !hasErrors) { callback(); } } for (let i = 0; i < links.length; i++) { co(spider(links[i], nesting - 1)).then(done); } } }
我們使用co并行運(yùn)行spider()函數(shù),調(diào)用Generator函數(shù)返回了一個(gè)Promise對象。這樣,等待Promise完成后調(diào)用done()函數(shù)。通常,基于Generator控制流的庫都有這一功能,因此如果需要,你總是可以將一個(gè)Generator轉(zhuǎn)換成一個(gè)基于回調(diào)或基于Promise的函數(shù)。
為了并行開啟多個(gè)下載任務(wù),我們只要重用在前面定義的基于回調(diào)的并行執(zhí)行的模式。我們應(yīng)該也注意到我們將spiderLinks()轉(zhuǎn)換成一個(gè)thunk(而不再是一個(gè)Generator函數(shù))。這使得當(dāng)全部并行任務(wù)完成時(shí),我們有一個(gè)回調(diào)函數(shù)可以調(diào)用。
限制并行執(zhí)行上面講到的是將一個(gè)Generator函數(shù)轉(zhuǎn)換為一個(gè)thunk的模式,使之能夠支持其他的基于回調(diào)或基于Promise的控制流算法,并可以通過同步阻塞的代碼風(fēng)格書寫異步代碼。
現(xiàn)在我們知道如何處理異步執(zhí)行流程,應(yīng)該很容易規(guī)劃我們的Web爬蟲應(yīng)用程序的第四版的實(shí)現(xiàn),這個(gè)版本對并發(fā)下載任務(wù)的數(shù)量施加了限制。我們有幾個(gè)方案可以用來做到這一點(diǎn)。其中一些方案如下:
使用先前實(shí)現(xiàn)的基于回調(diào)的TaskQueue類。我們只需要thunkify我們的Generator函數(shù)和其提供的回調(diào)函數(shù)即可。
使用基于Promise的TaskQueue類,并確保每個(gè)作為任務(wù)的Generator函數(shù)都被轉(zhuǎn)換成一個(gè)返回Promise對象的函數(shù)。
使用async,thunkify我們打算使用的工具函數(shù),此外還需要把我們用到的Generator函數(shù)轉(zhuǎn)化為基于回調(diào)的模式,以便于能夠被這個(gè)庫較好地使用。
使用基于co的生態(tài)系統(tǒng)中的庫,特別是專門為這種場景的庫,如co-limiter。
實(shí)現(xiàn)基于生產(chǎn)者 - 消費(fèi)者模型的自定義算法,這與co-limiter的內(nèi)部實(shí)現(xiàn)原理相同。
為了學(xué)習(xí),我們選擇最后一個(gè)方案,甚至幫助我們可以更好地理解一種經(jīng)常與協(xié)程(也和線程和進(jìn)程)同步相關(guān)的模式。
生產(chǎn)者 - 消費(fèi)者模式我們的目標(biāo)是利用隊(duì)列來提供固定數(shù)量的workers,與我們想要設(shè)置的并發(fā)級(jí)別一樣多。為了實(shí)現(xiàn)這個(gè)算法,我們將基于本章前面定義的TaskQueue類改寫:
class TaskQueue { constructor(concurrency) { this.concurrency = concurrency; this.running = 0; this.taskQueue = []; this.consumerQueue = []; this.spawnWorkers(concurrency); } pushTask(task) { if (this.consumerQueue.length !== 0) { this.consumerQueue.shift()(null, task); } else { this.taskQueue.push(task); } } spawnWorkers(concurrency) { const self = this; for (let i = 0; i < concurrency; i++) { co(function*() { while (true) { const task = yield self.nextTask(); yield task; } }); } } nextTask() { return callback => { if (this.taskQueue.length !== 0) { return callback(null, this.taskQueue.shift()); } this.consumerQueue.push(callback); } } }
讓我們分析這個(gè)TaskQueue類的新實(shí)現(xiàn)。首先是在構(gòu)造函數(shù)中。需要調(diào)用一次this.spawnWorkers(),因?yàn)檫@是啟動(dòng)worker的方法。
我們的worker很簡單,它們只是用co()包裝的立即執(zhí)行的Generator函數(shù),所以每個(gè)Generator函數(shù)可以并行執(zhí)行。在內(nèi)部,每個(gè)worker正在運(yùn)行在一個(gè)死循環(huán)(while(true){})中,一直阻塞(yield)到新任務(wù)在隊(duì)列中可用時(shí)(yield self.nextTask()),一旦可以執(zhí)行新任務(wù),yield這個(gè)異步任務(wù)直到其完成。您可能想知道我們?nèi)绾文軌蛳拗撇⑿袌?zhí)行,并讓下一個(gè)任務(wù)在隊(duì)列中處于等待狀態(tài)。答案是在nextTask()方法中。我們來詳細(xì)地看看在這個(gè)方法的原理:
nextTask() { return callback => { if (this.taskQueue.length !== 0) { return callback(null, this.taskQueue.shift()); } this.consumerQueue.push(callback); } }
我們看這個(gè)函數(shù)內(nèi)部發(fā)生了什么,這才是這個(gè)模式的核心:
這個(gè)方法返回一個(gè)對于co而言是一個(gè)合法的yieldable的thunk。
只要taskQueue類生成的實(shí)例中還有下一個(gè)任務(wù),thunk的回調(diào)函數(shù)會(huì)被立即調(diào)用。回調(diào)函數(shù)調(diào)用時(shí),立馬解鎖一個(gè)worker的阻塞狀態(tài),yield這一個(gè)任務(wù)。
如果隊(duì)列中沒有任務(wù)了,回調(diào)函數(shù)本身會(huì)被放入consumerQueue中。通過這種做法,我們將一個(gè)worker置于空閑(idle)的模式。一旦我們有一個(gè)新的任務(wù)來要處理,在consumerQueue隊(duì)列中的回調(diào)函數(shù)會(huì)被調(diào)用,立馬喚醒我們這一worker進(jìn)行異步處理。
現(xiàn)在,為了理解consumerQueue隊(duì)列中的空閑worker是如何恢復(fù)工作的,我們需要分析pushTask()方法。如果當(dāng)前有回調(diào)函數(shù)可用的話,pushTask()方法將調(diào)用consumerQueue隊(duì)列中的第一個(gè)回調(diào)函數(shù),從而將取消對worker的鎖定。如果沒有可用的回調(diào)函數(shù),這意味著所有的worker都是工作狀態(tài),只需要添加一個(gè)新的任務(wù)到taskQueue任務(wù)隊(duì)列中。
在TaskQueue類中,worker充當(dāng)消費(fèi)者的角色,而調(diào)用pushTask()函數(shù)的角色可以被認(rèn)為是生產(chǎn)者。這個(gè)模式向我們展示了一個(gè)Generator函數(shù)實(shí)際上可以跟一個(gè)線程或進(jìn)程類似。實(shí)際上,生產(chǎn)者 - 消費(fèi)者之間問題是研究進(jìn)程間通信和同步時(shí)最常見的問題,但正如我們已經(jīng)提到的那樣,它對于進(jìn)程和線程來說,也是一個(gè)常見的例子。
限制下載任務(wù)的并發(fā)量既然我們已經(jīng)使用Generator函數(shù)和生產(chǎn)者 - 消費(fèi)者模型實(shí)現(xiàn)一個(gè)限制并行算法,并且已經(jīng)在Web爬蟲應(yīng)用程序第四版應(yīng)用它來限制中下載任務(wù)的并發(fā)數(shù)。 首先,我們加載和初始化一個(gè)TaskQueue對象:
const TaskQueue = require("./taskQueue"); const downloadQueue = new TaskQueue(2);
然后,修改spiderLinks()函數(shù)。和之前不限制并發(fā)的版本類似,所以這里我們只展示修改的部分,主要是通過調(diào)用新版本的TaskQueue類生成的實(shí)例的pushTask()方法來限制并行執(zhí)行:
function spiderLinks(currentUrl, body, nesting) { //... return (callback) => { //... function done(err, result) { //... } links.forEach(function(link) { downloadQueue.pushTask(function*() { yield spider(link, nesting - 1); done(); }); }); } }
在每個(gè)任務(wù)中,我們在下載完成后立即調(diào)用done()函數(shù),因此我們可以計(jì)算下載了多少個(gè)鏈接,然后在完成下載時(shí)通知thunk的回調(diào)函數(shù)執(zhí)行。
配合Babel使用Async await新語法回調(diào)函數(shù)、Promise和Generator函數(shù)都是用于處理JavaScript和Node.js異步問題的方式。正如我們所看到的,Generator的真正意義在于它提供了一種方式來暫停一個(gè)函數(shù)的執(zhí)行,然后等待前面的任務(wù)完成后再繼續(xù)執(zhí)行。我們可以使用這樣的特性來書寫異步代碼,并且讓開發(fā)者用同步阻塞的代碼風(fēng)格來書寫異步代碼。等到異步操作的結(jié)果返回后才恢復(fù)當(dāng)前函數(shù)的執(zhí)行。
但Generator函數(shù)是更多的是用來處理迭代器,然而迭代器在異步代碼的使用顯得有點(diǎn)笨重。代碼可能難以理解,導(dǎo)致代碼易讀性和可維護(hù)性差。
但在不遠(yuǎn)的將來會(huì)有一種更加簡潔的語法。實(shí)際上,這個(gè)提議即將引入到ESMASCript 2017的規(guī)范中,這項(xiàng)規(guī)范定義了async函數(shù)語法。
async函數(shù)規(guī)范引入兩個(gè)關(guān)鍵字(async和await)到原生的JavaScript語言中,改進(jìn)我們書寫異步代碼的方式。
為了理解這項(xiàng)語法的用法和優(yōu)勢為,我們看一個(gè)簡單的例子:
const request = require("request"); function getPageHtml(url) { return new Promise(function(resolve, reject) { request(url, function(error, response, body) { resolve(body); }); }); } async function main() { const html = await getPageHtml("http://google.com"); console.log(html); } main(); console.log("Loading...");
在上述代碼中,有兩個(gè)函數(shù):getPageHtml和main。第一個(gè)函數(shù)的作用是提取給定URL的一個(gè)遠(yuǎn)程網(wǎng)頁的HTML文檔代碼。值得注意的是,這個(gè)函數(shù)返回一個(gè)Promise對象。
重點(diǎn)在于main函數(shù),因?yàn)樵谶@里使用了async和await關(guān)鍵字。首先要注意的是函數(shù)要以async關(guān)鍵字為前綴。意思是這個(gè)函數(shù)執(zhí)行的是異步代碼并且允許它在函數(shù)體內(nèi)使用await關(guān)鍵字。await關(guān)鍵字在getPageHtml調(diào)用之前,告訴JavaScript解釋器在繼續(xù)執(zhí)行下一條指令之前,等待getPageHtml返回的Promise對象的結(jié)果。這樣,main函數(shù)內(nèi)部哪部分代碼是異步的,它會(huì)等待異步代碼的完成再繼續(xù)執(zhí)行后續(xù)操作,并且不會(huì)阻塞這段程序其余部分的正常執(zhí)行。實(shí)際上,控制臺(tái)會(huì)打印字符串Loading...,隨后是Google主頁的HTML代碼。
是不是這種方法的可讀性更好并且更容易理解呢? 不幸地是,這個(gè)提議尚未定案,即使通過這個(gè)提議,我們需要等下一個(gè)版本
的ECMAScript規(guī)范出來并把它集成到Node.js后,才能使用這個(gè)新語法。 所以我們今天做了什么?只是漫無目的地等待?不是,當(dāng)然不是!我們已經(jīng)可以在我們的代碼中使用async await語法,只要我們使用Babel。
Babel是一個(gè)JavaScript編譯器(或翻譯器),能夠使用語法轉(zhuǎn)換器將高版本的JavaScript代碼轉(zhuǎn)換成其他JavaScript代碼。語法轉(zhuǎn)換器允許例如我們書寫并使用ES2015,ES2016,JSX和其它的新語法,來翻譯成往后兼容的代碼,在JavaScript運(yùn)行環(huán)境如瀏覽器或Node.js中都可以使用Babel。
在項(xiàng)目中使用npm安裝Babel,命令如下:
npm install --save-dev babel-cli
我們還需要安裝插件以支持async await語法的解釋或翻譯:
npm install --save-dev babel-plugin-syntax-async-functions babel-plugin-tranform-async-to-generator
現(xiàn)在假設(shè)我們想運(yùn)行我們之前的例子(稱為index.js)。我們需要通過以下命令啟動(dòng):
node_modules/.bin/babel-node --plugins "syntax-async-functions,transform-async-to-generator" index.js
這樣,我們使用支持async await的轉(zhuǎn)換器動(dòng)態(tài)地轉(zhuǎn)換源代碼。Node.js運(yùn)行的實(shí)際是保存在內(nèi)存中的往后兼容的代碼。
Babel也能被配置為一個(gè)代碼構(gòu)建工具,保存翻譯或解釋后的代碼到本地文件系統(tǒng)中,便于我們部署和運(yùn)行生成的代碼。
幾種方式的比較關(guān)于如何安裝和配置Babel,可以到官方網(wǎng)站 https://babeljs.io 查閱相關(guān)文檔。
現(xiàn)在,我們應(yīng)該對于怎么處理JavaScript的異步問題有了一個(gè)更好的認(rèn)識(shí)和總結(jié)。在下面的表格中總結(jié)幾大機(jī)制的優(yōu)勢和劣勢:
總結(jié)值得一提的是,我們選擇在本章中僅介紹處理異步控制流程的最受歡迎的解決方案,或者是廣泛使用的解決方案,但是例如Fibers( https://npmjs.org/package/fibers )和Streamline( https://npmjs.org/p ackage/streamline )也是值得一看的。
在本章中,我們分析了一些處理異步控制流的方法,分析了Promise、Generator函數(shù)和即將到來的async await語法。
我們學(xué)習(xí)了如何使用這些方法編寫更簡潔,更具有可讀性的異步代碼。我們討論了這些方法的一些最重要的優(yōu)點(diǎn)和缺點(diǎn),并認(rèn)識(shí)到即使它們非常有用,也需要一些時(shí)間來掌握。這就是這幾種方式也沒有完全取代在許多情況下仍然非常有用的回調(diào)的原因。作為一名開發(fā)人員,應(yīng)該按照實(shí)際情況分析決定使用哪種解決方案。如果您正在構(gòu)建執(zhí)行異步操作的公共庫,則應(yīng)該提供易于使用的API,即使對于只想使用回調(diào)的開發(fā)人員也是如此。
在下一章中,我們將探討另一個(gè)與異步代碼執(zhí)行相關(guān)的機(jī)制,這也是整個(gè)Node.js生態(tài)系統(tǒng)中的另一個(gè)基本構(gòu)建塊:streams。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/89978.html
摘要:而且已開源出來,隨著容器技術(shù)發(fā)展,大文件分發(fā)一直是個(gè)重要的問題,所以是一件值得研究的技術(shù)。實(shí)用推薦檢定攻略是近期推出的一項(xiàng)認(rèn)證,用以認(rèn)證開發(fā)者的移動(dòng)網(wǎng)頁開發(fā)技能。凈化,移除中不必要的文件技術(shù)周刊由小組出品,匯聚一周好文章,周刊原文。 業(yè)界動(dòng)態(tài) 直擊阿里雙11神秘技術(shù):PB級(jí)大規(guī)模文件分發(fā)系統(tǒng)蜻蜓 文章主要介紹了阿里的PB級(jí)大規(guī)模文件分發(fā)系統(tǒng)蜻蜓, 通過使用P2P技術(shù)同時(shí)結(jié)合智能壓縮、智...
摘要:通過本文,我們將學(xué)習(xí)如何使用來改變開發(fā)流程,從而使開發(fā)更加快速高效。中文網(wǎng)站詳細(xì)入門教程使用是基于的,需要要安裝為了確保依賴環(huán)境正確,我們先執(zhí)行幾個(gè)簡單的命令檢查。詳盡使用參見官方文檔,中文文檔項(xiàng)目地址 為了UED前端團(tuán)隊(duì)更好的協(xié)作開發(fā)同時(shí)提高項(xiàng)目編碼質(zhì)量,我們需要將Web前端使用工程化方式構(gòu)建; 目前需要一些簡單的功能: 1. 壓縮HTML 2. 檢查JS 3. 編譯SA...
摘要:的新特性往往會(huì)增加代碼的,這些特性卻有助于緩解當(dāng)前的性能危機(jī),尤其像在手機(jī)設(shè)備這樣的新興市場上。聯(lián)合聲明我們短期目標(biāo)是盡快實(shí)現(xiàn)少于倍的性能改善。我們會(huì)繼續(xù)針對的特性提升其性能。定期發(fā)布高質(zhì)量文章。 作者:Alon Zakai 編譯:胡子大哈 翻譯原文:http://huziketang.com/blog/posts/detail?postId=58d11a9aa6d8a07e449f...
摘要:關(guān)于作者程序開發(fā)人員,不拘泥于語言與技術(shù),目前主要從事和前端開發(fā),使用和,端使用混合式開發(fā)。合適和夠用是最完美的追求。沒錯(cuò),是一款的后端框架。的靈感來自一個(gè)名為的框架。功能亮點(diǎn)是圍繞實(shí)際用例構(gòu)建的。讓您忘掉傳統(tǒng)查詢,擁抱優(yōu)雅的數(shù)據(jù)模型。 關(guān)于作者 程序開發(fā)人員,不拘泥于語言與技術(shù),目前主要從事PHP和前端開發(fā),使用Laravel和VueJs,App端使用Apicloud混合式開發(fā)。合...
摘要:關(guān)于作者程序開發(fā)人員,不拘泥于語言與技術(shù),目前主要從事和前端開發(fā),使用和,端使用混合式開發(fā)。合適和夠用是最完美的追求。沒錯(cuò),是一款的后端框架。的靈感來自一個(gè)名為的框架。功能亮點(diǎn)是圍繞實(shí)際用例構(gòu)建的。讓您忘掉傳統(tǒng)查詢,擁抱優(yōu)雅的數(shù)據(jù)模型。 關(guān)于作者 程序開發(fā)人員,不拘泥于語言與技術(shù),目前主要從事PHP和前端開發(fā),使用Laravel和VueJs,App端使用Apicloud混合式開發(fā)。合...
閱讀 1917·2021-11-23 09:51
閱讀 1246·2019-08-30 15:55
閱讀 1613·2019-08-30 15:44
閱讀 759·2019-08-30 14:11
閱讀 1146·2019-08-30 14:10
閱讀 915·2019-08-30 13:52
閱讀 2630·2019-08-30 12:50
閱讀 615·2019-08-29 15:04