在 Node.js 中,許許多多的異步操做,都須要來一個兜底的超時,這時,就輪到 timer 登場了。因爲須要使用它的地方是那麼的多,並且都是基礎的功能模塊,因此,對於它性能的要求,天然是十分高的。總結來講,要求有:node
更快的添加操做。git
更快的移除操做。github
更快的超時觸發。數據結構
接下來就讓咱們跟着 Node.js 項目中的 lib/timer.js
和 lib/internal/linklist.js
來探究它具體的實現。app
說到添加和移除都十分高效的數據結構,第一個映入腦簾的,天然就是鏈表啦。是的,Node.js 就是使用了雙向鏈表,來將 timer 的插入和移除操做的時間複雜度都降至 O(1) 。雙向鏈表的具體實現便在 lib/internal/linklist.js
中:dom
// lib/internal/linklist.js 'use strict'; function init(list) { list._idleNext = list; list._idlePrev = list; } exports.init = init; function peek(list) { if (list._idlePrev == list) return null; return list._idlePrev; } exports.peek = peek; function shift(list) { var first = list._idlePrev; remove(first); return first; } exports.shift = shift; function remove(item) { if (item._idleNext) { item._idleNext._idlePrev = item._idlePrev; } if (item._idlePrev) { item._idlePrev._idleNext = item._idleNext; } item._idleNext = null; item._idlePrev = null; } exports.remove = remove; function append(list, item) { remove(item); item._idleNext = list._idleNext; list._idleNext._idlePrev = item; item._idlePrev = list; list._idleNext = item; } exports.append = append; function isEmpty(list) { return list._idleNext === list; } exports.isEmpty = isEmpty;
能夠看到,都是些修改鏈表中指針的操做,都十分高效。異步
鏈表的缺點,天然是它的查找時間,對於一個無序的鏈表來講,查找時間須要 O(n) ,可是,只要基於一個大前提,那麼咱們的實現就並不須要使用到鏈表的查詢,這也是更高效的超時觸發的基礎所在,那就是,對於同一延遲的 timers ,後添加的必定比先添加的晚觸發。因此,源碼的具體作法就是,對於同一延遲的全部 timers ,所有都維護在同一個雙向鏈表中,後來的,就不斷往鏈表末尾追加,而且這條鏈表實際上共享同一個定時器 。這個定時器會在當次超時觸發時,動態計算下一次的觸發時間點。全部的鏈表,都保存在一個對象 map 中。如此一來,既作到了定時器的複用優化,又對鏈表結構進行了揚長避短。函數
讓咱們先以 setTimeout
爲例看看具體代碼,首先是插入:性能
// lib/timer.js // ... const refedLists = {}; const unrefedLists = {}; exports.setTimeout = function(callback, after) { // ... var timer = new Timeout(after); var length = arguments.length; var ontimeout = callback; // ... timer._onTimeout = ontimeout; active(timer); return timer; }; const active = exports.active = function(item) { insert(item, false); }; function insert(item, unrefed) { const msecs = item._idleTimeout; if (msecs < 0 || msecs === undefined) return; item._idleStart = TimerWrap.now(); var list = lists[msecs]; if (!list) { // ... list = new TimersList(msecs, unrefed); L.init(list); list._timer._list = list; if (unrefed === true) list._timer.unref(); list._timer.start(msecs, 0); lists[msecs] = list; list._timer[kOnTimeout] = listOnTimeout; } L.append(list, item); assert(!L.isEmpty(list)); }
即檢查當前在對象 map 中,是否存在該超時時間(msecs
)的雙向鏈表,若無,則新建一條。你應該已經看出,超時觸發時具體的處理邏輯,就在 listOnTimeout
函數中:優化
// lib/timer.js // ... function listOnTimeout() { var list = this._list; var msecs = list.msecs; var now = TimerWrap.now(); var diff, timer; while (timer = L.peek(list)) { diff = now - timer._idleStart; if (diff < msecs) { this.start(msecs - diff, 0); return; } L.remove(timer); // ... tryOnTimeout(timer, list); // ... } this.close(); // ... }
即不斷從鏈表頭取出封裝好的包含了註冊時間點和處理函數的對象,而後挨個執行,直到計算出的超時時間點已經超過當前時間點。
舉個圖例,在時間點 10,100,400 時分別註冊了三個超時時間爲 1000 的 timer,在時間點 300 註冊了一個超時時間爲 3000 的 timer,即在時間點 500 時,對象 map 的結構即爲:
隨後在時間點 1200 觸發了超時事件,並在時間點 1300 執行完畢,彼時對象 map 的結構即爲:
setInterval
的實現整體和 setTimeout
很類似,區別在於對註冊的回調函數進行了封裝,在鏈表的尾部從新插入:
// lib/timer.js // ... function wrapper() { timer._repeat(); // 執行傳入的回調函數 if (!timer._repeat) return; // ... timer._idleTimeout = repeat; active(timer); }
而 setImmediate
和 setTimeout
實現上的主要區別則在於,它會一次性將鏈表中註冊的,都執行完:
// lib/timer.js // ... function processImmediate() { var queue = immediateQueue; var domain, immediate; immediateQueue = {}; L.init(immediateQueue); while (L.isEmpty(queue) === false) { immediate = L.shift(queue); // ... tryOnImmediate(immediate, queue); // ... } if (L.isEmpty(immediateQueue)) { process._needImmediateCallback = false; } }
因此做爲功能相似的 process.nextTick
和 setImmediate
,在功能層面上看,每次事件循環,它們都會將存儲的回調都執行完,但 process.nextTick
中的存儲的回調,會先於 setImmediate
中的執行:
'use strict' const print = (i) => () => console.log(i) process.nextTick(print(1)) process.nextTick(print(2)) setImmediate(() => { print(3)() setImmediate(print(6)) process.nextTick(print(5)) }) setImmediate(print(4)) console.log('發車') // 發車 // 1 // 2 // 3 // 4 // 5 // 6
參考: