1. 程式人生 > >深入廣東快樂十分源碼下載nodejs源碼-events模塊

深入廣東快樂十分源碼下載nodejs源碼-events模塊

自己 pre else if 鏈表 fine 構造函數 Once util ejs

學過DOM的應該都知道廣東快樂十分源碼下載【dashengba.com】Q3266397597一個API,叫addeventlistener,即事件綁定。這個東西貫穿了整個JS的學習過程,無論是剛開始的自己獲取DOM手動綁,還是後期vue的直接@click,所有的交互都離不開這個東西。

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

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

  但是在node中,不存在dom,綁定的目標是一個對象(dom本質上也是對象),在內部node自己用純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);
}

深入廣東快樂十分源碼下載nodejs源碼-events模塊