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