国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

【譯】理解Node事件驅(qū)動架構(gòu)

mrcode / 1305人閱讀

摘要:回調(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 executeAfter 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

相關(guān)文章

  • [] Node.js 架構(gòu)概覽

    摘要:文件系統(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...

    antyiwei 評論0 收藏0
  • []理解 Node.js 事件驅(qū)動機制

    摘要:事件驅(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ǔ),值得一讀。 閱讀原文 大...

    Snailclimb 評論0 收藏0
  • []事件循環(huán),Node.js背后的核心概念

    摘要:事件處理器,則是當(dāng)指定事件觸發(fā)時,執(zhí)行的一段代碼。事件循環(huán)以一個無限循環(huán)的形式啟動,存在于二進制文件里函數(shù)的最后,當(dāng)沒有更多可被執(zhí)行的事件處理器時,它就退出。 前言 如果你了解過Node.js,那么你一定聽說過事件循環(huán)。你一定想知道它為什么那么特殊,并且為什么你需要關(guān)注它?此時此刻的你,可能已經(jīng)寫過許多基于Express.js的后端代碼,但沒有接觸到任何的循環(huán)。 在下文中,我們會先在一...

    Meils 評論0 收藏0
  • 」JavaScript 究竟是如何工作的?(第一部分)

    摘要:文章的第二部分涵蓋了內(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...

    Youngdze 評論0 收藏0
  • node js event loop part 1.1

    原文 先說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,總是...

    macg0406 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<