摘要:雙向鏈表的具體實現便在中可以看到,都是些修改鏈表中指針的操作,都十分高效。如此一來,既做到了定時器的復用優化,又對鏈表結構進行了揚長避短。
在 Node.js 中,許許多多的異步操作,都需要來一個兜底的超時,這時,就輪到 timer 登場了。由于需要使用它的地方是那么的多,而且都是基礎的功能模塊,所以,對于它性能的要求,自然是十分高的。總結來說,要求有:
更快的添加操作。
更快的移除操作。
更快的超時觸發。
接下來就讓我們跟著 Node.js 項目中的 lib/timer.js 和 lib/internal/linklist.js 來探究它具體的實現。
更快的添加 / 移除操作說到添加和移除都十分高效的數據結構,第一個映入腦簾的,自然就是鏈表啦。是的,Node.js 就是使用了雙向鏈表,來將 timer 的插入和移除操作的時間復雜度都降至 O(1) 。雙向鏈表的具體實現便在 lib/internal/linklist.js 中:
// 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 和 setImmediatesetInterval 的實現總體和 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最后
參考:
https://github.com/nodejs/node/blob/master/lib/timers.js
https://github.com/nodejs/node/blob/master/lib/internal/linkedlist.js
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/87765.html
摘要:關于定時器的源碼在文件中,進入就關于定時器的一些設計解釋,因為是做服務端代碼,在內部等大部分事件都會創建一個定時器,任何時間都可能存在大量的定時器任務,所以設計一個高效的定時器是很有必要的。 博客文章地址 setTimeout與setInterval setTimeout 和 setInterval 是我們在 javaScript 中經常用到的定時器,setTimeout 方法用于...
摘要:注很多以前的源碼分析文章中,所寫的第一個執行的文件代碼為,但這個文件在中已被移除,并被拆解為了等其他下的文件,為正文作為第一段被執行的代碼,它的歷史使命免不了就是進行一些環境和全局變量的初始化工作。 大家可能會好奇,在 Node.js 啟動后,第一個執行的 JavaScript 文件會是哪個?它具體又會干些什么事? 一步步來看,翻開 Node.js 的源碼,不難看出,入口文件在 src...
摘要:事件觸發線程主要負責將準備好的事件交給引擎線程執行。它將不同的任務分配給不同的線程,形成一個事件循環,以異步的方式將任務的執行結果返回給引擎。 Fundebug經作者浪里行舟授權首發,未經同意請勿轉載。 前言 本文我們將會介紹 JS 實現異步的原理,并且了解了在瀏覽器和 Node 中 Event Loop 其實是不相同的。 一、線程與進程 1. 概念 我們經常說 JS 是單線程執行的,...
摘要:通過查看的文檔可以發現整個分為個階段定時器相關任務,中我們關注的是它會執行和中到期的回調執行某些系統操作的回調內部使用執行,一定條件下會在這個階段阻塞住執行的回調如果或者關閉了,就會在這個階段觸發事件,執行事件的回調的代碼在文件中。 showImg(https://segmentfault.com/img/bVbd7B7?w=1227&h=644); 這次我們就不要那么多前戲,直奔主題...
摘要:異步在中,是在單線程中執行的沒錯,但是內部完成工作的另有線程池,使用一個主進程和多個線程來模擬異步。在事件循環中,觀察者會不斷的找到線程池中已經完成的請求對象,從中取出回調函數和數據并執行。 1. 介紹 單線程編程會因阻塞I/O導致硬件資源得不到更優的使用。多線程編程也因為編程中的死鎖、狀態同步等問題讓開發人員頭痛。Node在兩者之間給出了它的解決方案:利用單線程,遠離多線程死鎖、狀態...
閱讀 3702·2021-11-11 11:00
閱讀 2180·2021-10-08 10:05
閱讀 2671·2021-10-08 10:04
閱讀 3204·2021-09-30 09:48
閱讀 3763·2021-09-27 14:10
閱讀 1704·2021-09-09 09:33
閱讀 2100·2019-08-30 15:55
閱讀 1602·2019-08-30 13:53