成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專(zhuān)欄INFORMATION COLUMN

《Node.js設(shè)計(jì)模式》Node.js基本模式

Seay / 1755人閱讀

摘要:回調(diào)函數(shù)是在異步操作完成后傳播其操作結(jié)果的函數(shù),總是用來(lái)替代同步操作的返回指令。下面的圖片顯示了中事件循環(huán)過(guò)程當(dāng)異步操作完成時(shí),執(zhí)行權(quán)就會(huì)交給這個(gè)異步操作開(kāi)始的地方,即回調(diào)函數(shù)。

本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書(shū)筆記,在GitHub連載更新,同步翻譯版鏈接。

歡迎關(guān)注我的專(zhuān)欄,之后的博文將在專(zhuān)欄同步:

Encounter的掘金專(zhuān)欄

知乎專(zhuān)欄 Encounter的編程思考

segmentfault專(zhuān)欄 前端小站

Node.js Essential Patterns

對(duì)于Node.js而言,異步特性是其最顯著的特征,但對(duì)于別的一些語(yǔ)言,例如PHP,就不常處理異步代碼。

在同步的編程中,我們習(xí)慣于把代碼的執(zhí)行想象為自上而下連續(xù)的執(zhí)行計(jì)算步驟。每個(gè)操作都是阻塞的,這意味著只有在一個(gè)操作執(zhí)行完成后才能執(zhí)行下一個(gè)操作,這種方式利于我們理解和調(diào)試。

然而,在異步的編程中,我們可以在后臺(tái)執(zhí)行諸如讀取文件或執(zhí)行網(wǎng)絡(luò)請(qǐng)求的一些操作。當(dāng)我們?cè)谡{(diào)用異步操作方法時(shí),即使當(dāng)前或之前的操作尚未完成,下面的后續(xù)操作也會(huì)繼續(xù)執(zhí)行,在后臺(tái)執(zhí)行的操作會(huì)在任意時(shí)刻執(zhí)行完畢,并且應(yīng)用程序會(huì)在異步調(diào)用完成時(shí)以正確的方式做出反應(yīng)。

雖然這種非阻塞方法相比于阻塞方法性能更好,但它實(shí)在是讓程序員難以理解,并且,在處理較為復(fù)雜的異步控制流的高級(jí)應(yīng)用程序時(shí),異步順序可能會(huì)變得難以操作。

Node.js提供了一系列工具和設(shè)計(jì)模式,以便我們最佳地處理異步代碼。了解如何使用它們編寫(xiě)性能和易于理解和調(diào)試的應(yīng)用程序非常重要。

在本章中,我們將看到兩個(gè)最重要的異步模式:回調(diào)和事件發(fā)布器。

回調(diào)模式

在上一章中介紹過(guò),回調(diào)是reactor模式handler的實(shí)例,回調(diào)本來(lái)就是Node.js獨(dú)特的編程風(fēng)格之一?;卣{(diào)函數(shù)是在異步操作完成后傳播其操作結(jié)果的函數(shù),總是用來(lái)替代同步操作的返回指令。而JavaScript恰好就是表示回調(diào)的最好的語(yǔ)言。在JavaScript中,函數(shù)是一等公民,我們可以把函數(shù)變量作為參數(shù)傳遞,并在另一個(gè)函數(shù)中調(diào)用它,把調(diào)用的結(jié)果存儲(chǔ)到某一數(shù)據(jù)結(jié)構(gòu)中。實(shí)現(xiàn)回調(diào)的另一個(gè)理想結(jié)構(gòu)是閉包。使用閉包,我們能夠保留函數(shù)創(chuàng)建時(shí)所在的上下文環(huán)境,這樣,無(wú)論何時(shí)調(diào)用回調(diào),都保持了請(qǐng)求異步操作的上下文。

在本節(jié)中,我們分析基于回調(diào)的編程思想和模式,而不是同步操作的返回指令的模式。

CPS

JavaScript中,回調(diào)函數(shù)作為參數(shù)傳遞給另一個(gè)函數(shù),并在操作完成時(shí)調(diào)用。在函數(shù)式編程中,這種傳遞結(jié)果的方法被稱(chēng)為CPS。這是一個(gè)一般概念,而且不只是對(duì)于異步操作而言。實(shí)際上,它只是通過(guò)將結(jié)果作為參數(shù)傳遞給另一個(gè)函數(shù)(回調(diào)函數(shù))來(lái)傳遞結(jié)果,然后在主體邏輯中調(diào)用回調(diào)函數(shù)拿到操作結(jié)果,而不是直接將其返回給調(diào)用者。

同步CPS

為了更清晰地理解CPS,讓我們來(lái)看看這個(gè)簡(jiǎn)單的同步函數(shù):

function add(a, b) {
  return a + b;
}

上面的例子成為直接編程風(fēng)格,其實(shí)沒(méi)什么特別的,就是使用return語(yǔ)句把結(jié)果直接傳遞給調(diào)用者。它代表的是同步編程中返回結(jié)果的最常見(jiàn)方法。上述功能的CPS寫(xiě)法如下:

function add(a, b, callback) {
  callback(a + b);
}

add()函數(shù)是一個(gè)同步的CPS函數(shù),CPS函數(shù)只會(huì)在它調(diào)用的時(shí)候才會(huì)拿到add()函數(shù)的執(zhí)行結(jié)果,下列代碼就是其調(diào)用方式:

console.log("before");
add(1, 2, result => console.log("Result: " + result));
console.log("after");

既然add()是同步的,那么上述代碼會(huì)打印以下結(jié)果:

before
Result: 3
after
異步CPS

那我們思考下面的這個(gè)例子,這里的add()函數(shù)是異步的:

function additionAsync(a, b, callback) {
 setTimeout(() => callback(a + b), 100);
}

在上邊的代碼中,我們使用setTimeout()模擬異步回調(diào)函數(shù)的調(diào)用?,F(xiàn)在,我們調(diào)用additionalAsync,并查看具體的輸出結(jié)果。

console.log("before");
additionAsync(1, 2, result => console.log("Result: " + result));
console.log("after");

上述代碼會(huì)有以下的輸出結(jié)果:

before
after
Result: 3

因?yàn)?b>setTimeout()是一個(gè)異步操作,所以它不會(huì)等待執(zhí)行回調(diào),而是立即返回,將控制權(quán)交給addAsync(),然后返回給其調(diào)用者。Node.js中的此屬性至關(guān)重要,因?yàn)橹灰挟惒秸?qǐng)求產(chǎn)生,控制權(quán)就會(huì)交給事件循環(huán),從而允許處理來(lái)自隊(duì)列的新事件。

下面的圖片顯示了Node.js中事件循環(huán)過(guò)程:

當(dāng)異步操作完成時(shí),執(zhí)行權(quán)就會(huì)交給這個(gè)異步操作開(kāi)始的地方,即回調(diào)函數(shù)。執(zhí)行將從事件循環(huán)開(kāi)始,所以它將有一個(gè)新的堆棧。對(duì)于JavaScript而言,這是它的優(yōu)勢(shì)所在。正是由于閉包保存了其上下文環(huán)境,即使在不同的時(shí)間點(diǎn)和不同的位置調(diào)用回調(diào),也能夠正常地執(zhí)行。

同步函數(shù)在其完成操作之前是阻塞的。而異步函數(shù)立即返回,結(jié)果將在事件循環(huán)的稍后循環(huán)中傳遞給處理程序(在我們的例子中是一個(gè)回調(diào))。

非CPS風(fēng)格的回調(diào)模式

某些情況下情況下,我們可能會(huì)認(rèn)為回調(diào)CPS式的寫(xiě)法像是異步的,然而并不是。比如以下代碼,Array對(duì)象的map()方法:

const result = [1, 5, 7].map(element => element - 1);
console.log(result); // [0, 4, 6]

在上述例子中,回調(diào)僅用于迭代數(shù)組的元素,而不是傳遞操作的結(jié)果。實(shí)際上,這個(gè)例子中是使用回調(diào)的方式同步返回,而非傳遞結(jié)果。是否是傳遞操作結(jié)果的回調(diào)通常在API文檔有明確說(shuō)明。

同步還是異步?

我們已經(jīng)看到代碼的執(zhí)行順序會(huì)因同步或異步的執(zhí)行方式產(chǎn)生根本性的改變。這對(duì)整個(gè)應(yīng)用程序的流程,正確性和效率都產(chǎn)生了重大影響。以下是對(duì)這兩種模式及其缺陷的分析。一般來(lái)說(shuō),必須避免的是由于其執(zhí)行順序不一致導(dǎo)致的難以檢測(cè)和拓展的混亂。下面是一個(gè)有陷阱的異步實(shí)例:

一個(gè)有問(wèn)題的函數(shù)

最危險(xiǎn)的情況之一是在特定條件下同步執(zhí)行本應(yīng)異步執(zhí)行的API。以下列代碼為例:

const fs = require("fs");
const cache = {};

function inconsistentRead(filename, callback) {
  if (cache[filename]) {
    // 如果緩存命中,則同步執(zhí)行回調(diào)
    callback(cache[filename]);
  } else {
    // 未命中,則執(zhí)行異步非阻塞的I/O操作
    fs.readFile(filename, "utf8", (err, data) => {
      cache[filename] = data;
      callback(data);
    });
  }
}

上述功能使用緩存來(lái)存儲(chǔ)不同文件讀取操作的結(jié)果。不過(guò)記得,這只是一個(gè)例子,它缺少錯(cuò)誤處理,并且其緩存邏輯本身不是最佳的(比如沒(méi)有緩存淘汰策略)。除此之外,上述函數(shù)是非常危險(xiǎn)的,因?yàn)槿绻麤](méi)有設(shè)置高速緩存,它的行為是異步的,直到fs.readFile()函數(shù)返回結(jié)果為止,它都不會(huì)同步執(zhí)行,這時(shí)緩存并不會(huì)觸發(fā),而會(huì)去走異步回調(diào)調(diào)用。

解放zalgo

關(guān)于zalgo,其實(shí)就是指同步或異步行為的不確定性,幾乎總是導(dǎo)致非常難追蹤的bug。

現(xiàn)在,我們來(lái)看看如何使用一個(gè)不可預(yù)測(cè)其順序的函數(shù),它甚至可以輕松地中斷一個(gè)應(yīng)用程序??匆韵麓a:

function createFileReader(filename) {
  const listeners = [];
  inconsistentRead(filename, value => {
    listeners.forEach(listener => listener(value));
  });
  return {
    onDataReady: listener => listeners.push(listener)
  };
}

當(dāng)上述函數(shù)被調(diào)用時(shí),它創(chuàng)建一個(gè)充當(dāng)事件發(fā)布器的新對(duì)象,允許我們?yōu)槲募x取操作設(shè)置多個(gè)事件監(jiān)聽(tīng)器。當(dāng)讀取操作完成并且數(shù)據(jù)可用時(shí),所有的監(jiān)聽(tīng)器將被立即被調(diào)用。前面的函數(shù)使用之前定義的inconsistentRead()函數(shù)來(lái)實(shí)現(xiàn)這個(gè)功能。我們現(xiàn)在嘗試調(diào)用createFileReader()函數(shù):

const reader1 = createFileReader("data.txt");
reader1.onDataReady(data => {
 console.log("First call data: " + data);
 // 之后再次通過(guò)fs讀取同一個(gè)文件
 const reader2 = createFileReader("data.txt");
 reader2.onDataReady(data => {
   console.log("Second call data: " + data);
 });
});

之后的輸出是這樣的:

First call data: some data

下面來(lái)分析為何第二次的回調(diào)沒(méi)有被調(diào)用:

在創(chuàng)建reader1的時(shí)候,inconsistentRead()函數(shù)是異步執(zhí)行的,這時(shí)沒(méi)有可用的緩存結(jié)果,因此我們有時(shí)間注冊(cè)事件監(jiān)聽(tīng)器。在讀操作完成后,它將在下一次事件循環(huán)中被調(diào)用。

然后,在事件循環(huán)的循環(huán)中創(chuàng)建reader2,其中所請(qǐng)求文件的緩存已經(jīng)存在。在這種情況下,內(nèi)部調(diào)用inconsistentRead()將是同步的。所以,它的回調(diào)將被立即調(diào)用,這意味著reader2的所有監(jiān)聽(tīng)器也將被同步調(diào)用。然而,在創(chuàng)建reader2之后,我們才開(kāi)始注冊(cè)監(jiān)聽(tīng)器,所以它們將永遠(yuǎn)不被調(diào)用。

inconsistentRead()回調(diào)函數(shù)的行為是不可預(yù)測(cè)的,因?yàn)樗Q于許多因素,例如調(diào)用的頻率,作為參數(shù)傳遞的文件名,以及加載文件所花費(fèi)的時(shí)間等。

在實(shí)際應(yīng)用中,例如我們剛剛看到的錯(cuò)誤可能會(huì)非常復(fù)雜,難以在真實(shí)應(yīng)用程序中識(shí)別和復(fù)制。想象一下,在Web服務(wù)器中使用類(lèi)似的功能,可以有多個(gè)并發(fā)請(qǐng)求;想象一下這些請(qǐng)求掛起,沒(méi)有任何明顯的理由,沒(méi)有任何日志被記錄。這絕對(duì)屬于煩人的bug。

npm的創(chuàng)始人和以前的Node.js項(xiàng)目負(fù)責(zé)人Isaac Z. Schlueter在他的一篇博客文章中比較了使用這種不可預(yù)測(cè)的功能來(lái)釋放Zalgo。如果您不熟悉Zalgo??梢钥纯碔saac Z. Schlueter的原始帖子。

使用同步API

從上述關(guān)于zalgo的示例中,我們知道,API必須清楚地定義其性質(zhì):是同步的還是異步的?

我們合適fix上述的inconsistentRead()函數(shù)產(chǎn)生的bug的方式是使它完全同步阻塞執(zhí)行。并且這是完全可能的,因?yàn)?b>Node.js為大多數(shù)基本I/O操作提供了一組同步方式的API。例如,我們可以使用fs.readFileSync()函數(shù)來(lái)代替它的異步對(duì)等體。代碼現(xiàn)在如下:

const fs = require("fs");
const cache = {};

function consistentReadSync(filename) {
 if (cache[filename]) {
   return cache[filename];
 } else {
   cache[filename] = fs.readFileSync(filename, "utf8");
   return cache[filename];
 }
}

我們可以看到整個(gè)函數(shù)被轉(zhuǎn)化為同步阻塞調(diào)用的模式。如果一個(gè)函數(shù)是同步的,那么它不會(huì)是CPS的風(fēng)格。事實(shí)上,我們可以說(shuō),使用CPS來(lái)實(shí)現(xiàn)一個(gè)同步的API一直是最佳實(shí)踐,這將消除其性質(zhì)上的任何混亂,并且從性能角度來(lái)看也將更加有效。

請(qǐng)記住,將APICPS更改為直接調(diào)用返回的風(fēng)格,或者說(shuō)從異步到同步的風(fēng)格。例如,在我們的例子中,我們必須完全改變我們的createFileReader()為同步,并使其適應(yīng)于始終工作。

另外,使用同步API而不是異步API,要特別注意以下注意事項(xiàng):

同步API并不適用于所有應(yīng)用場(chǎng)景。

同步API將阻塞事件循環(huán)并將并發(fā)請(qǐng)求置于阻塞狀態(tài)。它會(huì)破壞JavaScript的并發(fā)模型,甚至使得整個(gè)應(yīng)用程序的性能下降。我們將在本書(shū)后面看到這對(duì)我們的應(yīng)用程序的影響。

在我們的inconsistentRead()函數(shù)中,因?yàn)槊總€(gè)文件名僅調(diào)用一次,所以同步阻塞調(diào)用而對(duì)應(yīng)用程序造成的影響并不大,并且緩存值將用于所有后續(xù)的調(diào)用。如果我們的靜態(tài)文件的數(shù)量是有限的,那么使用consistentReadSync()將不會(huì)對(duì)我們的事件循環(huán)產(chǎn)生很大的影響。如果我們文件數(shù)量很大并且都需要被讀取一次,而且對(duì)性能要求較高的情況下,我們不建議在Node.js中使用同步I/O。然而,在某些情況下,同步I/O可能是最簡(jiǎn)單和最有效的解決方案。所以我們必須正確評(píng)估具體的應(yīng)用場(chǎng)景,以選擇最為合適的方案。上述實(shí)例其實(shí)說(shuō)明:在實(shí)際應(yīng)用程序中使用同步阻塞API加載配置文件是非常有意義的。

因此,記得只有不影響應(yīng)用程序并發(fā)能力時(shí)才考慮使用同步阻塞I/O

延時(shí)處理

另一種fix上述的inconsistentRead()函數(shù)產(chǎn)生的bug的方式是讓它僅僅是異步的。這里的解決辦法是下一次事件循環(huán)時(shí)同步調(diào)用,而不是在相同的事件循環(huán)周期中立即運(yùn)行,使得其實(shí)際上是異步的。在Node.js中,可以使用process.nextTick(),它延遲函數(shù)的執(zhí)行,直到下一次傳遞事件循環(huán)。它的功能非常簡(jiǎn)單,它將回調(diào)作為參數(shù),并將其推送到事件隊(duì)列的頂部,在任何未處理的I/O事件前,并立即返回。一旦事件循環(huán)再次運(yùn)行,就會(huì)立刻調(diào)用回調(diào)。

所以看下列代碼,我們可以較好的利用這項(xiàng)技術(shù)處理inconsistentRead()的異步順序:

const fs = require("fs");
const cache = {};

function consistentReadAsync(filename, callback) {
  if (cache[filename]) {
    // 下一次事件循環(huán)立即調(diào)用
    process.nextTick(() => callback(cache[filename]));
  } else {
    // 異步I/O操作
    fs.readFile(filename, "utf8", (err, data) => {
      cache[filename] = data;
      callback(data);
    });
  }
}

現(xiàn)在,上述函數(shù)保證在任何情況下異步地調(diào)用其回調(diào)函數(shù),解決了上述bug

另一個(gè)用于延遲執(zhí)行代碼的APIsetImmediate()。雖然它們的作用看起來(lái)非常相似,但實(shí)際含義卻截然不同。process.nextTick()的回調(diào)函數(shù)會(huì)在任何其他I/O操作之前調(diào)用,而對(duì)于setImmediate()則會(huì)在其它I/O操作之后調(diào)用。由于process.nextTick()在其它的I/O之前調(diào)用,因此在某些情況下可能會(huì)導(dǎo)致I/O進(jìn)入無(wú)限期等待,例如遞歸調(diào)用process.nextTick()但是對(duì)于setImmediate()則不會(huì)發(fā)生這種情況。當(dāng)我們?cè)诒緯?shū)后面分析使用延遲調(diào)用來(lái)運(yùn)行同步CPU綁定任務(wù)時(shí),我們將深入了解這兩種API之間的區(qū)別。

我們保證通過(guò)使用process.nextTick()異步調(diào)用其回調(diào)函數(shù)。

Node.js回調(diào)風(fēng)格

對(duì)于Node.js而言,CPS風(fēng)格的API和回調(diào)函數(shù)遵循一組特殊的約定。這些約定不只是適用于Node.js核心API,對(duì)于它們之后也是絕大多數(shù)用戶(hù)級(jí)模塊和應(yīng)用程序也很有意義。因此,我們了解這些風(fēng)格,并確保我們?cè)谛枰O(shè)計(jì)異步API時(shí)遵守規(guī)定顯得至關(guān)重要。

回調(diào)總是最后一個(gè)參數(shù)

在所有核心Node.js方法中,標(biāo)準(zhǔn)約定是當(dāng)函數(shù)在輸入中接受回調(diào)時(shí),必須作為最后一個(gè)參數(shù)傳遞。我們以下面的Node.js核心API為例:

fs.readFile(filename, [options], callback);

從前面的例子可以看出,即使是在可選參數(shù)存在的情況下,回調(diào)也始終置于最后的位置。其原因是在回調(diào)定義的情況下,函數(shù)調(diào)用更可讀。

錯(cuò)誤處理總在最前

CPS中,錯(cuò)誤以不同于正確結(jié)果的形式在回調(diào)函數(shù)中傳遞。在Node.js中,CPS風(fēng)格的回調(diào)函數(shù)產(chǎn)生的任何錯(cuò)誤總是作為回調(diào)的第一個(gè)參數(shù)傳遞,并且任何實(shí)際的結(jié)果從第二個(gè)參數(shù)開(kāi)始傳遞。如果操作成功,沒(méi)有錯(cuò)誤,第一個(gè)參數(shù)將為nullundefined。看下列代碼:

fs.readFile("foo.txt", "utf8", (err, data) => {
  if (err)
    handleError(err);
  else
    processData(data);
});

上面的例子是最好的檢測(cè)錯(cuò)誤的方法,如果不檢測(cè)錯(cuò)誤,我們可能難以發(fā)現(xiàn)和調(diào)試代碼中的bug,但另外一個(gè)要考慮的問(wèn)題是錯(cuò)誤總是為Error類(lèi)型,這意味著簡(jiǎn)單的字符串或數(shù)字不應(yīng)該作為錯(cuò)誤對(duì)象傳遞(難以被try catch代碼塊捕獲)。

錯(cuò)誤傳播

對(duì)于同步阻塞的寫(xiě)法而言,我們的錯(cuò)誤都是通過(guò)throw語(yǔ)句拋出,即使錯(cuò)誤在錯(cuò)誤棧中跳轉(zhuǎn),我們也能很好地捕獲到錯(cuò)誤上下文。

但是對(duì)于CPS風(fēng)格的異步調(diào)用而言,通過(guò)把錯(cuò)誤傳遞到錯(cuò)誤棧中的下一個(gè)回調(diào)來(lái)完成,下面是一個(gè)典型的例子:

const fs = require("fs");

function readJSON(filename, callback) {
  fs.readFile(filename, "utf8", (err, data) => {
    let parsed;
    if (err)
    // 如果有錯(cuò)誤產(chǎn)生則退出當(dāng)前調(diào)用
      return callback(err);
    try {
      // 解析文件中的數(shù)據(jù)
      parsed = JSON.parse(data);
    } catch (err) {
      // 捕獲解析中的錯(cuò)誤,如果有錯(cuò)誤產(chǎn)生,則進(jìn)行錯(cuò)誤處理
      return callback(err);
    }
    // 沒(méi)有錯(cuò)誤,調(diào)用回調(diào)
    callback(null, parsed);
  });
};

從上面的例子中我們注意到的細(xì)節(jié)是當(dāng)我們想要正確地進(jìn)行異常處理時(shí),我們?nèi)绾蜗?b>callback傳遞參數(shù)。此外,當(dāng)有錯(cuò)誤產(chǎn)生時(shí),我們使用了return語(yǔ)句,立即退出當(dāng)前函數(shù)調(diào)用,避免進(jìn)行下面的相關(guān)執(zhí)行。

不可捕獲的異常

從上述readJSON()函數(shù),為了避免將任何異常拋到fs.readFile()的回調(diào)函數(shù)中捕獲,我們對(duì)JSON.parse()周?chē)胖靡粋€(gè)try catch代碼塊。在異步回調(diào)中一旦出錯(cuò),將拋出異常,并跳轉(zhuǎn)到事件循環(huán),不把錯(cuò)誤傳播到下一個(gè)回調(diào)函數(shù)去。

Node.js中,這是一個(gè)不可恢復(fù)的狀態(tài),應(yīng)用程序會(huì)關(guān)閉,并將錯(cuò)誤打印到標(biāo)準(zhǔn)輸出中。為了證明這一點(diǎn),我們嘗試從之前定義的readJSON()函數(shù)中刪除try catch代碼塊:

const fs = require("fs");

function readJSONThrows(filename, callback) {
  fs.readFile(filename, "utf8", (err, data) => {
    if (err) {
      return callback(err);
    }
    // 假設(shè)parse的執(zhí)行沒(méi)有錯(cuò)誤
    callback(null, JSON.parse(data));
  });
};

在上面的代碼中,我們沒(méi)有辦法捕獲到JSON.parse產(chǎn)生的異常,如果我們嘗試傳遞一個(gè)非標(biāo)準(zhǔn)JSON格式的文件,將會(huì)拋出以下錯(cuò)誤:

SyntaxError: Unexpected token d
at Object.parse (native)
at [...]
at fs.js:266:14
at Object.oncomplete (fs.js:107:15)

現(xiàn)在,如果我們看看前面的錯(cuò)誤棧跟蹤,我們將看到它從fs模塊的某處開(kāi)始,恰好從本地API完成文件讀取返回到fs.readFile()函數(shù),通過(guò)事件循環(huán)。這些信息都很清楚地顯示給我們,異常從我們的回調(diào)傳入堆棧,然后直接進(jìn)入事件循環(huán),最終被捕獲并拋出到控制臺(tái)中。
這也意味著使用try catch代碼塊包裝對(duì)readJSONThrows()的調(diào)用將不起作用,因?yàn)閴K所在的堆棧與調(diào)用回調(diào)的堆棧不同。以下代碼顯示了我們剛才描述的相反的情況:

try {
  readJSONThrows("nonJSON.txt", function(err, result) {
    // ... 
  });
} catch (err) {
  console.log("This will not catch the JSON parsing exception");
}

前面的catch語(yǔ)句將永遠(yuǎn)不會(huì)收到JSON解析異常,因?yàn)樗鼘⒎祷氐綊伋霎惓5亩褩?。我們剛剛看到堆棧在事件循環(huán)中結(jié)束,而不是觸發(fā)異步操作的功能。
如前所述,應(yīng)用程序在異常到達(dá)事件循環(huán)的那一刻中止,然而,我們?nèi)匀挥袡C(jī)會(huì)在應(yīng)用程序終止之前執(zhí)行一些清理或日志記錄。事實(shí)上,當(dāng)這種情況發(fā)生時(shí),Node.js會(huì)在退出進(jìn)程之前發(fā)出一個(gè)名為uncaughtException的特殊事件。以下代碼顯示了一個(gè)示例用例:

process.on("uncaughtException", (err) => {
  console.error("This will catch at last the " +
    "JSON parsing exception: " + err.message);
  // Terminates the application with 1 (error) as exit code:
  // without the following line, the application would continue
  process.exit(1);
});

重要的是,未被捕獲的異常會(huì)使應(yīng)用程序處于不能保證一致的狀態(tài),這可能導(dǎo)致不可預(yù)見(jiàn)的問(wèn)題。例如,可能還有不完整的I/O請(qǐng)求運(yùn)行或關(guān)閉可能會(huì)變得不一致。這就是為什么總是建議,特別是在生產(chǎn)環(huán)境中,在接收到未被捕獲的異常之后寫(xiě)上述代碼進(jìn)行錯(cuò)誤日志記錄。

模塊系統(tǒng)及相關(guān)模式

模塊不僅是構(gòu)建大型應(yīng)用的基礎(chǔ),其主要機(jī)制是封裝內(nèi)部實(shí)現(xiàn)、方法與變量,通過(guò)接口。在本節(jié)中,我們將介紹Node.js的模塊系統(tǒng)及其最常見(jiàn)的使用模式。

關(guān)于模塊

JavaScript的主要問(wèn)題之一是沒(méi)有命名空間。在全局范圍內(nèi)運(yùn)行的程序會(huì)污染全局命名空間,造成相關(guān)變量、數(shù)據(jù)、方法名的沖突。解決這個(gè)問(wèn)題的技術(shù)稱(chēng)為模塊模式,看下列代碼:

const module = (() => {
  const privateFoo = () => {
    // ...
  };
  const privateBar = [];
  const exported = {
    publicFoo: () => {
      // ...
    },
    publicBar: () => {
      // ...
    }
  };
  return exported;
})();
console.log(module);

此模式利用自執(zhí)行匿名函數(shù)實(shí)現(xiàn)模塊,僅導(dǎo)出旨希望被公開(kāi)調(diào)用的部分。在上面的代碼中,模塊變量只包含導(dǎo)出的API,而其余的模塊內(nèi)容實(shí)際上從外部訪(fǎng)問(wèn)不到。我們將在稍后看到,這種模式背后的想法被用作Node.js模塊系統(tǒng)的基礎(chǔ)。

Node.js模塊相關(guān)解釋

CommonJS是一個(gè)旨在規(guī)范JavaScript生態(tài)系統(tǒng)的組織,他們提出了CommonJS模塊規(guī)范。 Node.js在此規(guī)范之上構(gòu)建了其模塊系統(tǒng),并添加了一些自定義的擴(kuò)展。為了描述它的工作原理,我們可以通過(guò)這樣一個(gè)例子解釋模塊模式,每個(gè)模塊都在私有命名空間下運(yùn)行,這樣模塊內(nèi)定義的每個(gè)變量都不會(huì)污染全局命名空間。

自定義模塊系統(tǒng)

為了解釋模塊系統(tǒng)的遠(yuǎn)離,讓我們從頭開(kāi)始構(gòu)建一個(gè)類(lèi)似的模塊系統(tǒng)。下面的代碼創(chuàng)建一個(gè)模仿Node.js原始require()函數(shù)的功能。

我們先創(chuàng)建一個(gè)加載模塊內(nèi)容的函數(shù),將其包裝到一個(gè)私有的命名空間內(nèi):

function loadModule(filename, module, require) {
  const wrappedSrc = `(function(module, exports, require) {
         ${fs.readFileSync(filename, "utf8")}
       })(module, module.exports, require);`;
  eval(wrappedSrc);
}

模塊的源代碼被包裝到一個(gè)函數(shù)中,如同自執(zhí)行匿名函數(shù)那樣。這里的區(qū)別在于,我們將一些固有的變量傳遞給模塊,特指moduleexportsrequire。注意導(dǎo)出模塊的參數(shù)是module.exportsexports,后面我們將再討論。

請(qǐng)記住,這只是一個(gè)例子,在真實(shí)項(xiàng)目中可不要這么做。諸如eval()或vm模塊有可能導(dǎo)致一些安全性的問(wèn)題,它人可能利用漏洞來(lái)進(jìn)行注入攻擊。我們應(yīng)該非常小心地使用甚至完全避免使用eval。

我們現(xiàn)在來(lái)看模塊的接口、變量等是如何被require()函數(shù)引入的:

const require = (moduleName) => {
  console.log(`Require invoked for module: ${moduleName}`);
  const id = require.resolve(moduleName);
  // 是否命中緩存
  if (require.cache[id]) {
    return require.cache[id].exports;
  }
  // 定義module
  const module = {
    exports: {},
    id: id
  };
  // 新模塊引入,存入緩存
  require.cache[id] = module;
  // 加載模塊
  loadModule(id, module, require);
  // 返回導(dǎo)出的變量
  return module.exports;
};
require.cache = {};
require.resolve = (moduleName) => {
  /* 通過(guò)模塊名作為參數(shù)resolve一個(gè)完整的模塊 */
};

上面的函數(shù)模擬了用于加載模塊的原生Node.jsrequire()函數(shù)的行為。當(dāng)然,這只是一個(gè)demo,它并不能準(zhǔn)確且完整地反映require()函數(shù)的真實(shí)行為,但是為了更好地理解Node.js模塊系統(tǒng)的內(nèi)部實(shí)現(xiàn),定義模塊和加載模塊。我們的自制模塊系統(tǒng)的功能如下:

模塊名稱(chēng)被作為參數(shù)傳入,我們首先做的是找尋模塊的完整路徑,我們稱(chēng)之為idrequire.resolve()專(zhuān)門(mén)負(fù)責(zé)這項(xiàng)功能,它通過(guò)一個(gè)特定的解析算法實(shí)現(xiàn)相關(guān)功能(稍后將討論)。

如果模塊已經(jīng)被加載,它應(yīng)該存在于緩存。在這種情況下,我們立即返回緩存中的模塊。

如果模塊尚未加載,我們將首次加載該模塊。創(chuàng)建一個(gè)模塊對(duì)象,其中包含一個(gè)使用空對(duì)象字面值初始化的exports屬性。該屬性將被模塊的代碼用于導(dǎo)出該模塊的公共API。

緩存首次加載的模塊對(duì)象。

模塊源代碼從其文件中讀取,代碼被導(dǎo)入,如前所述。我們通過(guò)require()函數(shù)向模塊提供我們剛剛創(chuàng)建的模塊對(duì)象。該模塊通過(guò)操作或替換module.exports對(duì)象來(lái)導(dǎo)出其公共API。

最后,將代表模塊的公共APImodule.exports的內(nèi)容返回給調(diào)用者。

正如我們所看到的,Node.js模塊系統(tǒng)的原理并不是想象中那么高深,只不過(guò)是通過(guò)我們一系列操作來(lái)創(chuàng)建和導(dǎo)入導(dǎo)出模塊源代碼。

定義一個(gè)模塊

通過(guò)查看我們的自定義require()函數(shù)的工作原理,我們現(xiàn)在既然已經(jīng)知道如何定義一個(gè)模塊。再來(lái)看下面這個(gè)例子:

// 加載另一個(gè)模塊
const dependency = require("./anotherModule");
// 模塊內(nèi)的私有函數(shù)
function log() {
  console.log(`Well done ${dependency.username}`);
}
// 通過(guò)導(dǎo)出API實(shí)現(xiàn)共有方法
module.exports.run = () => {
  log();
};

需要注意的是模塊內(nèi)的所有內(nèi)容都是私有的,除非它被分配給module.exports變量。然后,當(dāng)使用require()加載模塊時(shí),緩存并返回此變量的內(nèi)容。

定義全局變量

即使在模塊中聲明的所有變量和函數(shù)都在其本地范圍內(nèi)定義,仍然可以定義全局變量。事實(shí)上,模塊系統(tǒng)公開(kāi)了一個(gè)名為global的特殊變量。分配給此變量的所有內(nèi)容將會(huì)被定義到全局環(huán)境下。

注意:污染全局命名空間是不好的,并且沒(méi)有充分運(yùn)用模塊系統(tǒng)的優(yōu)勢(shì)。所以,只有真的需要使用全局變量,才去使用它。

module.exports和exports

對(duì)于許多還不熟悉Node.js的開(kāi)發(fā)人員而言,他們最容易混淆的是exportsmodule.exports來(lái)導(dǎo)出公共API的區(qū)別。變量export只是對(duì)module.exports的初始值的引用;我們已經(jīng)看到,exports本質(zhì)上在模塊加載之前只是一個(gè)簡(jiǎn)單的對(duì)象。

這意味著我們只能將新屬性附加到導(dǎo)出變量引用的對(duì)象,如以下代碼所示:

exports.hello = () => {
  console.log("Hello");
}

重新給exports賦值并不會(huì)有任何影響,因?yàn)樗⒉粫?huì)因此而改變module.exports的內(nèi)容,它只是改變了該變量本身。因此下列代碼是錯(cuò)誤的:

exports = () => {
  console.log("Hello");
}

如果我們想要導(dǎo)出除對(duì)象之外的內(nèi)容,比如函數(shù),我們可以給module.exports重新賦值:

module.exports = () => {
  console.log("Hello");
}
require函數(shù)是同步的

另一個(gè)重要的細(xì)節(jié)是上述我們寫(xiě)的require()函數(shù)是同步的,它使用了一個(gè)較為簡(jiǎn)單的方式返回了模塊內(nèi)容,并且不需要回調(diào)函數(shù)。因此,對(duì)于module.exports也是同步的,例如,下列的代碼是不正確的:

setTimeout(() => {
  module.exports = function() {
    // ...
  };
}, 100);

通過(guò)這種方式導(dǎo)出模塊會(huì)對(duì)我們定義模塊產(chǎn)生重要的影響,因?yàn)樗拗屏宋覀兺蕉x并使用模塊的方式。這實(shí)際上是為什么核心Node.js庫(kù)提供同步API以代替異步API的最重要的原因之一。

如果我們需要定義一個(gè)需要異步操作來(lái)進(jìn)行初始化的模塊,我們也可以隨時(shí)定義和導(dǎo)出需要我們異步初始化的模塊。但是這樣定義異步模塊我們并不能保證require()后可以立即使用,在第九章,我們將詳細(xì)分析這個(gè)問(wèn)題,并提出一些模式來(lái)優(yōu)化解決這個(gè)問(wèn)題。

實(shí)際上,在早期的Node.js中,曾經(jīng)有一個(gè)異步版本的require(),但由于它對(duì)初始化時(shí)間和異步I/O的性能有巨大影響,很快這個(gè)API就被刪除了。

resolve算法

依賴(lài)地獄描述了軟件的依賴(lài)于不同版本的軟件包的依賴(lài)關(guān)系,Node.js通過(guò)加載不同版本的模塊來(lái)解決這個(gè)問(wèn)題,具體取決于模塊的加載位置。而都是由npm來(lái)完成的,相關(guān)算法被稱(chēng)作resolve算法,被用到require()函數(shù)中。

現(xiàn)在讓我們快速概述一下這個(gè)算法。如下所述,resolve()函數(shù)將一個(gè)模塊名稱(chēng)(moduleName)作為輸入,并返回模塊的完整路徑。然后,該路徑用于加載其代碼,并且還可以唯一地標(biāo)識(shí)模塊。resolve算法可以分為以下三種規(guī)則:

文件模塊:如果moduleName/開(kāi)頭,那么它已經(jīng)被認(rèn)為是模塊的絕對(duì)路徑。如果以./開(kāi)頭,那么moduleName被認(rèn)為是相對(duì)路徑,它是從使用require的模塊的位置開(kāi)始計(jì)算的。

核心模塊:如果moduleName不以/./開(kāi)頭,則算法將首先嘗試在核心Node.js模塊中進(jìn)行搜索。

模塊包:如果沒(méi)有找到匹配moduleName的核心模塊,則搜索在當(dāng)前目錄下的node_modules,如果沒(méi)有搜索到node_modules,則會(huì)往上層目錄繼續(xù)搜索node_modules,直到它到達(dá)文件系統(tǒng)的根目錄。

對(duì)于文件和包模塊,單個(gè)文件和目錄也可以匹配到moduleName。特別地,算法將嘗試匹配以下內(nèi)容:

.js

/index.js

/package.jsonmain值下聲明的文件或目錄

resolve算法的具體文檔

node_modules目錄實(shí)際上是npm安裝每個(gè)包并存放相關(guān)依賴(lài)關(guān)系的地方。這意味著,基于我們剛剛描述的算法,每個(gè)包都有自身的私有依賴(lài)關(guān)系。例如,看以下目錄結(jié)構(gòu):

myApp
├── foo.js
└── node_modules
    ├── depA
    │   └── index.js
    └── depB
        │
        ├── bar.js
        ├── node_modules
        ├── depA
        │    └── index.js
        └── depC
             ├── foobar.js
             └── node_modules
                 └── depA
                     └── index.js

在前面的例子中,myAppdepBdepC都依賴(lài)于depA;然而,他們都有自己的私有依賴(lài)的版本!按照解析算法的規(guī)則,使用require("depA")將根據(jù)需要的模塊加載不同的文件,如下:

/myApp/foo.js中調(diào)用的require("depA")會(huì)加載/myApp/node_modules/depA/index.js

/myApp/node_modules/depB/bar.js中調(diào)用的require("depA")會(huì)加載/myApp/node_modules/depB/node_modules/depA/index.js

/myApp/node_modules/depC/foobar.js中調(diào)用的require("depA")會(huì)加載/myApp/node_modules/depC/node_modules/depA/index.js

resolve算法Node.js依賴(lài)關(guān)系管理的核心部分,它的存在使得即便應(yīng)用程序擁有成百上千包的情況下也不會(huì)出現(xiàn)沖突和版本不兼容的問(wèn)題。

當(dāng)我們調(diào)用require()時(shí),解析算法對(duì)我們是透明的。然而,仍然可以通過(guò)調(diào)用require.resolve()直接由任何模塊使用。

模塊緩存

每個(gè)模塊只會(huì)在它第一次引入的時(shí)候加載,此后的任意一次require()調(diào)用均從之前緩存的版本中取得。通過(guò)查看我們之前寫(xiě)的自定義的require()函數(shù),可以看到緩存對(duì)于性能提升至關(guān)重要,此外也具有一些其它的優(yōu)勢(shì),如下:

使得模塊依賴(lài)關(guān)系的重復(fù)利用成為可能

從某種程度上保證了在從給定的包中要求相同的模塊時(shí)總是返回相同的實(shí)例,避免了沖突

模塊緩存通過(guò)require.cache變量查看,因此如果需要,可以直接訪(fǎng)問(wèn)它。在實(shí)際運(yùn)用中的例子是通過(guò)刪除require.cache變量中的相對(duì)鍵來(lái)使某個(gè)緩存的模塊無(wú)效,這是在測(cè)試過(guò)程中非常有用,但在正常情況下會(huì)十分危險(xiǎn)。

循環(huán)依賴(lài)

許多人認(rèn)為循環(huán)依賴(lài)是Node.js內(nèi)在的設(shè)計(jì)問(wèn)題,但在真實(shí)項(xiàng)目中真的可能發(fā)生,所以我們至少知道如何在Node.js中使得循環(huán)依賴(lài)有效。再來(lái)看我們自定義的require()函數(shù),我們可以立即看到其工作原理和注意事項(xiàng)。

看下面這兩個(gè)模塊:

模塊a.js

exports.loaded = false;
const b = require("./b");
module.exports = {
  bWasLoaded: b.loaded,
  loaded: true
};

模塊b.js

exports.loaded = false;
const a = require("./a");
module.exports = {
  aWasLoaded: a.loaded,
  loaded: true
};

然后我們?cè)?b>main.js中寫(xiě)以下代碼:

const a = require("./a");
const b = require("./b");
console.log(a);
console.log(b);

執(zhí)行上述代碼,會(huì)打印以下結(jié)果:

{
  bWasLoaded: true,
  loaded: true
}
{
  aWasLoaded: false,
  loaded: true
}

這個(gè)結(jié)果展現(xiàn)了循環(huán)依賴(lài)的處理順序。雖然a.jsb.js這兩個(gè)模塊都在主模塊需要的時(shí)候完全初始化,但是當(dāng)從b.js加載時(shí),a.js模塊是不完整的。特別,這種狀態(tài)會(huì)持續(xù)到b.js加載完畢的那一刻。這種情況我們應(yīng)該引起注意,特別要確認(rèn)我們?cè)?b>main.js中兩個(gè)模塊所需的順序。

這是由于模塊a.js將收到一個(gè)不完整的版本的b.js。我們現(xiàn)在明白,如果我們失去了首先加載哪個(gè)模塊的控制,如果項(xiàng)目足夠大,這可能會(huì)很容易發(fā)生循環(huán)依賴(lài)。

關(guān)于循環(huán)引用的文檔

簡(jiǎn)單說(shuō)就是,為了防止模塊載入的死循環(huán),Node.js在模塊第一次載入后會(huì)把它的結(jié)果進(jìn)行緩存,下一次再對(duì)它進(jìn)行載入的時(shí)候會(huì)直接從緩存中取出結(jié)果。所以在這種循環(huán)依賴(lài)情形下,不會(huì)有死循環(huán),但是卻會(huì)因?yàn)榫彺嬖斐赡K沒(méi)有按照我們預(yù)想的那樣被導(dǎo)出(export,詳細(xì)的案例分析見(jiàn)下文)。

官網(wǎng)給出了三個(gè)模塊還不是循環(huán)依賴(lài)最簡(jiǎn)單的情形。實(shí)際上,兩個(gè)模塊就可以很清楚的表達(dá)出這種情況。根據(jù)遞歸的思想,解決了最簡(jiǎn)單的情形,這一類(lèi)任意大小規(guī)模的問(wèn)題也就解決了一半(另一半還需要探明隨著問(wèn)題規(guī)模增長(zhǎng),問(wèn)題的解將會(huì)如何變化)。

JavaScript作為一門(mén)解釋型的語(yǔ)言,上面的打印輸出清晰的展示出了程序運(yùn)行的軌跡。在這個(gè)例子中,a.js首先requireb.js, 程序進(jìn)入b.js,在b.js中第一行又requirea.js。

如前文所述,為了避免無(wú)限循環(huán)的模塊依賴(lài),在Node.js運(yùn)行a.js 之后,它就被緩存了,但需要注意的是,此時(shí)緩存的僅僅是一個(gè)未完工的a.jsan unfinished copy of the a.js)。所以在 b.jsrequirea.js時(shí),得到的僅僅是緩存中一個(gè)未完工的a.js,具體來(lái)說(shuō),它并沒(méi)有明確被導(dǎo)出的具體內(nèi)容(a.js尾端)。所以b.js中輸出的a是一個(gè)空對(duì)象。

之后,b.js順利執(zhí)行完,回到a.jsrequire語(yǔ)句之后,繼續(xù)執(zhí)行完成。

模塊定義模式

模塊系統(tǒng)除了自帶處理依賴(lài)關(guān)系的機(jī)制之外,最常見(jiàn)的功能就是定義API。對(duì)于定義API,主要需要考慮私有和公共功能之間的平衡。其目的是最大化信息隱藏內(nèi)部實(shí)現(xiàn)和暴露的API可用性,同時(shí)將這些與可擴(kuò)展性和代碼重用性進(jìn)行平衡。

在本節(jié)中,我們將分析一些在Node.js中定義模塊的最流行模式;每個(gè)模塊都保證了私有變量的透明,可擴(kuò)展性和代碼重用。

命名導(dǎo)出

暴露公共API的最基本方法是使用命名導(dǎo)出,其中包括將我們想要公開(kāi)的所有值分配給由export(或module.exports)引用的對(duì)象的屬性。以這種方式,生成的導(dǎo)出對(duì)象將成為一組相關(guān)功能的容器或命名空間。

看下面代碼,是此模式的實(shí)現(xiàn):

//file logger.js
exports.info = (message) => {
  console.log("info: " + message);
};
exports.verbose = (message) => {
  console.log("verbose: " + message);
};

導(dǎo)出的函數(shù)隨后作為引入其的模塊的屬性使用,如下面的代碼所示:

// file main.js
const logger = require("./logger");
logger.info("This is an informational message");
logger.verbose("This is a verbose message");

大多數(shù)Node.js模塊使用這種定義。

CommonJS規(guī)范僅允許使用exports變量來(lái)公開(kāi)public成員。因此,命名的導(dǎo)出模式是唯一與CommonJS規(guī)范兼容的模式。使用module.exportsNode.js提供的一個(gè)擴(kuò)展,以支持更廣泛的模塊定義模式。

函數(shù)導(dǎo)出

最流行的模塊定義模式之一包括將整個(gè)module.exports變量重新分配給一個(gè)函數(shù)。它的主要優(yōu)點(diǎn)是它只暴露了一個(gè)函數(shù),為模塊提供了一個(gè)明確的入口點(diǎn),使其更易于理解和使用,它也很好地展現(xiàn)了單一職責(zé)原則。這種定義模塊的方法在社區(qū)中也被稱(chēng)為substack模式,在以下示例中查看此模式:

// file logger.js
module.exports = (message) => {
  console.log(`info: ${message}`);
};

該模式也可以將導(dǎo)出的函數(shù)用作其他公共API的命名空間。這是一個(gè)非常強(qiáng)大的組合,因?yàn)樗匀唤o模塊一個(gè)多帶帶的入口點(diǎn)(exports的主函數(shù))。這種方法還允許我們公開(kāi)具有次要或更高級(jí)用例的其他函數(shù)。以下代碼顯示了如何使用導(dǎo)出的函數(shù)作為命名空間來(lái)擴(kuò)展我們之前定義的模塊:

module.exports.verbose = (message) => {
  console.log(`verbose: ${message}`);
};

這段代碼演示了如何調(diào)用我們剛才定義的模塊:

// file main.js
const logger = require("./logger");
logger("This is an informational message");
logger.verbose("This is a verbose message");

雖然只是導(dǎo)出一個(gè)函數(shù)也可能是一個(gè)限制,但實(shí)際上它是一個(gè)完美的方式,把重點(diǎn)放在一個(gè)單一的函數(shù),它代表著這個(gè)模塊最重要的一個(gè)功能,同時(shí)使得內(nèi)部私有變量屬性更加透明,而只是暴露導(dǎo)出函數(shù)本身的屬性。

Node.js的模塊化鼓勵(lì)我們遵循采用單一職責(zé)原則(SRP):每個(gè)模塊應(yīng)該對(duì)單個(gè)功能負(fù)責(zé),該職責(zé)應(yīng)完全由該模塊封裝,以保證復(fù)用性。

注意,這里講的substack模式,就是通過(guò)僅導(dǎo)出一個(gè)函數(shù)來(lái)暴露模塊的主要功能。使用導(dǎo)出的函數(shù)作為命名空間來(lái)導(dǎo)出別的次要功能。

構(gòu)造器(類(lèi))導(dǎo)出

導(dǎo)出構(gòu)造函數(shù)的模塊是導(dǎo)出函數(shù)的模塊的特例。其不同之處在于,使用這種新模式,我們?cè)试S用戶(hù)使用構(gòu)造函數(shù)創(chuàng)建新的實(shí)例,但是我們也可以擴(kuò)展其原型并創(chuàng)建新類(lèi)(繼承)。以下是此模式的示例:

// file logger.js
function Logger(name) {
  this.name = name;
}
Logger.prototype.log = function(message) {
  console.log(`[${this.name}] ${message}`);
};
Logger.prototype.info = function(message) {
  this.log(`info: ${message}`);
};
Logger.prototype.verbose = function(message) {
  this.log(`verbose: ${message}`);
};
module.exports = Logger;

我們通過(guò)以下方式使用上述模塊:

// file main.js
const Logger = require("./logger");
const dbLogger = new Logger("DB");
dbLogger.info("This is an informational message");
const accessLogger = new Logger("ACCESS");
accessLogger.verbose("This is a verbose message");

通過(guò)ES2015class關(guān)鍵字語(yǔ)法也可以實(shí)現(xiàn)相同的模式:

class Logger {
  constructor(name) {
    this.name = name;
  }
  log(message) {
    console.log(`[${this.name}] ${message}`);
  }
  info(message) {
    this.log(`info: ${message}`);
  }
  verbose(message) {
    this.log(`verbose: ${message}`);
  }
}
module.exports = Logger;

鑒于ES2015的類(lèi)只是原型的語(yǔ)法糖,該模塊的使用將與其基于原型和構(gòu)造函數(shù)的方案完全相同。

導(dǎo)出構(gòu)造函數(shù)或類(lèi)仍然是模塊的單個(gè)入口點(diǎn),但與substack模式比起來(lái),它暴露了更多的模塊內(nèi)部結(jié)構(gòu)。然而,另一方面,當(dāng)想要擴(kuò)展該模塊功能時(shí),我們可以更加方便。

這種模式的變種包括對(duì)不使用new的調(diào)用。這個(gè)小技巧讓我們將我們的模塊用作工廠(chǎng)??聪铝写a:

function Logger(name) {
  if (!(this instanceof Logger)) {
    return new Logger(name);
  }
  this.name = name;
};

其實(shí)這很簡(jiǎn)單:我們檢查this是否存在,并且是Logger的一個(gè)實(shí)例。如果這些條件中的任何一個(gè)都為false,則意味著Logger()函數(shù)在不使用new的情況下被調(diào)用,然后繼續(xù)正確創(chuàng)建新實(shí)例并將其返回給調(diào)用者。這種技術(shù)允許我們將模塊也用作工廠(chǎng):

// file logger.js
const Logger = require("./logger");
const dbLogger = Logger("DB");
accessLogger.verbose("This is a verbose message");

ES2015new.target語(yǔ)法從Node.js 6開(kāi)始提供了一個(gè)更簡(jiǎn)潔的實(shí)現(xiàn)上述功能的方法。該利用公開(kāi)了new.target屬性,該屬性是所有函數(shù)中可用的元屬性,如果使用new關(guān)鍵字調(diào)用函數(shù),則在運(yùn)行時(shí)計(jì)算結(jié)果為true。
我們可以使用這種語(yǔ)法重寫(xiě)工廠(chǎng):

function Logger(name) {
  if (!new.target) {
    return new LoggerConstructor(name);
  }
  this.name = name;
}

這個(gè)代碼完全與前一段代碼作用相同,所以我們可以說(shuō)ES2015new.target語(yǔ)法糖使得代碼更加可讀和自然。

實(shí)例導(dǎo)出

我們可以利用require()的緩存機(jī)制來(lái)輕松地定義具有從構(gòu)造函數(shù)或工廠(chǎng)創(chuàng)建的狀態(tài)的有狀態(tài)實(shí)例,可以在不同模塊之間共享。以下代碼顯示了此模式的示例:

//file logger.js
function Logger(name) {
  this.count = 0;
  this.name = name;
}
Logger.prototype.log = function(message) {
  this.count++;
  console.log("[" + this.name + "] " + message);
};
module.exports = new Logger("DEFAULT");

這個(gè)新定義的模塊可以這么使用:

// file main.js
const logger = require("./logger");
logger.log("This is an informational message");

因?yàn)槟K被緩存,所以每個(gè)需要Logger模塊的模塊實(shí)際上總是會(huì)檢索該對(duì)象的相同實(shí)例,從而共享它的狀態(tài)。這種模式非常像創(chuàng)建單例。然而,它并不保證整個(gè)應(yīng)用程序的實(shí)例的唯一性,因?yàn)樗l(fā)生在傳統(tǒng)的單例模式中。在分析解析算法時(shí),實(shí)際上已經(jīng)看到,一個(gè)模塊可能會(huì)多次安裝在應(yīng)用程序的依賴(lài)關(guān)系樹(shù)中。這導(dǎo)致了同一邏輯模塊的多個(gè)實(shí)例,所有這些實(shí)例都運(yùn)行在同一個(gè)Node.js應(yīng)用程序的上下文中。在第7章中,我們將分析導(dǎo)出有狀態(tài)的實(shí)例和一些可替代的模式。

我們剛剛描述的模式的擴(kuò)展包括exports用于創(chuàng)建實(shí)例的構(gòu)造函數(shù)以及實(shí)例本身。這允許用戶(hù)創(chuàng)建相同對(duì)象的新實(shí)例,或者如果需要也可以擴(kuò)展它們。為了實(shí)現(xiàn)這一點(diǎn),我們只需要為實(shí)例分配一個(gè)新的屬性,如下面的代碼所示:

module.exports.Logger = Logger;

然后,我們可以使用導(dǎo)出的構(gòu)造函數(shù)創(chuàng)建類(lèi)的其他實(shí)例:

const customLogger = new logger.Logger("CUSTOM");
customLogger.log("This is an informational message");

從代碼可用性的角度來(lái)看,這類(lèi)似于將導(dǎo)出的函數(shù)用作命名空間,該模塊導(dǎo)出一個(gè)對(duì)象的默認(rèn)實(shí)例,這是我們大部分時(shí)間使用的功能,而更多的高級(jí)功能(如創(chuàng)建新實(shí)例或擴(kuò)展對(duì)象的功能)仍然可以通過(guò)較少的暴露屬性來(lái)使用。

修改其他模塊或全局作用域

一個(gè)模塊甚至可以導(dǎo)出任何東西這可以看起來(lái)有點(diǎn)不合適;但是,我們不應(yīng)該忘記一個(gè)模塊可以修改全局范圍和其中的任何對(duì)象,包括緩存中的其他模塊。請(qǐng)注意,這些通常被認(rèn)為是不好的做法,但是由于這種模式在某些情況下(例如測(cè)試)可能是有用和安全的,有時(shí)確實(shí)可以利用這一特性,這是值得了解和理解的。我們說(shuō)一個(gè)模塊可以修改全局范圍內(nèi)的其他模塊或?qū)ο蟆KǔJ侵冈谶\(yùn)行時(shí)修改現(xiàn)有對(duì)象以更改或擴(kuò)展其行為或應(yīng)用的臨時(shí)更改。

以下示例顯示了我們?nèi)绾蜗蛄硪粋€(gè)模塊添加新函數(shù):

// file patcher.js
// ./logger is another module
require("./logger").customMessage = () => console.log("This is a new functionality");

編寫(xiě)以下代碼:

// file main.js
require("./patcher");
const logger = require("./logger");
logger.customMessage();

在上述代碼中,必須首先引入patcher程序才能使用logger模塊。

上面的寫(xiě)法是很危險(xiǎn)的。主要考慮的是擁有修改全局命名空間或其他模塊的模塊是具有副作用的操作。換句話(huà)說(shuō),它會(huì)影響其范圍之外的實(shí)體的狀態(tài),這可能導(dǎo)致不可預(yù)測(cè)的后果,特別是當(dāng)多個(gè)模塊與相同的實(shí)體進(jìn)行交互時(shí)。想象一下,有兩個(gè)不同的模塊嘗試設(shè)置相同的全局變量,或者修改同一個(gè)模塊的相同屬性,效果可能是不可預(yù)測(cè)的(哪個(gè)模塊勝出?),但最重要的是它會(huì)對(duì)在整個(gè)應(yīng)用程序產(chǎn)生影響。

觀(guān)察者模式

Node.js中的另一個(gè)重要和基本的模式是觀(guān)察者模式。與reactor模式,回調(diào)模式和模塊一樣,觀(guān)察者模式是Node.js基礎(chǔ)之一,也是使用許多Node.js核心模塊和用戶(hù)定義模塊的基礎(chǔ)。

觀(guān)察者模式是對(duì)Node.js的數(shù)據(jù)響應(yīng)的理想解決方案,也是對(duì)回調(diào)的完美補(bǔ)充。我們給出以下定義:

發(fā)布者定義一個(gè)對(duì)象,它可以在其狀態(tài)發(fā)生變化時(shí)通知一組觀(guān)察者(或監(jiān)聽(tīng)者)。

與回調(diào)模式的主要區(qū)別在于,主體實(shí)際上可以通知多個(gè)觀(guān)察者,而傳統(tǒng)的CPS風(fēng)格的回調(diào)通常主體的結(jié)果只會(huì)傳播給一個(gè)監(jiān)聽(tīng)器。

EventEmitter類(lèi)

在傳統(tǒng)的面向?qū)ο缶幊讨?,觀(guān)察者模式需要接口,具體類(lèi)和層次結(jié)構(gòu)。在Node.js中,都變得簡(jiǎn)單得多。觀(guān)察者模式已經(jīng)內(nèi)置在核心模塊中,可以通過(guò)EventEmitter類(lèi)來(lái)實(shí)現(xiàn)。 EventEmitter類(lèi)允許我們注冊(cè)一個(gè)或多個(gè)函數(shù)作為監(jiān)聽(tīng)器,當(dāng)特定的事件類(lèi)型被觸發(fā)時(shí),它的回調(diào)將被調(diào)用,以通知其監(jiān)聽(tīng)器。以下圖像直觀(guān)地解釋了這個(gè)概念:

EventEmitter是一個(gè)類(lèi)(原型),它是從事件核心模塊導(dǎo)出的。以下代碼顯示了如何獲得對(duì)它的引用:

const EventEmitter = require("events").EventEmitter;
const eeInstance = new EventEmitter();

EventEmitter的基本方法如下:

on(event,listener):此方法允許您為給定的事件類(lèi)型(String類(lèi)型)注冊(cè)一個(gè)新的偵聽(tīng)器(一個(gè)函數(shù))

once(event, listener):此方法注冊(cè)一個(gè)新的監(jiān)聽(tīng)器,然后在事件首次發(fā)布之后被刪除

emit(event, [arg1], [...]):此方法會(huì)生成一個(gè)新事件,并提供其他參數(shù)以傳遞給偵聽(tīng)器

removeListener(event, listener):此方法將刪除指定事件類(lèi)型的偵聽(tīng)器

所有上述方法將返回EventEmitter實(shí)例以允許鏈接。監(jiān)聽(tīng)器函數(shù)function([arg1], [...]),所以它只是接受事件發(fā)出時(shí)提供的參數(shù)。在偵聽(tīng)器中,這是指EventEmitter生成事件的實(shí)例。
我們可以看到,一個(gè)監(jiān)聽(tīng)器和一個(gè)傳統(tǒng)的Node.js回調(diào)有很大的區(qū)別;特別地,第一個(gè)參數(shù)不是error,它是在調(diào)用時(shí)傳遞給emit()的任何數(shù)據(jù)。

創(chuàng)建和使用EventEmitter

我們來(lái)看看我們?nèi)绾卧趯?shí)踐中使用EventEmitter。最簡(jiǎn)單的方法是創(chuàng)建一個(gè)新的實(shí)例并立即使用它。以下代碼顯示了在文件列表中找到匹配特定正則的文件內(nèi)容時(shí),使用EventEmitter實(shí)現(xiàn)實(shí)時(shí)通知訂閱者的功能:

const EventEmitter = require("events").EventEmitter;
const fs = require("fs");

function findPattern(files, regex) {
  const emitter = new EventEmitter();
  files.forEach(function(file) {
    fs.readFile(file, "utf8", (err, content) => {
      if (err)
        return emitter.emit("error", err);
      emitter.emit("fileread", file);
      let match;
      if (match = content.match(regex))
        match.forEach(elem => emitter.emit("found", file, elem));
    });
  });
  return emitter;
}

由前面的函數(shù)EventEmitter處理將產(chǎn)生的三個(gè)事件:

fileread事件:當(dāng)文件被讀取時(shí)觸發(fā)

found事件:當(dāng)文件內(nèi)容被正則匹配成功時(shí)觸發(fā)

error事件:當(dāng)讀取文件出現(xiàn)錯(cuò)誤時(shí)觸發(fā)

下面看findPattern()函數(shù)是如何被觸發(fā)的:

findPattern(["fileA.txt", "fileB.json"], /hello w+/g)
  .on("fileread", file => console.log(file + " was read"))
  .on("found", (file, match) => console.log("Matched "" + match + "" in file " + file))
  .on("error", err => console.log("Error emitted: " + err.message));

在前面的例子中,我們?yōu)?b>EventParttern()函數(shù)創(chuàng)建的EventEmitter生成的每個(gè)事件類(lèi)型注冊(cè)了一個(gè)監(jiān)聽(tīng)器。

錯(cuò)誤傳播

如果事件是異步發(fā)送的,EventEmitter不能在異常情況發(fā)生時(shí)拋出異常,異常會(huì)在事件循環(huán)中丟失。相反,而是emit是發(fā)出一個(gè)稱(chēng)為錯(cuò)誤的特殊事件,Error對(duì)象通過(guò)參數(shù)傳遞。這正是我們?cè)谥岸x的findPattern()函數(shù)中正在做的。

對(duì)于錯(cuò)誤事件,始終是最佳做法注冊(cè)偵聽(tīng)器,因?yàn)?b>Node.js會(huì)以特殊的方式處理它,并且如果沒(méi)有找到相關(guān)聯(lián)的偵聽(tīng)器,將自動(dòng)拋出異常并退出程序。

讓任意對(duì)象可觀(guān)察

有時(shí),直接通過(guò)EventEmitter類(lèi)創(chuàng)建一個(gè)新的可觀(guān)察的對(duì)象是不夠的,因?yàn)樵?b>EventEmitter類(lèi)并沒(méi)有提供我們實(shí)際運(yùn)用場(chǎng)景的拓展功能。我們可以通過(guò)擴(kuò)展EventEmitter類(lèi)使一個(gè)通用對(duì)象可觀(guān)察。

為了演示這個(gè)模式,我們?cè)囍趯?duì)象中實(shí)現(xiàn)findPattern()函數(shù)的功能,如下代碼所示:

const EventEmitter = require("events").EventEmitter;
const fs = require("fs");
class FindPattern extends EventEmitter {
  constructor(regex) {
    super();
    this.regex = regex;
    this.files = [];
  }
  addFile(file) {
    this.files.push(file);
    return this;
  }
  find() {
    this.files.forEach(file => {
      fs.readFile(file, "utf8", (err, content) => {
        if (err) {
          return this.emit("error", err);
        }
        this.emit("fileread", file);
        let match = null;
        if (match = content.match(this.regex)) {
          match.forEach(elem => this.emit("found", file, elem));
        }
      });
    });
    return this;
  }
}

我們定義的FindPattern類(lèi)中運(yùn)用了核心模塊util提供的inherits()函數(shù)來(lái)擴(kuò)展EventEmitter。以這種方式,它成為一個(gè)符合我們實(shí)際運(yùn)用場(chǎng)景的可觀(guān)察類(lèi)。以下是其用法的示例:

const findPatternObject = new FindPattern(/hello w+/);
findPatternObject
  .addFile("fileA.txt")
  .addFile("fileB.json")
  .find()
  .on("found", (file, match) => console.log(`Matched "${match}"
       in file ${file}`))
  .on("error", err => console.log(`Error emitted ${err.message}`));

現(xiàn)在,通過(guò)繼承EventEmitter的功能,我們現(xiàn)在可以看到FindPattern對(duì)象除了可觀(guān)察外,還有一整套方法。
這在Node.js生態(tài)系統(tǒng)中是一個(gè)很常見(jiàn)的模式,例如,核心HTTP模塊的Server對(duì)象定義了listen(),close(),setTimeout()等方法,并且在內(nèi)部它也繼承自EventEmitter函數(shù),從而允許它在收到新的請(qǐng)求、建立新的連接或者服務(wù)器關(guān)閉響應(yīng)請(qǐng)求相關(guān)的事件。

擴(kuò)展EventEmitter的對(duì)象的其他示例是Node.js流。我們將在第五章中更詳細(xì)地分析Node.js的流。

同步和異步事件

與回調(diào)模式類(lèi)似,事件也支持同步或異步發(fā)送。至關(guān)重要的是,我們決不應(yīng)當(dāng)在同一個(gè)EventEmitter中混合使用兩種方法,但是在發(fā)布相同的事件類(lèi)型時(shí)考慮同步或者異步顯得至關(guān)重要,以避免產(chǎn)生因同步與異步順序不一致導(dǎo)致的zalgo。

發(fā)布同步和異步事件的主要區(qū)別在于觀(guān)察者注冊(cè)的方式。當(dāng)事件異步發(fā)布時(shí),即使在EventEmitter初始化之后,程序也會(huì)注冊(cè)新的觀(guān)察者,因?yàn)楸仨毐WC此事件在事件循環(huán)下一周期之前不被觸發(fā)。正如上邊的findPattern()函數(shù)中的情況。它代表了大多數(shù)Node.js異步模塊中使用的常用方法。

相反,同步發(fā)布事件要求在EventEmitter函數(shù)開(kāi)始發(fā)出任何事件之前就得注冊(cè)好觀(guān)察者??聪旅娴睦樱?/p>

const EventEmitter = require("events").EventEmitter;
class SyncEmit extends EventEmitter {
  constructor() {
    super();
    this.emit("ready");
  }
}
const syncEmit = new SyncEmit();
syncEmit.on("ready", () => console.log("Object is ready to be  used"));

如果ready事件是異步發(fā)布的,那么上述代碼將會(huì)正常運(yùn)行,然而,由于事件是同步發(fā)布的,并且監(jiān)聽(tīng)器在發(fā)送事件之后才被注冊(cè),所以結(jié)果不調(diào)用監(jiān)聽(tīng)器,該代碼將無(wú)法打印到控制臺(tái)。

由于不同的應(yīng)用場(chǎng)景,有時(shí)以同步方式使用EventEmitter函數(shù)是有意義的。因此,要清楚地突出我們的EventEmitter的同步和異步性,以避免產(chǎn)生不必要的錯(cuò)誤和異常。

事件機(jī)制與回調(diào)機(jī)制的比較

在定義異步API時(shí),常見(jiàn)的難點(diǎn)是檢查是否使用EventEmitter的事件機(jī)制或僅接受回調(diào)函數(shù)。一般區(qū)分規(guī)則是這樣的:當(dāng)一個(gè)結(jié)果必須以異步方式返回時(shí),應(yīng)該使用回調(diào)函數(shù),當(dāng)需要結(jié)果不確定其方式時(shí),應(yīng)該使用事件機(jī)制來(lái)響應(yīng)。

但是,由于這兩者實(shí)在太相近,并且可能兩種方式都能實(shí)現(xiàn)相同的應(yīng)用場(chǎng)景,所以產(chǎn)生了許多混亂。以下列代碼為例:

function helloEvents() {
  const eventEmitter = new EventEmitter();
  setTimeout(() => eventEmitter.emit("hello", "hello world"), 100);
  return eventEmitter;
}

function helloCallback(callback) {
  setTimeout(() => callback("hello world"), 100);
}

helloEvents()helloCallback()在其功能上可以被認(rèn)為是等價(jià)的,第一個(gè)使用事件機(jī)制實(shí)現(xiàn),第二個(gè)則使用回調(diào)來(lái)通知調(diào)用者,而將事件作為參數(shù)傳遞。但是真正區(qū)分它們的是可執(zhí)行性,語(yǔ)義和要實(shí)現(xiàn)或使用的代碼量。雖然我們不能給出一套確定性的規(guī)則來(lái)選擇一種風(fēng)格,但我們當(dāng)然可以提供一些提示來(lái)幫助你做出決定。

相比于第一個(gè)例子,即觀(guān)察者模式而言,回調(diào)函數(shù)在支持不同類(lèi)型的事件時(shí)有一些限制。但是事實(shí)上,我們?nèi)匀豢梢酝ㄟ^(guò)將事件類(lèi)型作為回調(diào)的參數(shù)傳遞,或者通過(guò)接受多個(gè)回調(diào)來(lái)區(qū)分多個(gè)事件。然而,這樣做的話(huà)不能被認(rèn)為是一個(gè)優(yōu)雅的API。在這種情況下,EventEmitter可以提供更好的接口和更精簡(jiǎn)的代碼。

EventEmitter更優(yōu)秀的另一種應(yīng)用場(chǎng)景是多次觸發(fā)同一事件或不觸發(fā)事件的情況。事實(shí)上,無(wú)論操作是否成功,一個(gè)回調(diào)預(yù)計(jì)都只會(huì)被調(diào)用一次。但有一種特殊情況是,我們可能不知道事件在哪個(gè)時(shí)間點(diǎn)觸發(fā),在這種情況下,EventEmitter是首選。

最后,使用回調(diào)的API僅通知特定的回調(diào),但是使用EventEmitter函數(shù)可以讓多個(gè)監(jiān)聽(tīng)器都接收到通知。

回調(diào)機(jī)制和事件機(jī)制結(jié)合使用

還有一些情況可以將事件機(jī)制和回調(diào)結(jié)合使用。特別是當(dāng)我們導(dǎo)出異步函數(shù)時(shí),這種模式非常有用。node-glob模塊是該模塊的一個(gè)示例。

glob(pattern, [options], callback)

該函數(shù)將一個(gè)文件名匹配模式作為第一個(gè)參數(shù),后面兩個(gè)參數(shù)分別為一組選項(xiàng)和一個(gè)回調(diào)函數(shù),對(duì)于匹配到指定文件名匹配模式的文件列表,相關(guān)回調(diào)函數(shù)會(huì)被調(diào)用。同時(shí),該函數(shù)返回EventEmitter,它展現(xiàn)了當(dāng)前進(jìn)程的狀態(tài)。例如,當(dāng)成功匹配文件名時(shí)可以實(shí)時(shí)發(fā)布match事件,當(dāng)文件列表全部匹配完畢時(shí)可以實(shí)時(shí)發(fā)布end事件,或者該進(jìn)程被手動(dòng)中止時(shí)發(fā)布abort事件。看以下代碼:

const glob = require("glob");
glob("data/*.txt", (error, files) => console.log(`All files found: ${JSON.stringify(files)}`))
  .on("match", match => console.log(`Match found: ${match}`));
總結(jié)

在本章中,我們首先了解了同步和異步的區(qū)別。然后,我們探討了如何使用回調(diào)機(jī)制和回調(diào)機(jī)制來(lái)處理一些基本的異步方案。我們還了解到兩種模式之間的主要區(qū)別,何時(shí)比另一種模式更適合解決具體問(wèn)題。我們只是邁向更先進(jìn)的異步模式的第一步。

在下一章中,我們將介紹更復(fù)雜的場(chǎng)景,了解如何利用回調(diào)機(jī)制和事件機(jī)制來(lái)處理高級(jí)異步控制問(wèn)題。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/89980.html

相關(guān)文章

  • Node.js學(xué)習(xí)之路08——fs文件系統(tǒng)之stream流的基本介紹

    摘要:中各種用于讀取數(shù)據(jù)的對(duì)象對(duì)象描述用于讀取文件代表客戶(hù)端請(qǐng)求或服務(wù)器端響應(yīng)代表一個(gè)端口對(duì)象用于創(chuàng)建子進(jìn)程的標(biāo)準(zhǔn)輸出流。如果子進(jìn)程和父進(jìn)程共享輸入輸出流,則子進(jìn)程的標(biāo)準(zhǔn)輸出流被廢棄用于創(chuàng)建子進(jìn)程的標(biāo)準(zhǔn)錯(cuò)誤輸出流。 9. stream流 fs模塊中集中文件讀寫(xiě)方法的區(qū)別 用途 使用異步方式 使用同步方式 將文件完整讀入緩存區(qū) readFile readFileSync 將文件部...

    BoYang 評(píng)論0 收藏0
  • Node.js知識(shí)點(diǎn)詳解(一)基礎(chǔ)部分

    摘要:基礎(chǔ)的端到端的基準(zhǔn)測(cè)試顯示大約比快八倍。所謂單線(xiàn)程,就是指一次只能完成一件任務(wù)。在服務(wù)器端,異步模式甚至是唯一的模式,因?yàn)閳?zhí)行環(huán)境是單線(xiàn)程的,如果允許同步執(zhí)行所有請(qǐng)求,服務(wù)器性能會(huì)急劇下降,很快就會(huì)失去響應(yīng)。 模塊 Node.js 提供了exports 和 require 兩個(gè)對(duì)象,其中 exports 是模塊公開(kāi)的接口,require 用于從外部獲取一個(gè)模塊的接口,即所獲取模塊的 e...

    whjin 評(píng)論0 收藏0
  • Node.js設(shè)計(jì)模式》歡迎來(lái)到Node.js平臺(tái)

    摘要:事件多路復(fù)用器收集資源的事件并且把這些事件放入隊(duì)列中,直到事件被處理時(shí)都是阻塞狀態(tài)。最后,處理事件多路復(fù)用器返回的每個(gè)事件,此時(shí),與系統(tǒng)資源相關(guān)聯(lián)的事件將被讀并且在整個(gè)操作中都是非阻塞的。 本系列文章為《Node.js Design Patterns Second Edition》的原文翻譯和讀書(shū)筆記,在GitHub連載更新,同步翻譯版鏈接。 歡迎關(guān)注我的專(zhuān)欄,之后的博文將在專(zhuān)欄同步:...

    Paul_King 評(píng)論0 收藏0
  • webpack4.x升級(jí)摘要

    摘要:以為例,編寫(xiě)來(lái)幫助我們完成重復(fù)的工作編譯壓縮我只要執(zhí)行一下就可以檢測(cè)到文件的變化,然后為你執(zhí)行一系列的自動(dòng)化操作,同樣的操作也發(fā)生在這些的預(yù)處理器上。的使用是針對(duì)第三方類(lèi)庫(kù)使用各種模塊化寫(xiě)法以及語(yǔ)法。 showImg(https://segmentfault.com/img/bVbtZYK); 一:前端工程化的發(fā)展 很久以前,互聯(lián)網(wǎng)行業(yè)有個(gè)職位叫做 軟件開(kāi)發(fā)工程師 在那個(gè)時(shí)代,大家可能...

    levinit 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<