摘要:雙向鏈表的具體實現(xiàn)便在中可以看到,都是些修改鏈表中指針的操作,都十分高效。如此一來,既做到了定時器的復用優(yōu)化,又對鏈表結構進行了揚長避短。
在 Node.js 中,許許多多的異步操作,都需要來一個兜底的超時,這時,就輪到 timer 登場了。由于需要使用它的地方是那么的多,而且都是基礎的功能模塊,所以,對于它性能的要求,自然是十分高的??偨Y來說,要求有:
更快的添加操作。
更快的移除操作。
更快的超時觸發(fā)。
接下來就讓我們跟著 Node.js 項目中的 lib/timer.js 和 lib/internal/linklist.js 來探究它具體的實現(xiàn)。
更快的添加 / 移除操作說到添加和移除都十分高效的數(shù)據(jù)結構,第一個映入腦簾的,自然就是鏈表啦。是的,Node.js 就是使用了雙向鏈表,來將 timer 的插入和移除操作的時間復雜度都降至 O(1) 。雙向鏈表的具體實現(xiàn)便在 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;
可以看到,都是些修改鏈表中指針的操作,都十分高效。
更快的超時觸發(fā)鏈表的缺點,自然是它的查找時間,對于一個無序的鏈表來說,查找時間需要 O(n) ,但是,只要基于一個大前提,那么我們的實現(xiàn)就并不需要使用到鏈表的查詢,這也是更高效的超時觸發(fā)的基礎所在,那就是,對于同一延遲的 timers ,后添加的一定比先添加的晚觸發(fā)。所以,源碼的具體做法就是,對于同一延遲的所有 timers ,全部都維護在同一個雙向鏈表中,后來的,就不斷往鏈表末尾追加,并且這條鏈表實際上共享同一個定時器 。這個定時器會在當次超時觸發(fā)時,動態(tài)計算下一次的觸發(fā)時間點。所有的鏈表,都保存在一個對象 map 中。如此一來,既做到了定時器的復用優(yōu)化,又對鏈表結構進行了揚長避短。
讓我們先以 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)的雙向鏈表,若無,則新建一條。你應該已經(jīng)看出,超時觸發(fā)時具體的處理邏輯,就在 listOnTimeout 函數(shù)中:
// 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(); // ... }
即不斷從鏈表頭取出封裝好的包含了注冊時間點和處理函數(shù)的對象,然后挨個執(zhí)行,直到計算出的超時時間點已經(jīng)超過當前時間點。
舉個圖例,在時間點 10,100,400 時分別注冊了三個超時時間為 1000 的 timer,在時間點 300 注冊了一個超時時間為 3000 的 timer,即在時間點 500 時,對象 map 的結構即為:
隨后在時間點 1200 觸發(fā)了超時事件,并在時間點 1300 執(zhí)行完畢,彼時對象 map 的結構即為:
setInterval 和 setImmediatesetInterval 的實現(xiàn)總體和 setTimeout 很相似,區(qū)別在于對注冊的回調(diào)函數(shù)進行了封裝,在鏈表的尾部重新插入:
// lib/timer.js // ... function wrapper() { timer._repeat(); // 執(zhí)行傳入的回調(diào)函數(shù) if (!timer._repeat) return; // ... timer._idleTimeout = repeat; active(timer); }
而 setImmediate 和 setTimeout 實現(xiàn)上的主要區(qū)別則在于,它會一次性將鏈表中注冊的,都執(zhí)行完:
// 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 ,在功能層面上看,每次事件循環(huán),它們都會將存儲的回調(diào)都執(zhí)行完,但 process.nextTick 中的存儲的回調(diào),會先于 setImmediate 中的執(zhí)行:
"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("發(fā)車") // 發(fā)車 // 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
文章版權歸作者所有,未經(jīng)允許請勿轉載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉載請注明本文地址:http://systransis.cn/yun/87765.html
摘要:關于定時器的源碼在文件中,進入就關于定時器的一些設計解釋,因為是做服務端代碼,在內(nèi)部等大部分事件都會創(chuàng)建一個定時器,任何時間都可能存在大量的定時器任務,所以設計一個高效的定時器是很有必要的。 博客文章地址 setTimeout與setInterval setTimeout 和 setInterval 是我們在 javaScript 中經(jīng)常用到的定時器,setTimeout 方法用于...
摘要:注很多以前的源碼分析文章中,所寫的第一個執(zhí)行的文件代碼為,但這個文件在中已被移除,并被拆解為了等其他下的文件,為正文作為第一段被執(zhí)行的代碼,它的歷史使命免不了就是進行一些環(huán)境和全局變量的初始化工作。 大家可能會好奇,在 Node.js 啟動后,第一個執(zhí)行的 JavaScript 文件會是哪個?它具體又會干些什么事? 一步步來看,翻開 Node.js 的源碼,不難看出,入口文件在 src...
摘要:事件觸發(fā)線程主要負責將準備好的事件交給引擎線程執(zhí)行。它將不同的任務分配給不同的線程,形成一個事件循環(huán),以異步的方式將任務的執(zhí)行結果返回給引擎。 Fundebug經(jīng)作者浪里行舟授權首發(fā),未經(jīng)同意請勿轉載。 前言 本文我們將會介紹 JS 實現(xiàn)異步的原理,并且了解了在瀏覽器和 Node 中 Event Loop 其實是不相同的。 一、線程與進程 1. 概念 我們經(jīng)常說 JS 是單線程執(zhí)行的,...
摘要:通過查看的文檔可以發(fā)現(xiàn)整個分為個階段定時器相關任務,中我們關注的是它會執(zhí)行和中到期的回調(diào)執(zhí)行某些系統(tǒng)操作的回調(diào)內(nèi)部使用執(zhí)行,一定條件下會在這個階段阻塞住執(zhí)行的回調(diào)如果或者關閉了,就會在這個階段觸發(fā)事件,執(zhí)行事件的回調(diào)的代碼在文件中。 showImg(https://segmentfault.com/img/bVbd7B7?w=1227&h=644); 這次我們就不要那么多前戲,直奔主題...
摘要:異步在中,是在單線程中執(zhí)行的沒錯,但是內(nèi)部完成工作的另有線程池,使用一個主進程和多個線程來模擬異步。在事件循環(huán)中,觀察者會不斷的找到線程池中已經(jīng)完成的請求對象,從中取出回調(diào)函數(shù)和數(shù)據(jù)并執(zhí)行。 1. 介紹 單線程編程會因阻塞I/O導致硬件資源得不到更優(yōu)的使用。多線程編程也因為編程中的死鎖、狀態(tài)同步等問題讓開發(fā)人員頭痛。Node在兩者之間給出了它的解決方案:利用單線程,遠離多線程死鎖、狀態(tài)...
閱讀 3744·2021-11-11 11:00
閱讀 2213·2021-10-08 10:05
閱讀 2781·2021-10-08 10:04
閱讀 3240·2021-09-30 09:48
閱讀 3866·2021-09-27 14:10
閱讀 1741·2021-09-09 09:33
閱讀 2132·2019-08-30 15:55
閱讀 1630·2019-08-30 13:53