深入出不來nodejs源碼-events模塊

来源:https://www.cnblogs.com/QH-Jimmy/archive/2018/08/08/9438769.html
-Advertisement-
Play Games

這一節內容超級簡單,純JS,就當給自己放個假了,V8引擎和node的C++代碼看得有點腦闊疼。 學過DOM的應該都知道一個API,叫addeventlistener,即事件綁定。這個東西貫穿了整個JS的學習過程,無論是剛開始的自己獲取DOM手動綁,還是後期vue的直接@click,所有的交互都離不開 ...


  這一節內容超級簡單,純JS,就當給自己放個假了,V8引擎和node的C++代碼看得有點腦闊疼。

  學過DOM的應該都知道一個API,叫addeventlistener,即事件綁定。這個東西貫穿了整個JS的學習過程,無論是剛開始的自己獲取DOM手動綁,還是後期vue的直接@click,所有的交互都離不開這個東西。

  同樣,在node中,事件綁定也貫穿了整個框架。基本上大多數的內置模塊以events為原型,下麵的代碼隨處可見:

EventEmitter.call(this);

  不同的是,頁面上DOM的事件綁定是由瀏覽器來實現,觸發也是一些操作'間接'觸發,並不需要去主動emit對應事件,並且有冒泡和捕獲這兩特殊的性質。

  但是在node中,不存在dom,綁定的目標是一個對象(dom本質上也是對象),在內部node自己用純JS實現了一個事件綁定與事件觸發類。

  

  本文相關源碼來源於https://github.com/nodejs/node/blob/master/lib/events.js。

  首先看一下構造函數:

function EventEmitter() {
  EventEmitter.init.call(this);
}

  這裡會調用一個init方法,this指向調用對象,初始化方法也很簡單:

EventEmitter.init = function() {
  // 事件屬性
  if (this._events === undefined ||
      this._events === Object.getPrototypeOf(this)._events) {
    this._events = Object.create(null);
    this._eventsCount = 0;
  }
  // 同類型事件最大監聽數量
  this._maxListeners = this._maxListeners || undefined;
};

  涉及的三個屬性分別是:

1、_events => 一個掛載屬性,空對象,負責收集所有類型的事件

2、_eventsCount => 記錄目前綁定事件類型的數量

3、_maxListeners => 同類型事件listener數量限制

  事件相關的主要操作有3個,依次來看。

 

綁定事件/on

  雖然一般用的AP都是event.on,但是其實用addListener是一樣的:

EventEmitter.prototype.on = EventEmitter.prototype.addListener;
EventEmitter.prototype.addListener
= function addListener(type, listener) { return _addListener(this, type, listener, false); };

  這個addListener跟DOM的addEventListener稍微有點不一樣,前兩個參數一致,分別代表類型、回調函數。

  但是最後一個參數,這裡代表的是否優先插入該事件,有一個方法就是做這個的:

EventEmitter.prototype.prependListener =
    function prependListener(type, listener) {
      return _addListener(this, type, listener, true);
    };

  最終都指向這個_addListener,分步解釋:

/**
 * 事件綁定方法
 * @param {Object} target 目標對象
 * @param {String} type 事件名稱
 * @param {Function} listener 回調函數
 * @param {Boolean} prepend 是否插入
 */
function _addListener(target, type, listener, prepend) {
  // 指定事件類型的回調函數數量
  var m;
  // 事件屬性對象
  var events;
  // 對應類型的回調函數
  var existing;

  if (typeof listener !== 'function') {
    const errors = lazyErrors();
    throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener);
  }
  // 嘗試獲取對應類型的事件
  events = target._events;

  // 未找到對應的事件相關屬性
  if (events === undefined) {
    events = target._events = Object.create(null);
    target._eventsCount = 0;
  }
  // 當存在對象的事件屬性對象時
  else {}

  // more...

  return target;
}

  這裡首先會嘗試獲取指定對象的_events屬性,即構造函數中初始化的掛載對象屬性。

  由於無論是任意構造函數中調用EventEmitter.call(this)或者new EventEmitter()都會在生成對象上掛載一個_events對象,所以這個判斷暫時找不到反例。

  當不存在就手動初始化一個,並添加一個記數屬性重置為0。

  當存在時,處理代碼如下:

events = target._events;
if (events === undefined) {
  // ...
} else {
  // To avoid recursion in the case that type === "newListener"! Before
  // adding it to the listeners, first emit "newListener".
  if (events.newListener !== undefined) {
    target.emit('newListener', type,
                listener.listener ? listener.listener : listener);

    // Re-assign `events` because a newListener handler could have caused the
    // this._events to be assigned to a new object
    events = target._events;
  }
  // 嘗試獲取對應類型的回調函數集合
  existing = events[type];
}

  這個地方的註釋主要講的是,當綁定了type為newListener的事件時,每次都會觸發一次這個事件,如果再次綁定該事件會出現遞歸問題。所以要判斷是否存在newListener事件類型,如果有就先觸發一次newListener事件。

  先不管這個,最後會嘗試獲取指定類型的事件listener容器,下麵就是對existing的處理。

// 首次添加該類型事件時
if (existing === undefined) {
  // 直接把函數賦值給對應類型的key
  existing = events[type] = listener;
  // 記數+1
  ++target._eventsCount;
} else {
  // 1.已有對應類型 但是只有一個
  if (typeof existing === 'function') {
    // 轉換數組 根據prepend參數安排順序
    existing = events[type] =
      prepend ? [listener, existing] : [existing, listener];
    // If we've already got an array, just append.
  }
  // 2.已有多個 判斷是否有優先的flag進行前插或後插
  else if (prepend) {
    existing.unshift(listener);
  } else {
    existing.push(listener);
  }

  // Check for listener leak
  // ...
}

  這裡的處理就能很清楚的看到events模塊對於事件綁定的處理,_events相當於一個總對象,屬性的key就是對應的事件類型type,而key對應的value就是對應的listener。只有一個時,就直接用該listener做值。重覆綁定同類型的事件,這時值會轉換為數組保存所有的listener。這裡prepend就是之前的最後一個參數,允許函數插入到隊列的前面,優先觸發。

  最後還有一個綁定事件的數量判斷:

// 獲取_maxListeners參數 同類型事件listener最大綁定數量
m = $getMaxListeners(target);
// 如果超出就發出可能有記憶體泄漏的警告
if (m > 0 && existing.length > m && !existing.warned) {
  existing.warned = true;
  // 因為是warning所以不會有error code 可以不理這個東西
  // eslint-disable-next-line no-restricted-syntax
  const w = new Error('Possible EventEmitter memory leak detected. ' +
                      `${existing.length} ${String(type)} listeners ` +
                      'added. Use emitter.setMaxListeners() to ' +
                      'increase limit');
  w.name = 'MaxListenersExceededWarning';
  w.emitter = target;
  w.type = type;
  w.count = existing.length;
  process.emitWarning(w);
}

  看看就好,程式員不用管warning,哈哈。

 

一次綁定事件/once

  有些時候希望事件只觸發一次,原生的API目前不存在該功能,當初jquery也是封裝了一個once方法,對應的這個events模塊也有。

EventEmitter.prototype.once = function once(type, listener) {
  if (typeof listener !== 'function') {
    const errors = lazyErrors();
    throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener);
  }
  this.on(type, _onceWrap(this, type, listener));
  return this;
};

  除去那個判斷,其實綁定的方法還是同一個,只是對應的listener變成了一個包裝函數,來看看。

function _onceWrap(target, type, listener) {
  // this綁定對象
  var state = { fired: false, wrapFn: undefined, target, type, listener };
  var wrapped = onceWrapper.bind(state);
  // 原生的listener掛載到這個包裝函數上
  wrapped.listener = listener;
  // 處理完後更新state屬性
  state.wrapFn = wrapped;
  // 返回的是一個包裝的函數
  return wrapped;
}

function onceWrapper(...args) {
  // 這裡所有的this指向上面的state對象
  // args來源於觸發時候給的參數
  if (!this.fired) {
    // 解綁該包裝後的listener
    this.target.removeListener(this.type, this.wrapFn);
    this.fired = true;
    // 觸發listener
    Reflect.apply(this.listener, this.target, args);
  }
}

  思路其實跟jquery的源碼差不多,也是包裝listener,當觸發一次事件時,先解綁這個listener再觸發事件。

  需要註意的是,這裡存在兩個listener,一個是原生的,一個是包裝後的。綁定的是包裝的,所以解綁的第二個參數也要是包裝的。其中原生的作為listener屬性掛載到包裝後的函數上,實際上觸發包裝listener後內部會隱式調用原生listener。

 

事件觸發/emit

  看完綁定,來看觸發。

EventEmitter.prototype.emit = function emit(type, ...args) {
  let doError = (type === 'error');

  const events = this._events;
  // 判斷是否觸發的error類型事件
  if (events !== undefined)
    doError = (doError && events.error === undefined);
  else if (!doError)
    return false;

  // If there is no 'error' event listener then throw.
  if (doError) {
    // 錯誤處理 不看
  }
  // 跟之前的existing一個東西
  const handler = events[type];

  if (handler === undefined)
    return false;
  // 如果只有一個 直接調用
  if (typeof handler === 'function') {
    Reflect.apply(handler, this, args);
  } else {
    // 多個listener 依次觸發
    const len = handler.length;
    const listeners = arrayClone(handler, len);
    for (var i = 0; i < len; ++i)
      Reflect.apply(listeners[i], this, args);
  }

  return true;
};

  太簡單了,懶得解釋。

 

事件解綁/removeListener

  同樣分幾步來看解綁的過程,首先是參數聲明:

// Emits a 'removeListener' event if and only if the listener was removed.
EventEmitter.prototype.removeListener = function removeListener(type, listener) {
  // list => listener容器
  // events => 事件根對象
  // position => 記錄刪除listener位置
  // i => 迭代參數
  // originalListener => 原生listener 參考上面的once
  var list, events, position, i, originalListener;

  if (typeof listener !== 'function') {
    const errors = lazyErrors();
    throw new errors.ERR_INVALID_ARG_TYPE('listener', 'Function', listener);
  }

  events = this._events;
  if (events === undefined)
    return this;

  list = events[type];
  if (list === undefined)
    return this;

  // ...
}

  比較簡單,每個參數的用處都很明顯,錯誤判斷後,下麵有兩種不同的情況。

  當對應type的listener只有一個時:

EventEmitter.prototype.removeListener = function removeListener(type, listener) {
  // list => listener容器
  // events => 事件根對象
  // position => 記錄刪除listener位置
  // i => 迭代參數
  // originalListener => 原生listener 參考上面的once
  var list, events, position, i, originalListener;

  // ...

  // listener只有一個的情況
  if (list === listener || list.listener === listener) {
    // 如果一個綁定事件都沒了 直接重置_events對象
    if (--this._eventsCount === 0)
      this._events = Object.create(null);
    else {
      // 刪除對應的事件類型
      delete events[type];
      // 嘗試觸發一次removeListener事件
      if (events.removeListener)
        this.emit('removeListener', type, list.listener || listener);
    }
  } else if (typeof list !== 'function') {
    // ...
  }

  return this;
};

  這裡還分了兩種情況,如果_eventsCount為0,即所有的type都被清完,會重置_events對象。

  理論上來說,按照else分支的邏輯,當listener剩一個的時候都是直接delete對應的key,最後剩下的還是一個空對象,那這裡的重重置似乎變得沒有意義了。

  我猜測估計是為了V8層面的優化,因為對象的屬性在破壞性變動(添加屬性、重覆綁定同type事件導致函數變成函數數組)的時候,所需的記憶體會進行擴充,這個過程是不可逆的,就算最後只剩一個空殼對象,其實際占用也是相當大的。所以為了省空間,這裡進行重置,用很小的空間初始化_events對象,原來的空間被回收。

  當對應type的listener為多個時,就要遍歷了。

if (list === listener || list.listener === listener) {
  // ...
} else if (typeof list !== 'function') {
  position = -1;
  // 倒序遍歷
  for (i = list.length - 1; i >= 0; i--) {
    if (list[i] === listener || list[i].listener === listener) {
      // once綁定的事件有listener屬性
      originalListener = list[i].listener;
      // 記錄位置
      position = i;
      break;
    }
  }

  if (position < 0)
    return this;
  // 在第一個位置時
  if (position === 0)
    list.shift();
  else {
    // 刪除數組對應索引的值
    if (spliceOne === undefined)
      spliceOne = require('internal/util').spliceOne;
    spliceOne(list, position);
  }
  // 如果數組裡只有一個值 轉換為單個值
  // 有點像HashMap的鏈表-紅黑樹轉換……
  if (list.length === 1)
    events[type] = list[0];
  // 嘗試觸發removeListener
  if (events.removeListener !== undefined)
    this.emit('removeListener', type, originalListener || listener);
}

  太簡單了,自己看吧。

 

  其他還有諸如removeAllListeners、_listeners、eventNames等API,有興趣的可以自行去看。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ES6 為對象字面量添加了幾個實用的功能,雖然這幾個新功能基本上都是語法糖,但確實方便。 一、屬性的簡潔表示法 當定義一個對象時,允許直接寫入一個變數,作為對象的屬性,變數名就是屬性名。 例1: 與例2: 是相同的。 二、方法的簡潔表示法 下麵的例子是方法的簡潔表示法(例3) 和下例是完全等同的(例 ...
  • 規則 BFC觸發原理 作用 如果裡面的兩個div為浮動,那麼設置 .layout {overflow: hidden} 2. 防止margin重疊 總結: BFC通俗地說:就是一個容器,裡外不相互影響,記住:清除浮動的時候,如果使用 overflow: hidden,是存在缺點的,如果超過了範圍,那 ...
  • 1.技術棧 1.小程式; 2.使用了有贊提供的UI庫zan ui 3.express+mongoose+nginx; 2.功能介紹 1.假設有一天你在廣東的一間便利店裡面品嘗一瓶可口可樂,這時,你拿出了這個小程式“商品手賬”掃描這瓶可樂的商品條碼,然後寫下一段留言。 2.時間一晃就到了幾年後,這時你 ...
  • 1、背景:朋友請幫忙做一個比賽排程軟體 2、需求: ① 比賽人數未知,可以通過文本文件讀取參賽人員名稱; ② 對參賽人員隨機分組,一組兩人,兩兩PK,如果是奇數人數,某一個參賽人員成為幸運兒自動晉級; ③ 比賽線下進行,比賽結束後,可以線上選擇每組中晉級人員; ④ 晉級人員進行下一輪比賽分組,依此類 ...
  • 原型鏈 繼承 1 構造函數 2 原型鏈 3 組合繼承 把公共數據放在Parent中,這樣的話就不會公用一個引用類型 4 優化組合繼承 判斷原型和實例的關係 ...
  • MAIN結構 //// <%@ Master Language="C#" AutoEventWireup="true" CodeFile="Main.master.cs" Inherits="Main" %> <!DOCTYPE html><html xmlns="http://www.w3.org ...
  • 當父組件引用了子組件的時候,會遇到父組件執行子組件的方法,比如下拉刷新上拉載入等事件只有在頁面中才能檢測到,但是獲取數據的方法在子組件,這時就可以執行子組件方法。 思路很簡單,類似於vue中給子組件加ref執行子組件方法道理一樣,這裡是給子組件加一個 屬性: id="子組件名稱",比如: 然後在父組 ...
  • 引言 最近在學習node.js 連接redis的模塊,所以嘗試了一下在虛擬機中安裝cent OS7,並安裝redis,並使用node.js 操作redis。所以順便做個筆記。 如有不對的地方,歡迎大家指正! 1、cent OS7 下使用redis 1.1、配置編譯環境: 1.2、下載源碼: 1.3、 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...