摘要:回調(diào)方式將回調(diào)函數(shù)作為參數(shù)傳遞給主函數(shù),同時在主函數(shù)內(nèi)部處理錯誤信息。模塊是促進中對象之間交流的模塊,它是異步事件驅(qū)動機制的核心。在異步函數(shù)的回調(diào)中,根據(jù)執(zhí)行情況觸發(fā)或者事件。比如,當(dāng)異常事件觸發(fā)關(guān)閉數(shù)據(jù)庫的動作時。
原文鏈接:Understanding Nodejs Event-driven Architecture
作者:Samer Buna
翻譯:野草
本文首發(fā)于前端早讀課【第958期】
Node中的絕大多數(shù)對象,比如HTTP請求,響應(yīng),流,都是實現(xiàn)了EventEmitter模塊,所以它們可以觸發(fā)或監(jiān)聽事件。
const EventEmitter = require("events");
能體現(xiàn)事件驅(qū)動機制本質(zhì)的最簡單形式就是函數(shù)的回調(diào),比如Node中常用的fs.readFile。在這個例子中,事件僅觸發(fā)一次(當(dāng)Node完成文件的讀取操作后),回調(diào)函數(shù)也就充當(dāng)了事件處理者的身份。
讓我們更深入地探究一下回調(diào)形式。
Node的回調(diào)Node處理異步事件最開始使用的是回調(diào)。很久之后(也就是現(xiàn)在),原生JavaScript有了Promise對象和async/await特性來處理異步。
回調(diào)函數(shù)其實就是作為函數(shù)參數(shù)的函數(shù),這個概念的實現(xiàn)得益于JavaScript語言中的函數(shù)是第一類對象。
但我們必須要搞清楚,回調(diào)并不意味著異步。函數(shù)的回調(diào)可以是同步的,也可以是異步的。
比如,下例中的主函數(shù)fileSize接受一個名為cb的回調(diào)函數(shù)。該回調(diào)函數(shù)可以根據(jù)判斷條件來決定是同步執(zhí)行還是異步執(zhí)行回調(diào)。
function fileSize (fileName, cb) { if (typeof fileName !== "string") { return cb(new TypeError("argument should be string")); // 同步調(diào)用 } fs.stat(fileName, (err, stats) => { if (err) { return cb(err); } // 異步調(diào)用 cb(null, stats.size); // 異步調(diào)用 }); }
注意,這是不好的實踐,很容易出現(xiàn)意想不到的bug。設(shè)計主函數(shù)時,回調(diào)函數(shù)的調(diào)用應(yīng)該總是同步或者異步的。
再看一個經(jīng)典的異步回調(diào)例子:
const readFileAsArray = function(file, cb) { fs.readFile(file, function(err, data) { if (err) { return cb(err); } const lines = data.toString().trim().split(" "); cb(null, lines); }); };
readFileAsArray接收一個文件路徑參數(shù)以及一個回調(diào)函數(shù)。它讀取文件內(nèi)容,將內(nèi)容拆分成數(shù)組lines,然后調(diào)用回調(diào)函數(shù)處理這個數(shù)組。
舉個實例。假設(shè)有個numbers.txt文件,內(nèi)容如下:
//numbers.txt 10 11 12 13 14 15
現(xiàn)需要計算文件中奇數(shù)的個數(shù),上面的readFileAsArray函數(shù)就可以利用起來了:
readFileAsArray("./numbers.txt", (err, lines) => { if (err) throw err; const numbers = lines.map(Number); const oddNumbers = numbers.filter(n => n%2 === 1); console.log("Odd numbers count:", oddNumbers.length); });
這段代碼讀取txt文件中的數(shù)字成字符數(shù)組,解析成數(shù)字,然后計算出奇數(shù)的個數(shù)。
此處的回調(diào)函數(shù)用得恰到好處。主函數(shù)將回調(diào)函數(shù)作為最后一個參數(shù),而回調(diào)函數(shù)的第一個參數(shù)是可為null的錯誤信息參數(shù)err。這種參數(shù)傳遞方式是開發(fā)者默認(rèn)的規(guī)則,你最好也遵守:將回調(diào)作為主函數(shù)的最后一個參數(shù),將錯誤信息作為回調(diào)函數(shù)的第一個參數(shù)。
Promise:回調(diào)的取代者如今,JavaScript有了Promise對象,異步可以不再需要回調(diào)了。回調(diào)方式將回調(diào)函數(shù)作為參數(shù)傳遞給主函數(shù),同時在主函數(shù)內(nèi)部處理錯誤信息。Promise對象則不同,它可以多帶帶處理成功/失敗情況,也可以鏈接多個異步調(diào)用,而不是嵌套處理。
如果readFileAsArray函數(shù)支持Promise寫法,我們就可以這么用:
readFileAsArray("./numbers.txt") .then(lines => { const numbers = lines.map(Number); const oddNumbers = numbers.filter(n => n%2 === 1); console.log("Odd numbers count:", oddNumbers.length); }) .catch(console.error);
Promise用法使得我們可以直接在主函數(shù)的返回值上調(diào)用.then函數(shù),而不是傳入一個回調(diào)函數(shù)。.then函數(shù)能獲取到之前用回調(diào)獲取的內(nèi)容,并且執(zhí)行相同的業(yè)務(wù)操作。繼續(xù)添加.catch函數(shù),捕捉可能會產(chǎn)生的錯誤信息。
由于原生JavaScript自帶 Promise對象,主函數(shù)很容易改造成支持Promise接口。以下是改造后的結(jié)合回調(diào)方式的readFileAsArray。
const readFileAsArray = function(file, cb = () => {}) { return new Promise((resolve, reject) => { fs.readFile(file, function(err, data) { if (err) { reject(err); return cb(err); } const lines = data.toString().trim().split(" "); resolve(lines); cb(null, lines); }); }); };
現(xiàn)在這個函數(shù)返回一個包含fs.readFile異步調(diào)用的Promise對象。Promise對象有兩個參數(shù),resolve函數(shù)和reject函數(shù)。
當(dāng)我們獲取了錯誤信息需要回調(diào)時,用reject處理信息;反之,當(dāng)我們獲取結(jié)果數(shù)據(jù)需要回調(diào)時,用resolve來處理。
另外,回調(diào)函數(shù)要指定一個缺省值,以免直接用Promise接口調(diào)用,這里我們指定為空函數(shù)()=>{}。
Promise升級:結(jié)合async/await使用當(dāng)異步遇到循環(huán)的時候,Promise接口會讓代碼簡單很多。用回調(diào)的話,代碼容易混亂。處理異步的最新特性是async函數(shù),它能讓我們像處理同步函數(shù)一樣處理異步函數(shù),使得代碼更具可讀性。
我們用async/await的方式調(diào)用readFileAsArray:
async function countOdd () { try { const lines = await readFileAsArray("./numbers"); const numbers = lines.map(Number); const oddCount = numbers.filter(n => n%2 === 1).length; console.log("Odd numbers count:", oddCount); } catch(err) { console.error(err); } } countOdd();
首先創(chuàng)建一個異步函數(shù),其實就是一個帶async關(guān)鍵詞的普通函數(shù)。函數(shù)內(nèi)部,在readFileAsArray函數(shù)前面加上await關(guān)鍵詞,保證lines結(jié)果返回才執(zhí)行下一行。
執(zhí)行這個異步函數(shù)countOdd,就能得到我們想要的結(jié)果。代碼看起來簡單且更具可讀性。需要注意的是,我們需要用try/catch處理這個異步調(diào)用,以免出錯。
有了async/await特性之后,我們不再需要像.then,.catch之類的特殊接口。我們僅僅標(biāo)記一下函數(shù),然后用純原生的代碼寫書。
我們可以給所有支持Promise接口的函數(shù)添加async/await特性,不過,不包括異步回調(diào)的函數(shù),比如setTimeout。
EventEmitter模塊EventEmitter是促進Node中對象之間交流的模塊,它是Node異步事件驅(qū)動機制的核心。Node中很多自帶的模塊都繼承自事件觸發(fā)模塊。
概念很簡單:觸發(fā)器觸發(fā)事件,該事件對應(yīng)的監(jiān)聽函數(shù)被調(diào)用。也就是說,觸發(fā)器有兩個特征:
觸發(fā)某個事件
注冊/注銷監(jiān)聽函數(shù)
我們創(chuàng)建一個繼承EventEmitter模塊的類:
class MyEmitter extends EventEmitter { }
實例化該類,得到一個事件觸發(fā)器:
const myEmitter = new MyEmitter();
在事件觸發(fā)器的生命周期任何時候,我們都能利用emit函數(shù)觸發(fā)已有的事件。
myEmitter.emit("something-happened");
觸發(fā)事件意味著某些情況發(fā)生,通常是關(guān)于觸發(fā)器的狀態(tài)變化。
使用on方法添加某個事件的監(jiān)聽函數(shù),每次觸發(fā)器觸發(fā)事件時,對應(yīng)的監(jiān)聽函數(shù)就會被執(zhí)行。
事件!==異步看個例子:
const EventEmitter = require("events"); class WithLog extends EventEmitter { execute(taskFunc) { console.log("Before executing"); this.emit("begin"); taskFunc(); this.emit("end"); console.log("After executing"); } } const withLog = new WithLog(); withLog.on("begin", () => console.log("About to execute")); withLog.on("end", () => console.log("Done with execute")); withLog.execute(() => console.log("*** Executing task ***"));
WithLog類是事件觸發(fā)器,它里面定義了execute函數(shù)。該函數(shù)接收一個任務(wù)函數(shù)的參數(shù),頭尾分別用打印語句打印提示信息,并且在任務(wù)函數(shù)執(zhí)行前后觸發(fā)事件。
為了弄清楚執(zhí)行順序,我們注冊好事件的監(jiān)聽函數(shù),給定一個簡單的任務(wù)函數(shù),然后執(zhí)行代碼。
運行的結(jié)果如下:
Before executing About to execute *** Executing task *** Done with execute After executing
注意,上述的結(jié)果說明代碼執(zhí)行是同步的,沒有任何異步代碼。
首先輸出Before executing
begin事件觸發(fā)對應(yīng)的監(jiān)聽函數(shù),函數(shù)執(zhí)行輸出About to execute
任務(wù)函數(shù)執(zhí)行并且輸出*** Executing task ***
end事件觸發(fā)對應(yīng)的監(jiān)聽函數(shù),函數(shù)執(zhí)行輸出Done with execute
最后輸出After executing
正如回調(diào)一樣,不要想當(dāng)然地認(rèn)為事件一定是同步或者異步的。
明白這點至關(guān)重要,如果給execute函數(shù)傳入異步的taskFunc,事件觸發(fā)時機就不準(zhǔn)確了。
我們可以借助setImmediate函數(shù)模擬異步的函數(shù):
// ... withLog.execute(() => { setImmediate(() => { console.log("*** Executing task ***") }); });
執(zhí)行結(jié)果如下:
Before executing About to execute Done with execute After executing *** Executing task ***
執(zhí)行的結(jié)果是有問題的,異步調(diào)用之后的那些代碼,即輸出Done with execute,After executing的部分,不再是正確有效的提示。
若要在異步函數(shù)執(zhí)行完畢之后觸發(fā)事件,需要結(jié)合回調(diào)或者Promise對象。下文會具體講到如何解決。
相對于一般的回調(diào),事件觸發(fā)的優(yōu)點在于可以通過定義多個監(jiān)聽函數(shù)來達到一個事件觸發(fā)多個函數(shù)的執(zhí)行。如果用回調(diào)方式,需要在單個回調(diào)函數(shù)中寫很多代碼邏輯。
異步事件我們將上面這個同步的例子再修改一下,變成實用一點的異步例子。
const fs = require("fs"); const EventEmitter = require("events"); class WithTime extends EventEmitter { execute(asyncFunc, ...args) { this.emit("begin"); console.time("execute"); asyncFunc(...args, (err, data) => { if (err) { return this.emit("error", err); } this.emit("data", data); console.timeEnd("execute"); this.emit("end"); }); } } const withTime = new WithTime(); withTime.on("begin", () => console.log("About to execute")); withTime.on("end", () => console.log("Done with execute")); withTime.execute(fs.readFile, __filename);
WithTime類執(zhí)行異步函數(shù)asyncFunc,通過console.time和 console.timeEnd打印出異步函數(shù)執(zhí)行所需的時間,并且在函數(shù)執(zhí)行前后觸發(fā)正確的事件。在異步函數(shù)的回調(diào)中,根據(jù)執(zhí)行情況觸發(fā)error或者data事件。
我們傳入異步函數(shù)fs.readFile來測試WithTime。 現(xiàn)在我們不再需要通過回調(diào)來處理讀取后的文件數(shù)據(jù),我們只要監(jiān)聽data事件就好了。
執(zhí)行之后,我們得到正確的事件觸發(fā)結(jié)果,也得到了函數(shù)執(zhí)行所需的時間。
About to execute execute: 4.507ms Done with execute
我們可以看到上述代碼是如何結(jié)合回調(diào)和事件觸發(fā)器完成的。如果asyncFunc支持Promise的話,我們還可以用async/await來代替。
class WithTime extends EventEmitter { async execute(asyncFunc, ...args) { this.emit("begin"); try { console.time("execute"); const data = await asyncFunc(...args); this.emit("data", data); console.timeEnd("execute"); this.emit("end"); } catch(err) { this.emit("error", err); } } }
我不知道你怎么看,但對我來說這代碼比起回調(diào)或者.then/.catch來說清晰多了。async/await特性讓我們更近距離地接觸JavaScript語言本身,我覺得非常棒。
事件參數(shù)和錯誤處理上一個例子中,有兩個事件觸發(fā)時附帶額外參數(shù)。
error事件觸發(fā)時帶有錯誤信息:
this.emit("error", err);
而data事件對應(yīng)的是數(shù)據(jù)信息:
this.emit("data", data);
我們可以在事件參數(shù)后面帶上任意多的參數(shù),這些參數(shù)會作為對應(yīng)監(jiān)聽函數(shù)的參數(shù)。
比如,我們傳入的data參數(shù)會被注冊的監(jiān)聽函數(shù)接收,而這個data對象正好是異步函數(shù)asyncFunc返回的結(jié)果數(shù)據(jù)。
withTime.on("data", (data) => { // do something with data });
error事件比較特殊,在那個回調(diào)例子中,如果我們不人為處理錯誤事件,node進程會自動退出。
下面例子可以證明:
class WithTime extends EventEmitter { execute(asyncFunc, ...args) { console.time("execute"); asyncFunc(...args, (err, data) => { if (err) { return this.emit("error", err); // 未被處理 } console.timeEnd("execute"); }); } } const withTime = new WithTime(); withTime.execute(fs.readFile, ""); // 不好的調(diào)用 withTime.execute(fs.readFile, __filename);
第一次調(diào)用會拋出錯誤,node進程崩潰然后自動退出;
events.js:163 throw er; // Unhandled "error" event ^ Error: ENOENT: no such file or directory, open ""
第二次調(diào)用受上一行的崩潰影響,根本就沒有機會執(zhí)行。
如果我們注冊error事件的監(jiān)聽函數(shù),結(jié)果就不一樣。比如:
withTime.on("error", (err) => { // 處理錯誤信息, 比如說打印出來 console.log(err) });
如有上述代碼存在,第一次調(diào)用的錯誤會被報告,node進程不會像之前一樣崩潰退出。這也就意味著第二次調(diào)用正常進行:
{ Error: ENOENT: no such file or directory, open "" errno: -2, code: "ENOENT", syscall: "open", path: "" } execute: 4.276ms
但是,如果是Promise形式函數(shù)的話,Node中表現(xiàn)又會不一樣,它只會輸出警告:
UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: ENOENT: no such file or directory, open "" DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
處理error事件觸發(fā)的異常的另一種方式是注冊一個監(jiān)聽全局uncaughtException進程事件的函數(shù),但這并不是個好主意。
一般情況下,建議避免使用uncaughtException。但如果非用不可(比如打印日志或者清理工作之類的),必須在監(jiān)聽函數(shù)中退出進程。
process.on("uncaughtException", (err) => { // 還不夠 console.error(err); // 還需要強制推出進程 process.exit(1); });
問題是,如果同時有多個錯誤事件觸發(fā),就會多次觸發(fā)uncaughtException事件注冊的監(jiān)聽函數(shù),多次清理工作可能會造成問題。比如,當(dāng)異常事件觸發(fā)關(guān)閉數(shù)據(jù)庫的動作時。
EventEmitter模塊暴露一個once方法,限制了事件觸發(fā)的監(jiān)聽函數(shù)只能被調(diào)用一次。它很適用未捕獲異常的情況,因為只要第一次異常發(fā)生,我們就會開始清理,然后退出進程。
監(jiān)聽函數(shù)的順序如果給一個事件注冊了多個監(jiān)聽函數(shù),它們的調(diào)用是有序進行的。調(diào)用的順序跟注冊的順序保持一致。
// 第一個監(jiān)聽函數(shù) withTime.on("data", (data) => { console.log(`Length: ${data.length}`); }); // 第二個監(jiān)聽函數(shù) withTime.on("data", (data) => { console.log(`Characters: ${data.toString().length}`); }); withTime.execute(fs.readFile, __filename);
上述代碼執(zhí)行后,會先打印出Length這行信息,然后再打印Characters這行信息,因為這是監(jiān)聽函數(shù)的注冊順序。
如果想讓定義在后面的監(jiān)聽函數(shù)先調(diào)用,可以通過prependListener方法:
// 第一個監(jiān)聽函數(shù) withTime.on("data", (data) => { console.log(`Length: ${data.length}`); }); // 第二個監(jiān)聽函數(shù) withTime.prependListener("data", (data) => { console.log(`Characters: ${data.toString().length}`); }); withTime.execute(fs.readFile, __filename);
這樣就會先打印出Characters這行信息了。
最后,如果想要移除某個監(jiān)聽函數(shù),用removeListener方法。
【譯者注】如果你看到這里,那么謝謝你耐心地看完了本文。是不是有著滿滿的疑惑,不是講事件驅(qū)動架構(gòu)嗎,怎么看完一臉懵逼?很巧,我第一次看完這篇文章的時候也是這種感受,直到現(xiàn)在我也沒很理解題目與文章內(nèi)容的聯(lián)系。不過,反正我看完有點收獲,關(guān)于異步,事件等等。希望你也有點收獲吧,至少也花了時間閱讀了。
野草,前端早讀課專欄作者。為社區(qū)持續(xù)輸出優(yōu)秀前沿的前端技術(shù)文章翻譯,歡迎關(guān)注【野草】,也歡迎關(guān)注【前端早讀課】微信公眾號。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/87007.html
摘要:文件系統(tǒng)請求和相關(guān)請求都會放進這個線程池處理其他的請求,如網(wǎng)絡(luò)平臺特性相關(guān)的請求會分發(fā)給相應(yīng)的系統(tǒng)處理單元參見設(shè)計概覽。 譯者按:在 Medium 上看到這篇文章,行文脈絡(luò)清晰,闡述簡明利落,果斷點下翻譯按鈕。第一小節(jié)背景鋪陳略啰嗦,可以略過。剛開始我給這部分留了個 blah blah blah 直接翻后面的,翻完之后回頭看,考慮完整性才把第一節(jié)給補上。接下來的內(nèi)容干貨滿滿,相信對 N...
摘要:事件驅(qū)動機制的最簡單形式,是在中十分流行的回調(diào)函數(shù),例如。在回調(diào)函數(shù)這種形式中,事件每被觸發(fā)一次,回調(diào)就會被觸發(fā)一次。回調(diào)函數(shù)需要作為宿主函數(shù)的一個參數(shù)進行傳遞多個宿主回調(diào)進行嵌套就形成了回調(diào)地獄,而且錯誤和成功都只能在其中進行處理。 學(xué)習(xí) Node.js 一定要理解的內(nèi)容之一,文中主要涉及到了 EventEmitter 的使用和一些異步情況的處理,比較偏基礎(chǔ),值得一讀。 閱讀原文 大...
摘要:事件處理器,則是當(dāng)指定事件觸發(fā)時,執(zhí)行的一段代碼。事件循環(huán)以一個無限循環(huán)的形式啟動,存在于二進制文件里函數(shù)的最后,當(dāng)沒有更多可被執(zhí)行的事件處理器時,它就退出。 前言 如果你了解過Node.js,那么你一定聽說過事件循環(huán)。你一定想知道它為什么那么特殊,并且為什么你需要關(guān)注它?此時此刻的你,可能已經(jīng)寫過許多基于Express.js的后端代碼,但沒有接觸到任何的循環(huán)。 在下文中,我們會先在一...
摘要:文章的第二部分涵蓋了內(nèi)存管理的概念,不久后將發(fā)布。的標(biāo)準(zhǔn)化工作是由國際組織負責(zé)的,相關(guān)規(guī)范被稱為或者。隨著分析器和編譯器不斷地更改字節(jié)碼,的執(zhí)行性能逐漸提高。 原文地址:How Does JavaScript Really Work? (Part 1) 原文作者:Priyesh Patel 譯者:Chor showImg(https://segmentfault.com/img...
原文 先說1.1總攬: Reactor模式 Reactor模式中的協(xié)調(diào)機制Event Loop Reactor模式中的事件分離器Event Demultiplexer 一些Event Demultiplexer處理不了的復(fù)雜I/O接口比如File I/O、DNS等 復(fù)雜I/O的解決方案 未完待續(xù) 前言 nodejs和其他編程平臺的區(qū)別在于如何去處理I/O接口,我們聽一個人介紹nodejs,總是...
閱讀 870·2021-11-22 09:34
閱讀 1003·2021-10-08 10:16
閱讀 1816·2021-07-25 21:42
閱讀 1790·2019-08-30 15:53
閱讀 3519·2019-08-30 13:08
閱讀 2174·2019-08-29 17:30
閱讀 3342·2019-08-29 17:22
閱讀 2173·2019-08-29 15:35