最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

理解 Node.js 事件驅動機制的原理

 更新時間:2017年08月16日 10:13:34   作者:痕跡絕陌路  
本篇文章主要介紹了理解 Node.js 事件驅動機制的原理,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧

學習 Node.js 一定要理解的內容之一,文中主要涉及到了 EventEmitter 的使用和一些異步情況的處理,比較偏基礎,值得一讀。

大多數(shù) Node.js 對象都依賴了 EventEmitter 模塊來監(jiān)聽和響應事件,比如我們常用的 HTTP requests, responses, 以及 streams。

const EventEmitter = require('events');

事件驅動機制的最簡單形式,是在 Node.js 中十分流行的回調函數(shù),例如 fs.readFile。 在回調函數(shù)這種形式中,事件每被觸發(fā)一次,回調就會被觸發(fā)一次。

我們先來探索下這個最基本的方式。

你準備好了就叫我哈,Node!

很久很久以前,在 js 里還沒有原生支持 Promise,async/await 還只是一個遙遠的夢想,回調函數(shù)是處理異步問題的最原始的方式。

回調從本質上講是傳遞給其他函數(shù)的函數(shù),在 JavaScript 中函數(shù)是第一類對象,這也讓回調的存在成為可能。

一定要搞清楚的是,回調在代碼中的并不表示異步調用。 回調既可以是同步調用的,也可以是異步調用的。

舉個例子,這里有一個宿主函數(shù) fileSize,它接受一個回調函數(shù) cb,并且可以通過條件判斷來同步或者異步地調用該回調函數(shù):

function fileSize (fileName, cb) {
 if (typeof fileName !== 'string') {
  // Sync
  return cb(new TypeError('argument should be string')); 
 } 
 fs.stat(fileName, (err, stats) => {
  if (err) {  
   // Async
   return cb(err); 
   } 
   // Async
  cb(null, stats.size);
 });
}

這其實也是個反例,這樣寫經常會引起一些意外的錯誤,在設計宿主函數(shù)的時候,應當盡可能的使用同一種風格,要么始終都是同步的使用回調,要么始終都是異步的。

我們來研究下一個典型的異步 Node 函數(shù)的簡單示例,它用回調樣式編寫:

const readFileAsArray = function(file, cb) {
 fs.readFile(file, function(err, data) {
  if (err) {
   return cb(err);
  }
  const lines = data.toString().trim().split('\n');
  cb(null, lines);
 });
};

readFileAsArray 函數(shù)接受兩個參數(shù):一個文件路徑和一個回調函數(shù)。它讀取文件內容,將其拆分成行數(shù)組,并將該數(shù)組作為回調函數(shù)的參數(shù)傳入,調用回調函數(shù)。

現(xiàn)在設計一個用例,假設我們在同一目錄中的文件 numbers.txt 包含如下內容:

10
11
12
13
14
15

如果我們有一個需求,要求統(tǒng)計該文件中的奇數(shù)數(shù)量,我們可以使用 readFileAsArray 來簡化代碼:

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);
});

這段代碼將文件內容讀入字符串數(shù)組中,回調函數(shù)將其解析為數(shù)字,并計算奇數(shù)的個數(shù)。

這才是最純粹的 Node 回調風格。回調的第一個參數(shù)要遵循錯誤優(yōu)先的原則,err 可以為空,我們要將回調作為宿主函數(shù)的最后一個參數(shù)傳遞。你應該一直用這種方式這樣設計你的函數(shù),因為用戶可能會假設。讓宿主函數(shù)把回調當做其最后一個參數(shù),并讓回調函數(shù)以一個可能為空的錯誤對象作為其第一個參數(shù)。

回調在現(xiàn)代 JavaScript 中的替代品

在現(xiàn)代 JavaScript 中,我們有 Promise,Promise 可以用來替代異步 API 的回調?;卣{函數(shù)需要作為宿主函數(shù)的一個參數(shù)進行傳遞(多個宿主回調進行嵌套就形成了回調地獄),而且錯誤和成功都只能在其中進行處理。而 Promise 對象可以讓我們分開處理成功和錯誤,還允許我們鏈式調用多個異步事件。

如果 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);

我們在宿主函數(shù)的返回值上調用了一個函數(shù)來處理我們的需求,這個 .then 函數(shù)會把剛剛在回調版本中的那個行數(shù)組傳遞給這里的匿名函數(shù)。為了處理錯誤,我們在結果上添加一個 .catch 調用,當發(fā)生錯誤時,它會捕捉到錯誤并讓我們訪問到這個錯誤。

在現(xiàn)代 JavaScript 中已經支持了 Promise 對象,因此我們可以很容易的將其使用在宿主函數(shù)之中。下面是支持 Promise 版本的 readFileAsArray 函數(shù)(同時支持舊有的回調函數(shù)方式):

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('\n');
   resolve(lines);
   cb(null, lines);
  });
 });
};

我們使該函數(shù)返回一個 Promise 對象,該對象包裹了 fs.readFile 的異步調用。Promise 對象暴露了兩個參數(shù),一個 resolve 函數(shù)和一個 reject 函數(shù)。

當有異常拋出時,我們可以通過向回調函數(shù)傳遞 error 來處理錯誤,也同樣可以使用 Promise 的 reject 函數(shù)。每當我們將數(shù)據(jù)交給回調函數(shù)處理時,我們同樣也可以用 Promise 的 resolve 函數(shù)。

在這種同時可以使用回調和 Promise 的情況下,我們需要做的唯一一件事情就是為這個回調參數(shù)設置默認值,防止在沒有傳遞回調函數(shù)參數(shù)時,其被執(zhí)行然后報錯的情況。 在這個例子中使用了一個簡單的默認空函數(shù):()=> {}。

通過 async/await 使用 Promise

當需要連續(xù)調用異步函數(shù)時,使用 Promise 會讓你的代碼更容易編寫。不斷的使用回調會讓事情變得越來越復雜,最終陷入回調地獄。

Promise 的出現(xiàn)改善了一點,Generator 的出現(xiàn)又改善了一點。 處理異步問題的最新解決方式是使用 async 函數(shù),它允許我們將異步代碼視為同步代碼,使其整體上更加可讀。

以下是使用 async/await 版本的調用 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)建了一個 async 函數(shù) —— 就是一個普通的函數(shù)聲明之前,加了個 async 關鍵字。在 async 函數(shù)內部,我們調用了 readFileAsArray 函數(shù),就像把它的返回值賦值給變量 lines 一樣,為了真的拿到 readFileAsArray 處理生成的行數(shù)組,我們使用關鍵字 await。之后,我們繼續(xù)執(zhí)行代碼,就好像 readFileAsArray 的調用是同步的一樣。

要讓代碼運行,我們可以直接調用 async 函數(shù)。這讓我們的代碼變得更加簡單和易讀。為了處理異常,我們需要將異步調用包裝在一個 try/catch 語句中。

有了 async/await 這個特性,我們不必使用任何特殊的API(如 .then 和 .catch )。我們只是把這種函數(shù)標記出來,然后使用純粹的 JavaScript 寫代碼。

我們可以把 async/await 這個特性用在支持使用 Promise 處理后續(xù)邏輯的函數(shù)上。但是,它無法用在只支持回調的異步函數(shù)上(例如setTimeout)。

EventEmitter 模塊

EventEmitter 是一個處理 Node 中各個對象之間通信的模塊。 EventEmitter 是 Node 異步事件驅動架構的核心。 Node 的許多內置模塊都繼承自 EventEmitter。

它的概念其實很簡單:emitter 對象會發(fā)出被定義過的事件,導致之前注冊的所有監(jiān)聽該事件的函數(shù)被調用。所以,emitter 對象基本上有兩個主要特征:

  • 觸發(fā)定義過的事件
  • 注冊或者取消注冊監(jiān)聽函數(shù)

為了使用 EventEmitter,我們需要創(chuàng)建一個繼承自 EventEmitter 的類。

class MyEmitter extends EventEmitter {
}

我們從 EventEmitter 的子類實例化的對象,就是 emitter 對象:

const myEmitter = new MyEmitter();

在這些 emitter 對象的生命周期里,我們可以調用 emit 函數(shù)來觸發(fā)我們想要的觸發(fā)的任何被命名過的事件。

myEmitter.emit('something-happened');
emit 函數(shù)的使用表示發(fā)生某種情況發(fā)生了,讓大家去做該做的事情。 這種情況通常是某些狀態(tài)變化引起的。

我們可以使用 on 方法添加監(jiān)聽器函數(shù),并且每次 emitter 對象觸發(fā)其關聯(lián)的事件時,將執(zhí)行這些監(jiān)聽器函數(shù)。

事件 !== 異步

先看看這個例子:

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ù),并在其前后包裹 log 以輸出其執(zhí)行日志。

為了看到這里會以什么順序執(zhí)行,我們在兩個命名的事件上都注冊了監(jiān)聽器,最后執(zhí)行一個簡單的任務來觸發(fā)事件。

下面是上面程序的輸出結果:

Before executing
About to execute
*** Executing task ***
Done with execute
After executing

這里我想證實的是以上的輸出都是同步發(fā)生的,這段代碼里沒有什么異步的成分。

  • 第一行輸出了 "Before executing"
  • begin 事件被觸發(fā),輸出 "About to execute"
  • 真正應該被執(zhí)行的任務函數(shù)被調用,輸出 " Executing task "
  • end 事件被觸發(fā),輸出 "Done with execute"
  • 最后輸出 "After executing"

就像普通的回調一樣,不要以為事件意味著同步或異步代碼。

跟之前的回調一樣,不要一提到事件就認為它是異步的或者同步的,還要具體分析。

如果我們傳遞 taskFunc 是一個異步函數(shù),會發(fā)生什么呢?

// ...

withLog.execute(() => {
 setImmediate(() => {
  console.log('*** Executing task ***')
 });
});

輸出結果變成了這樣:

Before executing
About to execute
Done with execute
After executing
*** Executing task ***

這樣就有問題了,異步函數(shù)的調用導致 "Done with execute" 和 "After executing" 的輸出并不準確。

要在異步函數(shù)完成后發(fā)出事件,我們需要將回調(或 Promise)與基于事件的通信相結合。 下面的例子說明了這一點。

使用事件而不是常規(guī)回調的一個好處是,我們可以通過定義多個監(jiān)聽器對相同的信號做出多個不同的反應。如果使用回調來完成這件事,我們要在單個回調中寫更多的處理邏輯。事件是應用程序允許多個外部插件在應用程序核心之上構建功能的好辦法。你可以把它們當成鉤子來掛一些由于狀態(tài)變化而引發(fā)執(zhí)行的程序。

異步事件

我們把剛剛那些同步代碼的示例改成異步的:

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í)行 asyncFunc 函數(shù),并通過調用 console.time 和 console.timeEnd 報告該asyncFunc 所花費的時間。它在執(zhí)行之前和之后都將以正確的順序觸發(fā)相應的事件,并且還會發(fā)出 error/data 事件作為處理異步調用的信號。

我們傳遞一個異步的 fs.readFile 函數(shù)來測試一下 withTime emitter。 我們現(xiàn)在可以直接通過監(jiān)聽 data 事件來處理讀取到的文件數(shù)據(jù),而不用把這套處理邏輯寫到 fs.readFile 的回調函數(shù)中。

執(zhí)行這段代碼,我們以預期的順序執(zhí)行了一系列事件,并且得到異步函數(shù)的執(zhí)行時間,這些是十分重要的。

About to execute
execute: 4.507ms
Done with execute

請注意,我們是將回調與事件觸發(fā)器 emitter 相結合實現(xiàn)的這部分功能。 如果 asynFunc 支持Promise,我們可以使用 async/await 函數(shù)來做同樣的事情:

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);
  }
 }
}

我認為這段代碼比之前的回調風格的代碼以及使用 .then/.catch 風格的代碼更具可讀性。async/await 讓我們更加接近 JavaScript 語言本身(不必再使用 .then/.catch 這些 api)。

事件參數(shù)和錯誤

在之前的例子中,有兩個事件被發(fā)出時還攜帶了別的參數(shù)。

error 事件被觸發(fā)時會攜帶一個 error 對象。

this.emit('error', err);

data 事件被觸發(fā)時會攜帶一個 data 對象。

this.emit('data', data);

我們可以在 emit 函數(shù)中不斷的添加參數(shù),當然第一個參數(shù)一定是事件的名稱,除去第一個參數(shù)之外的所有參數(shù)都可以在該事件注冊的監(jiān)聽器中使用。

例如,要處理 data 事件,我們注冊的監(jiān)聽器函數(shù)將訪問傳遞給 emit 函數(shù)的 data 參數(shù),而這個 data 也正是由 asyncFunc 返回的數(shù)據(jù)。

withTime.on('data', (data) => {
 // do something with data
});

error 事件比較特殊。在我們基于回調的那個示例中,如果不使用監(jiān)聽器處理 error 事件,node 進程將會退出。

舉個由于錯誤使用參數(shù)而造成程序崩潰的例子:

class WithTime extends EventEmitter {
 execute(asyncFunc, ...args) {
  console.time('execute');
  asyncFunc(...args, (err, data) => {
   if (err) {
    return this.emit('error', err); // Not Handled
   }

   console.timeEnd('execute');
  });
 }
}

const withTime = new WithTime();

withTime.execute(fs.readFile, ''); // BAD CALL
withTime.execute(fs.readFile, __filename);

第一次調用 execute 將會觸發(fā) error 事件,由于沒有處理 error ,Node 程序隨之崩潰:

events.js:163
   throw er; // Unhandled 'error' event
   ^
Error: ENOENT: no such file or directory, open ''

第二次執(zhí)行調用將受到此崩潰的影響,并且可能根本不會被執(zhí)行。

如果我們?yōu)檫@個 error 事件注冊一個監(jiān)聽器函數(shù)來處理 error,結果將大不相同:

withTime.on('error', (err) => {
 // do something with err, for example log it somewhere
 console.log(err)
});

如果我們執(zhí)行上述操作,將會報告第一次執(zhí)行 execute 時發(fā)送的錯誤,但是這次 node 進程不會崩潰退出,其他程序的調用也都能正常完成:

{ Error: ENOENT: no such file or directory, open '' errno: -2, code: 'ENOENT', syscall: 'open', path: '' }
execute: 4.276ms

需要注意的是,基于 Promise 的函數(shù)有些不同,它們暫時只是輸出一個警告:

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.

另一種處理異常的方式是在監(jiān)聽全局的 uncaughtException 進程事件。 然而,使用該事件全局捕獲錯誤并不是一個好辦法。

關于 uncaughtException,一般都會建議你避免使用它,但是如果必須用它,你應該讓進程退出:

process.on('uncaughtException', (err) => {
 // something went unhandled.
 // Do any cleanup and exit anyway!

 console.error(err); // don't do just that.

 // FORCE exit the process too.
 process.exit(1);
});

但是,假設在同一時間發(fā)生多個錯誤事件,這意味著上面的 uncaughtException 監(jiān)聽器將被多次觸發(fā),這可能會引起一些問題。

EventEmitter 模塊暴露了 once 方法,這個方法發(fā)出的信號只會調用一次監(jiān)聽器。所以,這個方法常與 uncaughtException 一起使用。

監(jiān)聽器的順序

如果針對一個事件注冊多個監(jiān)聽器函數(shù),當事件被觸發(fā)時,這些監(jiān)聽器函數(shù)將按其注冊的順序被觸發(fā)。

// first
withTime.on('data', (data) => {
 console.log(`Length: ${data.length}`);
});

// second
withTime.on('data', (data) => {
 console.log(`Characters: ${data.toString().length}`);
});

withTime.execute(fs.readFile, __filename);

上述代碼會先輸出 Length 信息,再輸出 Characters 信息,執(zhí)行的順序與注冊的順序保持一致。

如果你想定義一個新的監(jiān)聽函數(shù),但是希望它能夠第一個被執(zhí)行,你還可以使用 prependListener 方法:

withTime.on('data', (data) => {
 console.log(`Length: ${data.length}`);
});

withTime.prependListener('data', (data) => {
 console.log(`Characters: ${data.toString().length}`);
});

withTime.execute(fs.readFile, __filename);

上述代碼中,Charaters 信息將首先被輸出。

最后,你可以用 removeListener 函數(shù)來刪除某個監(jiān)聽器函數(shù)。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。

相關文章

  • node.js+npm的環(huán)境配置以及添加鏡像(保姆級教程)

    node.js+npm的環(huán)境配置以及添加鏡像(保姆級教程)

    本文主要介紹了node.js+npm的環(huán)境配置以及添加鏡像,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2025-02-02
  • Yapi安裝部署詳細圖文教程

    Yapi安裝部署詳細圖文教程

    YApi 是一個可本地部署的、打通前后端及QA的、可視化的接口管理平臺,下面這篇文章主要給大家介紹了關于Yapi安裝部署的相關資料,文中通過圖文介紹的非常詳細,需要的朋友可以參考下
    2022-09-09
  • 基于nodejs 的多頁面爬蟲實例代碼

    基于nodejs 的多頁面爬蟲實例代碼

    本篇文章主要介紹了基于nodejs 的多頁面爬蟲 ,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-05-05
  • 使用upstart把nodejs應用封裝為系統(tǒng)服務實例

    使用upstart把nodejs應用封裝為系統(tǒng)服務實例

    這篇文章主要介紹了使用upstart把nodejs應用封裝為系統(tǒng)服務實例,需要的朋友可以參考下
    2014-06-06
  • node.js中的path.sep方法使用說明

    node.js中的path.sep方法使用說明

    這篇文章主要介紹了node.js中的path.sep方法使用說明,本文介紹了path.sep的方法說明、語法、使用實例,需要的朋友可以參考下
    2014-12-12
  • autojs的Node.js正確退出腳本示例

    autojs的Node.js正確退出腳本示例

    這篇文章主要為大家介紹了autojs的Node.js正確退出腳本示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2023-01-01
  • nodejs中實現(xiàn)路由功能

    nodejs中實現(xiàn)路由功能

    這篇文章主要介紹了nodejs中實現(xiàn)路由功能,顧名思義,路由指的就是我們要針對不同的URL有不同的處理方式,本文將教你在node.js中實現(xiàn)路由功能,需要的朋友可以參考下
    2014-12-12
  • 我用的一些Node.js開發(fā)工具、開發(fā)包、框架等總結

    我用的一些Node.js開發(fā)工具、開發(fā)包、框架等總結

    這篇文章主要介紹了我個人使用的一些Node.js開發(fā)工具、開發(fā)包、框架等總結,需要的朋友可以參考下
    2014-09-09
  • node.js卸載并重新安裝的方法步驟

    node.js卸載并重新安裝的方法步驟

    本文詳細介紹了Node.js的卸載和重新安裝步驟,包括進入控制面板卸載程序,手動刪除文件夾,清理環(huán)境變量,以及如何重新下載并安裝Node.js,提供了詳細的操作指導和安裝Node.js的官方下載地址,確保用戶能夠順利完成Node.js的更新或重裝過程,感興趣的可以了解一下
    2024-11-11
  • 使用Node.js的readline模塊逐行讀取并解析大文件

    使用Node.js的readline模塊逐行讀取并解析大文件

    在Node.js環(huán)境中處理大文件是一個常見的需求,尤其是在處理日志文件、數(shù)據(jù)庫導出、或任何形式的大規(guī)模文本數(shù)據(jù)時,本文將深入探討如何使用Node.js的readline模塊來實現(xiàn)這一功能,并討論相關的性能優(yōu)化和注意事項,需要的朋友可以參考下
    2024-09-09

最新評論

平昌县| 兴业县| 宁波市| 东山县| 固阳县| 探索| 宁武县| 噶尔县| 东山县| 连南| 筠连县| 乌拉特后旗| 普陀区| 皋兰县| 泰安市| 神木县| 临桂县| 祁连县| 阳原县| 新乡县| 望谟县| 和静县| 沁源县| 丰城市| 平南县| 江口县| 华亭县| 九龙城区| 灌南县| 资兴市| 睢宁县| 乐平市| 濮阳县| 平和县| 遂昌县| 金坛市| 松原市| 新沂市| 菏泽市| 宝兴县| 新乡县|