摘要:之前的文章里有說,在中,流是許許多多原生對象的父類,角色可謂十分重要。效率更高的從數組中去除一個元素。不過這個所提供的功能過于多了,它支持去除自定義數量的元素,還支持向數組中添加自定義的元素。
之前的文章里有說,在 Node.js 中,流(stream)是許許多多原生對象的父類,角色可謂十分重要。但是,當我們沿著“族譜”往上看時,會發現 EventEmitter 類是流(stream)類的父類,所以可以說,EventEmitter 類是 Node.js 的根基類之一,地位可顯一般。雖然 EventEmitter 類暴露的接口并不多而且十分簡單,并且是少數純 JavaScript 實現的模塊之一,但因為它的應用實在是太廣泛,身份太基礎,所以在它的實現里處處閃光著一些優化代碼執行效率,和保證極端情況下代碼結果正確性的小細節。在了解之后,我們也可以將其使用到我們的日常編碼之后,學以致用。
好,現在就讓我們跟隨 Node.js 項目中的 lib/events.js 中的代碼,來逐一了解:
效率更高的 鍵 / 值 對存儲對象的創建。
效率更高的從數組中去除一個元素。
效率更高的不定參數的函數調用。
如果防止在一個事件監聽器中監聽同一個事件,接而導致死循環?
emitter.once 是怎么辦到的?
效率更高的 鍵 / 值 對存儲對象的創建在 EventEmitter 類中,以 鍵 / 值 對的方式來存儲事件名和對應的監聽器。在 Node.js里 ,最簡單的 鍵 / 值 對的存儲方式就是直接創建一個空對象:
let store = {} store.key = "value"
你可能會說,ES2015 中的 Map 已經在目前版本的 Node.js 中可用了,在語義上它更有優勢:
let store = new Map() store.set("key", "value")
不過,你可能只需要一個純粹的 鍵 / 值 對存儲對象,并不需要 Object 和 Map 這兩個類的原型中的提供的那些多余的方法,所以你直接:
let store = Object.create(null) store.key = "value"
好,我們已經做的挺極致了,但這還不是 EventEmitter 中的最終實現,它的辦法是使用一個空的構造函數,并且把這個構造的原型事先置空:
function Store () {} Store.prototype = Object.create(null)
然后:
let store = new Store() store.key = "value"
現在讓我們來比一比效率,代碼:
/* global suite bench */ "use strict" suite("key / value store", function () { function Store () {} Store.prototype = Object.create(null) bench("let store = {}", function () { let store = {} store.key = "value" }) bench("let store = new Map()", function () { let store = new Map() store.set("key", "value") }) bench("let store = Object.create(null)", function () { let store = Object.create(null) store.key = "value" }) bench("EventEmitter way", function () { let store = new Store() store.key = "value" }) })
比較結果:
key / value store 83,196,978 op/s ? let store = {} 4,826,143 op/s ? let store = new Map() 7,405,904 op/s ? let store = Object.create(null) 165,608,103 op/s ? EventEmitter way效率更高的從數組中去除一個元素
在 EventEmitter#removeListener 這個 API 的實現里,需要從存儲的監聽器數組中除去一個元素,我們首先想到的就是使用 Array#splice 這個 API ,即 arr.splice(i, 1) 。不過這個 API 所提供的功能過于多了,它支持去除自定義數量的元素,還支持向數組中添加自定義的元素。所以,源碼中選擇自己實現一個最小可用的:
// lib/events.js // ... function spliceOne(list, index) { for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) list[i] = list[k]; list.pop(); }
比一比,代碼:
/* global suite bench */ "use strict" suite("Remove one element from an array", function () { function spliceOne (list, index) { for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) { list[i] = list[k] } list.pop() } bench("Array#splice", function () { let array = [1, 2, 3] array.splice(1, 1) }) bench("EventEmitter way", function () { let array = [1, 2, 3] spliceOne(array, 1) }) })
結果,好吧,秒了:
Remove one element from an array 4,262,168 op/s ? Array#splice 54,829,749 op/s ? EventEmitter way效率更高的不定參數的函數調用
在事件觸發時,監聽器擁有的參數數量是任意的,所以源碼中優化了不定參數的函數調用。
不過好吧,這里使用的是笨辦法,即...把不定參數的函數調用轉變成固定參數的函數調用,且最多支持到三個參數:
// lib/events.js // ... function emitNone(handler, isFn, self) { // ... } function emitOne(handler, isFn, self, arg1) { // ... } function emitTwo(handler, isFn, self, arg1, arg2) { // ... } function emitThree(handler, isFn, self, arg1, arg2, arg3) { // ... } function emitMany(handler, isFn, self, args) { // ... }
雖然結果不言而喻,我們還是比較下會差多少,以三個參數為例:
/* global suite bench */ "use strict" suite("calling function with any amount of arguments", function () { function nope () {} bench("Function#apply", function () { function callMany () { nope.apply(null, arguments) } callMany(1, 2, 3) }) bench("EventEmitter way", function () { function callThree (a, b, c) { nope.call(null, a, b, c) } callThree(1, 2, 3) }) })
結果顯示差了一倍:
calling function with any amount of arguments 11,354,996 op/s ? Function#apply 23,773,458 op/s ? EventEmitter way如果防止在一個事件監聽器中監聽同一個事件,接而導致死循環?
在注冊事件監聽器時,你可否曾想到過這種情況:
"use strict" const EventEmitter = require("events") let myEventEmitter = new EventEmitter() myEventEmitter.on("wtf", function wtf () { myEventEmitter.on("wtf", wtf) }) myEventEmitter.emit("wtf")
運行上述代碼,是否會直接導致死循環?答案是不會,因為源碼中做了處理。
我們先看一下具體的代碼:
// lib/events.js // ... function emitMany(handler, isFn, self, args) { if (isFn) handler.apply(self, args); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].apply(self, args); } } // ... function arrayClone(arr, i) { var copy = new Array(i); while (i--) copy[i] = arr[i]; return copy; }
其中的 handler 便是具體的事件監聽器數組,不難看出,源碼中的解決方案是,使用 arrayClone 方法,拷貝出另一個一模一樣的數組,來執行它,這樣一來,當我們在監聽器內監聽同一個事件時,的確給原監聽器數組添加了新的函數,但并沒有影響到當前這個被拷貝出來的副本數組。
emitter.once 是怎么辦到的這個很簡單,使用了閉包:
function _onceWrap(target, type, listener) { var fired = false; function g() { target.removeListener(type, g); if (!fired) { fired = true; listener.apply(target, arguments); } } g.listener = listener; return g; }
你可能會問,我既然已經在 g 函數中的第一行中移除了當前的監聽器,為何還要使用 fired 這個 flag ?我個人覺得是因為,在 removeListener 這個同步方法中,會將這個 g 函數暴露出來給 removeListener 事件的監聽器,所以該 flag 用來保證 once 注冊的函數只會被調用一次。
最后分析就到這里啦,在了解了這些做法之后,在今后我們寫一些有性能要求的底層工具庫等東西時,我們便可以用上它們啦。EventEmitter 類的源碼并不復雜,并且是純 JavaScript 實現的,所以也非常推薦大家閑時一讀。
參考:https://github.com/nodejs/node/blob/master/lib/events.js
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/79295.html
摘要:為指定事件注冊一個單次監聽器,即監聽器最多只會觸發一次,觸發后立刻解除該監聽器。移除指定事件的某個監聽器,監聽器必須是該事件已經注冊過的監聽器。返回指定事件的監聽器數組。如何創建空對象我們已經了解到,是要來儲存監聽事件監聽器數組的。 毫無疑問,nodeJS改變了整個前端開發生態。本文通過分析nodeJS當中events模塊源碼,由淺入深,動手實現了屬于自己的ES6事件觀察者系統。千萬不...
摘要:實際上,我在看代碼的過程中順手提交了這個,作者眼明手快,當天就進行了修復,現在最新的代碼里已經不是這個樣子了而且狀態機標識由字符串換成了數字常量,解析更準確的同時執行效率也會更高。 最近饒有興致的又把最新版?Vue.js?的源碼學習了一下,覺得真心不錯,個人覺得 Vue.js 的代碼非常之優雅而且精辟,作者本身可能無 (bu) 意 (xie) 提及這些。那么,就讓我來吧:) 程序結構梳...
摘要:實際上,我在看代碼的過程中順手提交了這個,作者眼明手快,當天就進行了修復,現在最新的代碼里已經不是這個樣子了而且狀態機標識由字符串換成了數字常量,解析更準確的同時執行效率也會更高。 最近饒有興致的又把最新版?Vue.js?的源碼學習了一下,覺得真心不錯,個人覺得 Vue.js 的代碼非常之優雅而且精辟,作者本身可能無 (bu) 意 (xie) 提及這些。那么,就讓我來吧:) 程序結構梳...
摘要:感謝大神的免費的計算機編程類中文書籍收錄并推薦地址,以后在倉庫里更新地址,聲音版全文狼叔如何正確的學習簡介現在,越來越多的科技公司和開發者開始使用開發各種應用。 說明 2017-12-14 我發了一篇文章《沒用過Node.js,就別瞎逼逼》是因為有人在知乎上黑Node.js。那篇文章的反響還是相當不錯的,甚至連著名的hax賀老都很認同,下班時讀那篇文章,竟然坐車的還坐過站了。大家可以很...
閱讀 2849·2021-11-22 11:56
閱讀 3554·2021-11-15 11:39
閱讀 898·2021-09-24 09:48
閱讀 759·2021-08-17 10:14
閱讀 1322·2019-08-30 15:55
閱讀 2753·2019-08-30 15:55
閱讀 1310·2019-08-30 15:44
閱讀 2776·2019-08-30 10:59