目錄
* 一.概述 <https://www.cnblogs.com/dashnowords/p/11078891.html#一.概述>
* 二. 數(shù)據(jù)結(jié)構(gòu) <https://www.cnblogs.com/dashnowords/p/11078891.html#二.-數(shù)據(jù)結(jié)構(gòu)>
* 2.1 鏈表 <https://www.cnblogs.com/dashnowords/p/11078891.html#鏈表>
* 2.2 二叉堆 <https://www.cnblogs.com/dashnowords/p/11078891.html#二叉堆>
* 三. 從setTimeout理解Timer模塊源碼
<https://www.cnblogs.com/dashnowords/p/11078891.html#三.-從settimeout理解timer模塊源碼>
* 3.1 timers.js中的定義
<https://www.cnblogs.com/dashnowords/p/11078891.html#timers.js中的定義>
* 3.2 Timeout類(lèi)定義
<https://www.cnblogs.com/dashnowords/p/11078891.html#timeout類(lèi)定義>
* 3.3 active(timeout)
<https://www.cnblogs.com/dashnowords/p/11078891.html#activetimeout>
* 3.4 定時(shí)器的處理執(zhí)行邏輯
<https://www.cnblogs.com/dashnowords/p/11078891.html#定時(shí)器的處理執(zhí)行邏輯>
* 3.5 實(shí)例分析 <https://www.cnblogs.com/dashnowords/p/11078891.html#實(shí)例分析>
* 四. 小結(jié) <https://www.cnblogs.com/dashnowords/p/11078891.html#四.-小結(jié)>
示例代碼托管在:http://www.github.com/dashnowords/blogs
<http://www.github.com/dashnowords/blogs>
博客園地址:《大史住在大前端》原創(chuàng)博文目錄 <https://www.cnblogs.com/dashnowords/p/10127926.html>
華為云社區(qū)地址:【你要的前端打怪升級(jí)指南】
<https://bbs.huaweicloud.com/blogs/8ae7e6420a4611e9bd5a7ca23e93a891>
一.概述
Timer模塊相關(guān)的邏輯較為復(fù)雜,不僅包含JavaScript層的實(shí)現(xiàn),也包括C++編寫(xiě)的與底層libuv協(xié)作的代碼,想要完整地看明白是比較困難的,本章僅以
setTimeout這個(gè)API的實(shí)現(xiàn)機(jī)制為主線(xiàn),講述源碼中的JavaScript相關(guān)的實(shí)現(xiàn)部分,這部分只需要一些數(shù)據(jù)結(jié)構(gòu)的基本知識(shí)就可以理解。
二. 數(shù)據(jù)結(jié)構(gòu)
setTimeout這個(gè)API的實(shí)現(xiàn)基于兩類(lèi)基本數(shù)據(jù)結(jié)構(gòu),我們先來(lái)復(fù)習(xí)一下相關(guān)的特點(diǎn)。對(duì)數(shù)據(jù)結(jié)構(gòu)知識(shí)比較陌生的小伙伴可以參考【野生前端的數(shù)據(jù)結(jié)構(gòu)基礎(chǔ)練習(xí)】
<https://www.cnblogs.com/dashnowords/p/10127926.html#%E9%87%8E%E7%94%9F%E5%89%8D%E7%AB%AF%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%92%8C%E7%AE%97%E6%B3%95%E5%9F%BA%E7%A1%80%E7%BB%83%E4%B9%A0%E7%B3%BB%E5%88%97>
系列博文自行學(xué)習(xí),所有的章節(jié)都有示例代碼。
2.1 鏈表
鏈表是一種物理存儲(chǔ)單元上非連續(xù)的存儲(chǔ)結(jié)構(gòu),存儲(chǔ)元素的邏輯順序是由鏈表中的指針鏈接次序來(lái)決定的。每一個(gè)節(jié)點(diǎn)包含一個(gè)存放數(shù)據(jù)的數(shù)據(jù)域和存放下一個(gè)節(jié)點(diǎn)的指針域(雙向鏈表中指針數(shù)量為2)。鏈表在插入元素時(shí)的時(shí)間復(fù)雜度為
O(1)(因?yàn)橹挥绊懖迦朦c(diǎn)前后的節(jié)點(diǎn),無(wú)論鏈表有多大),但是由于空間不連續(xù)的特點(diǎn),訪問(wèn)一個(gè)未排序鏈表的指定節(jié)點(diǎn)時(shí)就需要逐個(gè)對(duì)比,時(shí)間復(fù)雜度為O(n)
,比數(shù)組結(jié)構(gòu)就要慢一些。鏈表結(jié)構(gòu)也可以根據(jù)指針特點(diǎn)分為單向鏈表,雙向鏈表和循環(huán)鏈表,Timer模塊中使用的鏈表結(jié)構(gòu)就是雙向循環(huán)鏈表,Node.js
中源碼的底層數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)都是獨(dú)立的,鏈表的源碼放在lib/internal/linkedlist.js
<https://github.com/nodejs/node/blob/master/lib/internal/linkedlist.js>:
'use strict'; function init(list) { list._idleNext = list; list._idlePrev =
list; } // Show the most idle item. function peek(list) { if (list._idlePrev
=== list) return null; return list._idlePrev; } // Remove an item from its
list. 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; } // Remove an
item from its list and place at the end. function append(list, item) { if
(item._idleNext || item._idlePrev) { remove(item); } // Items are linked with
_idleNext -> (older) and _idlePrev -> (newer). // Note: This linkage (next
being older) may seem counter-intuitive at first. item._idleNext =
list._idleNext; //1 item._idlePrev = list;//2 // The list _idleNext points to
tail (newest) and _idlePrev to head (oldest). list._idleNext._idlePrev =
item;//3 list._idleNext = item;//4 } function isEmpty(list) { return
list._idleNext === list; }
鏈表實(shí)例初始化了兩個(gè)指針,初始時(shí)均指向自己,_idlePrev指針將指向鏈表中最新添加進(jìn)來(lái)的元素,_idleNext
指向最新添加進(jìn)來(lái)的元素,實(shí)現(xiàn)的兩個(gè)主要操作為remove和append。鏈表的remove
操作非常簡(jiǎn)單,只需要將刪除項(xiàng)前后的元素指針加以調(diào)整,然后將被刪除項(xiàng)的指針置空即可,就像從一串鎖鏈中拿掉一節(jié),很形象。
源碼中的idlePrev和idleNext
很容易混淆,建議不用強(qiáng)行翻譯為“前后”或者“新舊”,(反復(fù)記憶N次都記不住我也很無(wú)奈),直接按對(duì)應(yīng)位置來(lái)記憶就可以了,愛(ài)翻譯成什么就翻譯成什么。
源碼中的鏈表實(shí)現(xiàn)并沒(méi)有提供指定位置插入的方法,append( )方法默認(rèn)只接收l(shuí)ist和item
兩個(gè)參數(shù),新元素會(huì)被默認(rèn)插入在鏈表的固定位置,這與它的使用方式有關(guān),所以沒(méi)必要實(shí)現(xiàn)完整的鏈表數(shù)據(jù)結(jié)構(gòu)。append
稍微復(fù)雜一些,但是源碼中也做了非常詳細(xì)的注釋。首先需要確保插入的元素是獨(dú)立的(也就是prev和next指針都為null
),然后再開(kāi)始調(diào)整,源碼中的鏈表是一個(gè)雙向循環(huán)鏈表,我們調(diào)整一下源碼的順序會(huì)更容易理解,其實(shí)插入一個(gè)元素就是要將各個(gè)元素的prev和next
兩個(gè)指針調(diào)整到位就可以了。先來(lái)看_idlePrev指針鏈的調(diào)整, 也就是指針調(diào)整代碼中標(biāo)記為2和3的語(yǔ)句:
item._idlePrev = list;//2 list._idleNext._idlePrev = item;//3
這里可以把list看作是一個(gè)prev指針連接起來(lái)的單向鏈表,相當(dāng)于將新元素item按照prev指針的指向添加到list和原本的list._idleNext
指向的元素中間,而1和4語(yǔ)句是調(diào)整了反方向的next指針鏈:
item._idleNext = list._idleNext; //1 list._idleNext = item;//4
調(diào)整后的鏈表以next指針為依據(jù)就可以形成反方向的循環(huán)鏈表,然后只需要記住list._idleNext指針指向的是最新添加的項(xiàng)就可以了。
如上圖所示,next和prev分別可以作為鏈表的邏輯順序形成循環(huán)鏈。
2.2 二叉堆
源碼放在lib/internal/priority_queue.js
<https://github.com/nodejs/node/blob/master/lib/internal/priority_queue.js>
中,一些博文也直接翻譯為優(yōu)先隊(duì)列,它們是抽象結(jié)構(gòu)和具體實(shí)現(xiàn)之間的關(guān)系,特性是一致的。二叉堆是一棵有序的完全二叉樹(shù),又以節(jié)點(diǎn)與其后裔節(jié)點(diǎn)的關(guān)系分為最大堆和最小堆。
完全二叉樹(shù)的特點(diǎn)使其可以很容易地轉(zhuǎn)化為一維數(shù)組來(lái)存儲(chǔ),且不需要二外記錄其父子關(guān)系,索引為i的節(jié)點(diǎn)的左右子節(jié)點(diǎn)對(duì)應(yīng)的索引為2i+1和2i+2
(當(dāng)然左右子節(jié)點(diǎn)也可能只有一個(gè)或都不存在)。Node.js就使用一維數(shù)組來(lái)模擬最小堆。源碼基本上就是這一數(shù)據(jù)結(jié)構(gòu)和“插入”,“刪除”這些基本操作的實(shí)現(xiàn)。
堆結(jié)構(gòu)的使用最主要的是為了獲得堆頂?shù)脑?,因?yàn)樗偸撬袛?shù)據(jù)里最大或最小的,同時(shí)堆
結(jié)構(gòu)是一個(gè)動(dòng)態(tài)調(diào)整的數(shù)據(jù)結(jié)構(gòu),插入操作時(shí)會(huì)將新節(jié)點(diǎn)插入到堆底,然后逐層檢測(cè)和父節(jié)點(diǎn)值的相對(duì)大小而“上浮”直到整個(gè)結(jié)構(gòu)重新變?yōu)槎?br>;進(jìn)行移除操作(移除堆頂元素也是移除操作的一種)時(shí),需要將堆尾元素置換到移除的位置,以維持整個(gè)數(shù)據(jù)結(jié)構(gòu)依然是一棵完全二叉樹(shù)
,然后通過(guò)與父節(jié)點(diǎn)和子節(jié)點(diǎn)進(jìn)行比較來(lái)決定該位置的元素應(yīng)該“上浮”或“下沉”,并遞歸這個(gè)過(guò)程直到整個(gè)數(shù)據(jù)結(jié)構(gòu)被重建為堆
。相關(guān)的文章非常,本文不再贅述(可以參考這篇博文【二叉堆的添加和刪除元素方法】
<https://blog.csdn.net/qq_19782019/article/details/78301832>,有動(dòng)畫(huà)好理解)。
三. 從setTimeout理解Timer模塊源碼
timer模塊并不需要手動(dòng)引入,它的源碼在/lib/timers.js
<https://github.com/nodejs/node/blob/master/lib/timers.js>目錄中,我們以這樣一段代碼來(lái)看看
setTimeout方法的執(zhí)行機(jī)制:
setTimeout(()=>{console.log(1)},1000); setTimeout(()=>{console.log(2)},500);
setTimeout(()=>{console.log(3)},1000);
3.1 timers.js中的定義
最上層方法的定義進(jìn)行了一些參數(shù)格式化,將除了回調(diào)函數(shù)和延遲時(shí)間以外的其他參數(shù)組成數(shù)組(應(yīng)該是用apply來(lái)執(zhí)行callback
方法時(shí)把這些參數(shù)傳進(jìn)去),接著做了三件事,生成timeout實(shí)例,激活實(shí)例,返回實(shí)例。
3.2 Timeout類(lèi)定義
Timeout類(lèi)定義在【lib/internal/timers.js】中:
初始化了一些屬性,可以看到傳入構(gòu)造函數(shù)的callback,after,args都被記錄下來(lái),可以看到after的最小值為1ms,Timeout
還定義了一些原型方法可以先不用管,然后調(diào)用了initAsyncResource( )這個(gè)方法,它在實(shí)例上添加了[async_id_symbol]和
[trigger_async_id_symbol]兩個(gè)標(biāo)記后,又調(diào)用了emitInit( )方法將這些參數(shù)均傳了進(jìn)去,這個(gè)emitInit( )方法來(lái)自于
/lib/internal/async_hooks.js,官方文檔對(duì)async_hook模塊的解釋是:
The?async_hooks?module provides an API to register callbacks tracking the
lifetime of asynchronous resources created inside a Node.js application.
它是一個(gè)實(shí)驗(yàn)性質(zhì)的API,是為了Node.js內(nèi)部創(chuàng)建的用于追蹤異步資源生命周期的模塊,所以推測(cè)這部分邏輯和執(zhí)行機(jī)制關(guān)系不大,可以先擱在一邊。
3.3 active(timeout)
獲得了timeout實(shí)例后再回到上層函數(shù)來(lái),接下來(lái)執(zhí)行的是active(timeout)這個(gè)方法,它調(diào)用的是insert( item, true,
getLibuvNow()),不難猜測(cè)最后這個(gè)方法就是從底層libuv中獲取一個(gè)準(zhǔn)確的當(dāng)前時(shí)間,insert方法的源碼如下:
首先為timeout實(shí)例添加了開(kāi)始執(zhí)行時(shí)間idleStart屬性,接下來(lái)的邏輯涉及到兩個(gè)對(duì)象,這里提前說(shuō)明一下:timerListMap
是一個(gè)哈希表,延時(shí)的毫秒數(shù)為key,其value是一個(gè)雙向鏈表,鏈表中存放著timeout實(shí)例,所以timerListMap
就相當(dāng)于一個(gè)按延時(shí)時(shí)間來(lái)分組存放定時(shí)器實(shí)例的Hash+linkedList結(jié)構(gòu),另一個(gè)重要對(duì)象timerListQueue
就是上面講過(guò)的優(yōu)先隊(duì)列(后文使用“二叉堆”這一概念)。
這里有一個(gè)小細(xì)節(jié),就是將新的定時(shí)器鏈表加入二叉堆時(shí),比較函數(shù)是自定義傳入的,在源碼中很容易看到compareTimersLists ( )這個(gè)方法使用鏈表的
expiry屬性的值進(jìn)行比較來(lái)得到最小堆,由此可以知道,堆頂?shù)逆湵砜偸莈xpiry最小的,也就是說(shuō)堆頂鏈表的__idlePrev
指向的定時(shí)器,就是所有定時(shí)器里下一個(gè)需要觸發(fā)回調(diào)的。
接下來(lái)再來(lái)看看active( )函數(shù)體的具體邏輯,如果有對(duì)應(yīng)鍵的鏈表則獲取到它(list
變量),如果沒(méi)有則生成一個(gè)新的空鏈表,然后將這個(gè)鏈表添加進(jìn)二叉堆,跳過(guò)中間的步驟,在最后可以看到執(zhí)行了:
L.append(list, item);
這個(gè)L實(shí)際上是來(lái)自于前文提過(guò)的linkedList.js中的方法,就是將timeout實(shí)例添加到list鏈表中,來(lái)個(gè)圖就很容易理解了:
中間我們跳過(guò)了一點(diǎn)邏輯,就是在新鏈表生成時(shí)執(zhí)行的:
if(nextExpiry > expiry){ scheduleTimer(msecs); nextExpiry = expiry; }
nextExpiry是timer模塊中維護(hù)的一個(gè)模塊內(nèi)的相對(duì)全局變量,這里的expiry是新鏈表的下一個(gè)定時(shí)器的過(guò)期時(shí)間(也就是新鏈表中唯一一個(gè)timeout
實(shí)例的過(guò)期時(shí)間),這里針對(duì)的情況就是新生成的定時(shí)器比已存在的所有定時(shí)器都要更早觸發(fā),這時(shí)就需要重新調(diào)度一下,并把當(dāng)前這個(gè)定時(shí)器的過(guò)期時(shí)間點(diǎn)設(shè)置為
nextExpiry時(shí)間。
這個(gè)scheduleTimer( )使用internalBinding('timers')引入的,在lib/timer.cc中找到這個(gè)方法:
void ScheduleTimer(const FunctionCallbackInfo<Value>& args) { auto env =
Environment::GetCurrent(args);
env->ScheduleTimer(args[0]->IntegerValue(env->context()).FromJust()); }
再跳到env.cc:
void Environment::ScheduleTimer(int64_t duration_ms) { if (started_cleanup_)
return; uv_timer_start(timer_handle(), RunTimers, duration_ms, 0); }
可以看到這里就將定時(shí)器的信息和libuv的事件循環(huán)聯(lián)系在一起了,libuv
還沒(méi)有研究,所以這條邏輯線(xiàn)暫時(shí)到此為止。再回到之前的示例,當(dāng)三個(gè)定時(shí)器都添加完成后,內(nèi)存中的對(duì)象關(guān)系基本是下面的樣子:
3.4 定時(shí)器的處理執(zhí)行邏輯
至此我們已經(jīng)將定時(shí)器的信息都存放好了,那么它是如何被觸發(fā)的呢?我們找到node.js的啟動(dòng)文件lib/internal/bootstrap/node.js
<https://github.com/nodejs/node/blob/master/lib/internal/bootstrap/node.js>
284-290行,可以看到,在啟動(dòng)函數(shù)中,Node.js通過(guò)調(diào)用setTimers( )方法將定時(shí)器處理函數(shù)processTimers
傳遞給了底層,它最終會(huì)被用來(lái)調(diào)度執(zhí)行定時(shí)器,processTimers方法由lib/internal/timers.js中提供的
getTimerCallbacks(runNextTicks) 方法運(yùn)行得到,所以聚焦到/lib/internal/timers.js中:
推測(cè)libuv每次需要檢查是否有定時(shí)器到期時(shí)都會(huì)運(yùn)行processTimers( )方法,來(lái)看一下對(duì)應(yīng)的邏輯,一個(gè)無(wú)限循環(huán)的while
語(yǔ)句,直到二叉堆的堆頂沒(méi)有任何定時(shí)器時(shí)跳出循環(huán)并返回0。在循環(huán)體內(nèi)部,會(huì)用堆頂元素的過(guò)期時(shí)間和當(dāng)前時(shí)間相比,如果list.expiry
更大,說(shuō)明時(shí)機(jī)未到還不需要執(zhí)行,把它的過(guò)期時(shí)間賦值給nextExpiry然后返回(返回邏輯先不細(xì)究)。如果邏輯執(zhí)行到471行,說(shuō)明堆頂元素的過(guò)期時(shí)間已經(jīng)過(guò)了,
ranAtLeastOneList這個(gè)標(biāo)記位使得這段邏輯按照如下方式運(yùn)行:
1.獲取到一個(gè)expiry已經(jīng)過(guò)期的鏈表,首次向下執(zhí)行時(shí)`ranAtLeastOneList`為false,則將其置為true,然后執(zhí)行`listOnTimeout()`這個(gè)方法;
2.然后繼續(xù)取堆頂?shù)逆湵恚绻策^(guò)期了,再次執(zhí)行時(shí),會(huì)先執(zhí)行`runNextTicks()`,再執(zhí)行`listOnTimeout()`。
我們按照邏輯順序,先來(lái)看看listOnTimeout( )這個(gè)方法,它有近100行(我們以上面3個(gè)定時(shí)器的實(shí)例來(lái)看看它的執(zhí)行邏輯):
function listOnTimeout(list, now) { const msecs = list.msecs; //500 ,
500ms的鏈表在堆頂 debug('timeout callback %d', msecs); var diff, timer; let
ranAtLeastOneTimer = false; while (timer = L.peek(list)) {
//取鏈表_idlePrev指向的定時(shí)器,也就是鏈表中最先到期的 diff = now - timer._idleStart;
//計(jì)算當(dāng)前時(shí)間和它開(kāi)始計(jì)時(shí)那個(gè)時(shí)間點(diǎn)的時(shí)間差, // Check if this loop iteration is too early for the
next timer. // This happens if there are more timers scheduled for later in the
list. // 原文翻譯:檢測(cè)當(dāng)前事件循環(huán)對(duì)于下一個(gè)定時(shí)器是否過(guò)早,這種情況會(huì)在鏈表中還有其他定時(shí)器時(shí)發(fā)生。 //
人話(huà)翻譯:就是當(dāng)前的時(shí)間點(diǎn)只需要觸發(fā)鏈表中第一個(gè)500ms定時(shí)器,下一個(gè)500ms定時(shí)器還沒(méi)到觸發(fā)時(shí)間。 //
極端的相反情況就是由于阻塞時(shí)間已經(jīng)過(guò)去很久了,鏈表里的N個(gè)定時(shí)器全都過(guò)期了,都得執(zhí)行。 if (diff < msecs) {
//更新鏈表中下一個(gè)到期定時(shí)器的時(shí)間記錄,計(jì)算邏輯稍微有點(diǎn)繞 list.expiry = Math.max(timer._idleStart + msecs,
now + 1); list.id = timerListId++;
timerListQueue.percolateDown(1);//堆頂元素值發(fā)生更新,需要通過(guò)“下沉”來(lái)重構(gòu)“堆” debug('%d list wait
because diff is %d', msecs, diff); return; //直接結(jié)束了 } //是不是貌似見(jiàn)過(guò)這段,先放著等會(huì)一塊說(shuō) if
(ranAtLeastOneTimer) runNextTicks(); else ranAtLeastOneTimer = true; // The
actual logic for when a timeout happens. L.remove(timer); const asyncId =
timer[async_id_symbol]; if (!timer._onTimeout) { if (timer[kRefed]) refCount--;
timer[kRefed] = null; if (destroyHooksExist() && !timer._destroyed) {
emitDestroy(asyncId); timer._destroyed = true; } continue; }
emitBefore(asyncId, timer[trigger_async_id_symbol]); let start; if
(timer._repeat) //這部分看起來(lái)應(yīng)該是interval的邏輯,interval底層實(shí)際上就是一個(gè)重復(fù)的timeout start =
getLibuvNow(); try { const args = timer._timerArgs; if (args === undefined)
timer._onTimeout(); //設(shè)置定時(shí)器時(shí)傳入的回調(diào)函數(shù)被執(zhí)行了 else timer._onTimeout(...args); }
finally { if (timer._repeat && timer._idleTimeout !== -1) { timer._idleTimeout
= timer._repeat; if (start === undefined) start = getLibuvNow(); insert(timer,
timer[kRefed], start);//interval的真實(shí)執(zhí)行邏輯,重新獲取時(shí)間然后插入到鏈表中 } else if
(!timer._idleNext && !timer._idlePrev) { if (timer[kRefed]) refCount--;
timer[kRefed] = null; if (destroyHooksExist() && !timer._destroyed) {
emitDestroy(timer[async_id_symbol]); timer._destroyed = true; } } }
emitAfter(asyncId); } //這塊需要注意的是,上面整個(gè)邏輯都包在while(timer = L.peek(list)){...}里面 //
If `L.peek(list)` returned nothing, the list was either empty or we have //
called all of the timer timeouts. // As such, we can remove the list from the
object map and // the PriorityQueue. debug('%d list empty', msecs); // The
current list may have been removed and recreated since the reference // to
`list` was created. Make sure they're the same instance of the list // before
destroying. // 原文翻譯:當(dāng)前的list標(biāo)識(shí)符所引用的list有可能已經(jīng)經(jīng)過(guò)了重建,刪除前需要確保它指向哈希表中的同一個(gè)實(shí)例。 if (list
=== timerListMap[msecs]) { delete timerListMap[msecs]; timerListQueue.shift();
} }
3.5 實(shí)例分析
代碼邏輯因?yàn)榘撕芏鄺l件分支,所以不容易理解,我們以前文的實(shí)例作為線(xiàn)索來(lái)看看定時(shí)器觸發(fā)時(shí)的執(zhí)行邏輯:
程序啟動(dòng)后,processTimer( )方法不斷執(zhí)行,第一個(gè)過(guò)期的定時(shí)器會(huì)在堆頂?shù)?00ms定時(shí)器鏈表(下面稱(chēng)為500鏈表)中產(chǎn)生,過(guò)期時(shí)間戳為511。
假設(shè)時(shí)間戳到達(dá)600時(shí)程序再次執(zhí)行processTimer( ),此時(shí)發(fā)現(xiàn)當(dāng)前時(shí)間已經(jīng)超過(guò)nextExpiry記錄的時(shí)間戳511,于是繼續(xù)向下執(zhí)行進(jìn)入
listOnTimeout(list, now),這里的list就是哈希表中鍵為500的鏈表,now就是當(dāng)前時(shí)間600,進(jìn)入listOnTimeout
方法后,獲取到鏈表中最早的一個(gè)定時(shí)器timer,然后計(jì)算diff參數(shù)為600-11=589, 589 > 500, 于是繞過(guò)條件分支語(yǔ)句,
ranAtLeastOneTimer為false也跳過(guò)(跳過(guò)后其值為true),接下來(lái)的邏輯從鏈表中刪除了這個(gè)timer,然后執(zhí)行
timer._onTimeout指向的回調(diào)函數(shù),500鏈表只有一個(gè)定時(shí)器,所以下一循環(huán)時(shí)L.peek(list)返回null,循環(huán)語(yǔ)句跳出,繼續(xù)向后執(zhí)行。此時(shí)
list依然指向500鏈表,于是執(zhí)行刪除邏輯,從哈希表和二叉堆中均移除500鏈表,兩個(gè)數(shù)據(jù)結(jié)構(gòu)在底層會(huì)進(jìn)行自調(diào)整。
processTimer( )再次執(zhí)行時(shí),堆頂?shù)逆湵碜兂闪?000ms定時(shí)器鏈表(下面稱(chēng)為1000鏈表),nextExpiry賦值為list.expiry
,也就是1001,表示1000ms定時(shí)器鏈表中下一個(gè)到期的定時(shí)器會(huì)在時(shí)間戳超過(guò)1001時(shí)過(guò)期,但時(shí)間還沒(méi)有到。下面分兩種情況來(lái)分析:
1.時(shí)間戳為1010時(shí)執(zhí)行processTimer( )
時(shí)間來(lái)到1010點(diǎn),processTimer( )被執(zhí)行,當(dāng)前時(shí)間1010大于nextExpiry=1001,于是從堆頂獲取到1000鏈表,進(jìn)入
listOnTimeout( ),第一輪while循環(huán)執(zhí)行時(shí)的情形和500鏈表執(zhí)行時(shí)是一致的,在第二輪循環(huán)中,timer指向1000鏈表中后添加的那個(gè)定時(shí)器,
diff的值為 1010 - 21 = 989,989 < 1000 ,所以進(jìn)入if(diff < msecs)的條件分支,list.expiry調(diào)整為下一個(gè)
timer的過(guò)期時(shí)間1021,然后通過(guò)下沉來(lái)重建二叉堆(堆頂元素的expiry
發(fā)生了變化),上面的實(shí)例中只剩了唯一一個(gè)鏈表,所以下沉操作沒(méi)有引發(fā)什么實(shí)質(zhì)影響,接著退出當(dāng)前函數(shù)回到processTimer的循環(huán)體中,接著
processTimer
里的while循環(huán)繼續(xù)執(zhí)行,再次檢查棧頂元素,時(shí)間還沒(méi)到,然后退出,等時(shí)間超過(guò)下一個(gè)過(guò)期時(shí)間1021后,最后一個(gè)定時(shí)器被觸發(fā),過(guò)程基本一致,只是鏈表耗盡后會(huì)觸發(fā)
listOnTimeout后面的清除哈希表和二叉堆中相關(guān)記錄的邏輯。
總結(jié)一下,鏈表的消耗邏輯是,從鏈表中不斷取出peek位置的定時(shí)器,如果過(guò)期了就執(zhí)行,如果取到一個(gè)沒(méi)過(guò)期的,說(shuō)明鏈表里后續(xù)的都沒(méi)過(guò)期,就修改鏈表上的
list.expiry屬性然后退回到processTimer的循環(huán)體里,如果鏈表耗盡了,就將它從哈希表和二叉堆中把這個(gè)鏈表刪掉。
2.時(shí)間戳為1050時(shí)執(zhí)行processTimer( )
假如程序因?yàn)槠渌蛑钡綍r(shí)間為1050時(shí)才開(kāi)始檢查1000鏈表,此時(shí)它的兩個(gè)定時(shí)器都過(guò)期了需要被觸發(fā),listOnTimeout( )
中的循環(huán)語(yǔ)句執(zhí)行第一輪時(shí)是一樣的,第二次循環(huán)執(zhí)行時(shí),跳過(guò)if(diff < msecs)的分支,然后由于ranAtLeastOneTimer
標(biāo)記位的變化,除了第一個(gè)定時(shí)器的回調(diào)外,其他都會(huì)先執(zhí)行runNextTicks( )然后再執(zhí)行定時(shí)器上綁的回調(diào),等到鏈表耗盡后,進(jìn)入后續(xù)的清除邏輯。
我們?cè)賮?lái)看一種更極端的情況,假如程序一直阻塞到時(shí)間戳為2000時(shí)才執(zhí)行到processTimer
(此時(shí)3個(gè)定時(shí)器都過(guò)期了,2個(gè)延遲1000ms,1個(gè)延遲500ms,堆頂為500ms鏈表),此時(shí)processTimer( )
先進(jìn)入第一次循環(huán),處理500鏈表,然后500鏈表中唯一的定時(shí)器處理完后,邏輯回到processTimer
的循環(huán)體,再進(jìn)行第二輪循環(huán),此時(shí)獲取到堆頂?shù)?000鏈表,發(fā)現(xiàn)仍然需要執(zhí)行,那么就會(huì)先執(zhí)行runNextTicks( )
,然后在處理1000鏈表,后面的邏輯就和上面時(shí)間戳為1050時(shí)執(zhí)行processTimer基本一致了。
至此定時(shí)器的常規(guī)邏輯已經(jīng)解析完了,還有兩個(gè)細(xì)節(jié)需要提一下,首先是runNextTicks( ),從名字可以推測(cè)它應(yīng)該是執(zhí)行通過(guò)
process.nextTick( )添加的函數(shù),從這里的實(shí)現(xiàn)邏輯來(lái)看,當(dāng)有多個(gè)定時(shí)器需要觸發(fā)時(shí),每個(gè)間隙都會(huì)去消耗nextTicks
隊(duì)列中的待執(zhí)行函數(shù),以保證它可以起到“盡可能早地執(zhí)行”的職責(zé),對(duì)此不了解的讀者可以參考上一篇博文
【譯】Node.js中的事件循環(huán),定時(shí)器和process.nextTick
<https://www.cnblogs.com/dashnowords/p/11042623.html>。
四. 小結(jié)
timer模塊比較大,在了解基本數(shù)據(jù)結(jié)構(gòu)的前提下不算特別難理解,setImmediate( )和process.nextTick( )
的實(shí)現(xiàn)感興趣的讀者可以自行學(xué)習(xí),想要對(duì)事件循環(huán)機(jī)制有更深入的理解,需要學(xué)習(xí)C++和libuv的相關(guān)原理,筆者尚未深入涉獵,以后有機(jī)會(huì)再寫(xiě)。
熱門(mén)工具 換一換

感谢您访问我们的网站,您可能还对以下资源感兴趣:
调教肉文小说-国产成本人片免费av-空姐av种子无码-在线观看免费午夜视频-综合久久精品激情-国产成人丝袜视频在线观看软件-大芭区三区四区无码-啊啊好爽啊啊插啊用力啊啊-wanch视频网-国产精品成人a免费观看