前言
根據(jù)上一篇文章,我們可知球订,node對(duì)回調(diào)事件的處理完全是基于事件循環(huán)的tick的后裸,因此具有幾大特征:
1、在應(yīng)用層面冒滩,JS是單線程的微驶,業(yè)務(wù)代碼中不能存在耗時(shí)過長(zhǎng)的代碼,否則可能會(huì)嚴(yán)重拖后續(xù)代碼(包括回調(diào))的處理开睡。如果遇到需要復(fù)雜的業(yè)務(wù)計(jì)算時(shí)因苹,應(yīng)當(dāng)想辦法啟用獨(dú)立進(jìn)程或交給其他服務(wù)進(jìn)行處理。
2篇恒、回調(diào)是不精確容燕,因?yàn)榍懊娴脑颍瑂etTimeout并不能得到準(zhǔn)確的超時(shí)回調(diào)婚度。
3蘸秘、不同類型的觀察者,處理的優(yōu)先級(jí)不同蝗茁,idle觀察者最先醋虏,I/O觀察者其次,check觀察者最后哮翘。
那么本文主要要分析的是基于tick的幾個(gè)主要回調(diào)實(shí)現(xiàn)颈嚼,setTimeout/setInterval/process.nextTick/setImmediate,這幾個(gè)屬于js異步回調(diào)的比較特殊的饭寺,因?yàn)樗麄儾⒉皇窍衿胀↖/O操作那樣真的需要等待事件異步處理結(jié)束再進(jìn)行回調(diào)阻课,而是出于定時(shí)或延遲處理的原因才設(shè)計(jì)的。分析起來相對(duì)簡(jiǎn)單艰匙,因此我們就從它們?nèi)胧窒奚罚鸩浇议_事件循環(huán)的秘密。
區(qū)別及源碼分析
◇setTimeout/setInterval
setTimeout和setInterval的表現(xiàn)和實(shí)現(xiàn)其實(shí)基本相同员凝,不同的只是setInterval會(huì)不斷重復(fù)署驻。在底層實(shí)現(xiàn)上他們是創(chuàng)建了一個(gè)Timeout的中間對(duì)象,并且放到了實(shí)現(xiàn)定時(shí)器的紅黑樹中,每一次tick開始時(shí)旺上,都會(huì)到這個(gè)紅黑樹中檢查是否存在超時(shí)的回調(diào)瓶蚂,如果存在,則一一按照超時(shí)順序取出來進(jìn)行回調(diào)宣吱。因此窃这,我們可以得出這樣一個(gè)結(jié)論:
js的定時(shí)器是不可靠的。因此單線程的原因征候,它是基于tick的钦听,每次tick開始時(shí)才開始檢查是否有超時(shí),如果一個(gè)tick耗時(shí)過長(zhǎng)倍奢,在它之后出發(fā)的定時(shí)回調(diào)都將被延遲朴上。因此才會(huì)出現(xiàn)像“問題1”這樣的情況。
setTimeout第二個(gè)參數(shù)設(shè)置為0或者不設(shè)置卒煞,意思不是立即執(zhí)行回調(diào)痪宰,而是在下次tick時(shí)立即執(zhí)行(當(dāng)然,實(shí)際上畔裕,這里有點(diǎn)小問題衣撬,后面會(huì)講到)!這setTimeout也解釋了Promise的實(shí)現(xiàn)中扮饶,resolve方法里為什么有些要用setTimeout(..., 0)具练,這是為了解決在碰到同步代碼時(shí),resolve先于then執(zhí)行的問題甜无。但是它有一個(gè)嚴(yán)重的問題扛点,就是回調(diào)依然被送入定時(shí)器的紅黑樹,存在一定的性能問題岂丘。因此陵究,通常大家會(huì)用process.nextTick()或setImmediate()來替代它。
這里先創(chuàng)建了一個(gè)Timeout對(duì)象奥帘,然后調(diào)用active函數(shù)使他生效
這里調(diào)用insert方法把當(dāng)前Timeout對(duì)象插入到了一個(gè)地方
這個(gè)insert方法比較有意思铜邮,list是一個(gè)Timer對(duì)象,通過調(diào)用它的start方法可以使定時(shí)器生效寨蹋,同時(shí)它又是個(gè)雙向鏈表松蒜,這iterm就是被插入到了這個(gè)雙向鏈表中。這是為什么已旧?
其實(shí)秸苗,代碼里面已經(jīng)給出了解釋
原來因?yàn)閷?shí)際開發(fā)過程中,經(jīng)常會(huì)出現(xiàn)很多的socket會(huì)被設(shè)置為相同的超時(shí)時(shí)間评姨,如果為每一個(gè)這樣的超時(shí)請(qǐng)求都設(shè)置一個(gè)watcher难述,那就太浪費(fèi)系統(tǒng)資源了萤晴,系統(tǒng)負(fù)載也會(huì)變得很高吐句,性能變差胁后。因?yàn)椋@里用了一個(gè)非常巧妙的方法嗦枢,那就是把超時(shí)時(shí)間相同的Timeout對(duì)象都扔到同一個(gè)鏈表中攀芯,然后再把這個(gè)Timer鏈表作為一個(gè)獨(dú)立的超時(shí)單位啟動(dòng)。
這里調(diào)用了uv_timer_start(不同系統(tǒng)實(shí)現(xiàn)方式不同文虏,這里的源碼是unix的)
原來這個(gè)uv_timer_start其實(shí)主要就是把這個(gè)Timer對(duì)象插入到了一顆紅黑樹上侣诺。
如果還記得我上文對(duì)事件循環(huán)的代碼分析的話,你一定會(huì)注意在事件循環(huán)的while中氧秘,有uv__run_timers這一行年鸳,通過上面這段代碼,就能看出來這個(gè)uv__run_timers就是從紅黑樹上取下所有超時(shí)的Timer對(duì)象丸相,然后依次調(diào)用他們的回調(diào)方法進(jìn)行回調(diào)搔确。
◇process.nextTick
實(shí)際上,process.nextTick()方法的操作相對(duì)較為輕量灭忠,每次調(diào)用Process.nextTick()方法膳算,只會(huì)將回調(diào)函數(shù)放入隊(duì)列中,在下一輪Tick時(shí)取出執(zhí)行弛作。定時(shí)器采用紅黑樹的操作時(shí)間復(fù)雜度為o(lg(n))涕蜂,而nextTick()的時(shí)間復(fù)雜度為o(1)。相較之下映琳,process.nextTick()更高效机隙。
由以上代碼可知,nextTick函數(shù)萨西,會(huì)將callback封裝為一個(gè)obj對(duì)象黍瞧,并且插入到nextTickQueue隊(duì)列(數(shù)組)中。
由上述代碼可知原杂,每次nextTick回調(diào)印颤,都會(huì)nextTickQueue數(shù)組中的回調(diào)全部跑完!
◇setImmediate
setImmediate函數(shù)穿肄,首先把callback封裝成了一個(gè)immediate對(duì)象年局,然后把它插入到了immediateQueue隊(duì)列(數(shù)組)中。
注意上面的那句process._immediateCallback = processImmediate咸产,這行代碼就是把process._immediateCallback設(shè)置成了processImmediate的別名矢否,下次tick的時(shí)候就會(huì)調(diào)用這個(gè)函數(shù)進(jìn)行回調(diào)。
setImmediate()方法和process.nextTick()方法十分類似脑溢,都是將回調(diào)函數(shù)延遲在下一次立即執(zhí)行僵朗。setImmediate是創(chuàng)建了一個(gè)叫為immediate的中間對(duì)象赖欣,并且放入到了immediateQueue隊(duì)列中在Node v0.9.1之前,setImmediate()還沒有實(shí)現(xiàn)验庙,那時(shí)候?qū)崿F(xiàn)類似的功能主要是通過process.nextTick()來完成顶吮。
但兩者之間其實(shí)是有差別的。區(qū)別表現(xiàn)為兩點(diǎn):
1粪薛、process.nextTick中回調(diào)函數(shù)的優(yōu)先級(jí)高于setImmediate悴了,根據(jù)我前面寫的那篇文章可知,原因在于事件循環(huán)對(duì)觀察者的檢查是有先后順序的违寿,process.nextTick屬于idle觀察者湃交,setImmediate屬于check觀察者。在每一輪循環(huán)檢查中藤巢,idle觀察者先于I/O觀察者搞莺,I/O觀察者先于check觀察者。
而且掂咒,這里最有意思的是下面這段代碼的執(zhí)行結(jié)果才沧,大家以為會(huì)是什么樣的輸出?
他的實(shí)際輸出是:
nextTick 1
nextTick 2
timeout
immediate
上面代碼中表明俏扩,由于process.nextTick方法指定的回調(diào)函數(shù)糜工,總是在當(dāng)前"執(zhí)行棧"的尾部觸發(fā),所以不僅函數(shù)A比setTimeout指定的回調(diào)函數(shù)timeout先執(zhí)行录淡,而且函數(shù)B也比timeout先執(zhí)行捌木。這說明,如果有多個(gè)process.nextTick語(yǔ)句(不管它們是否嵌套)嫉戚,將全部在當(dāng)前"執(zhí)行棧"執(zhí)行刨裆。這里具體為什么這樣,其實(shí)我現(xiàn)在也搞不懂彬檀,以后有機(jī)會(huì)可以慢慢在讀讀代碼帆啃,如果有知道的朋友,可以告訴我一下窍帝,謝謝了努潘。
我們由此得到了一個(gè)重要區(qū)別:多個(gè)process.nextTick語(yǔ)句總是一次執(zhí)行完,多個(gè)setImmediate則需要多次才能執(zhí)行完坤学。事實(shí)上疯坤,這正是Node.js 10.0版添加setImmediate方法的原因,否則像下面這樣的遞歸調(diào)用process.nextTick深浮,將會(huì)沒完沒了压怠,主線程根本不會(huì)去讀取"事件隊(duì)列"封孙!
由于process.nextTick指定的回調(diào)函數(shù)是在本次"事件循環(huán)"觸發(fā)崇败,而setImmediate指定的是在下次"事件循環(huán)"觸發(fā)澎语,所以很顯然坑匠,前者總是比后者發(fā)生得早,而且執(zhí)行效率也高(因?yàn)椴挥脵z查"任務(wù)隊(duì)列")雨让。
2雇盖、在實(shí)現(xiàn)上,process.nextTick的回調(diào)函數(shù)保存在一個(gè)數(shù)組中宫患,setImmediate則保存在一個(gè)鏈表中刊懈。順便這里拋出一個(gè)樸靈老師在《深入淺出Node.js》中對(duì)process.nextTick和setImmediate的不夠準(zhǔn)確的描述:“在行為上这弧,process.nextTick在每輪循環(huán)中將數(shù)組中的回調(diào)函數(shù)全部執(zhí)行完娃闲,而setImmediate在每輪循環(huán)中執(zhí)行鏈表中的一個(gè)回調(diào)函數(shù)∝依耍” 并且用了一段代碼進(jìn)行作證:
樸靈老師書里面說的結(jié)果是:
正常執(zhí)行
nextTick延遲執(zhí)行1
nextTick延遲執(zhí)行2
setImmediate延遲執(zhí)行1
強(qiáng)勢(shì)插入
setImmediate延遲執(zhí)行2
但我跑出來的真實(shí)結(jié)果卻是:
正常執(zhí)行
nextTick延遲執(zhí)行1
nextTick延遲執(zhí)行2
setImmediate延遲執(zhí)行1
setImmediate延遲執(zhí)行2
強(qiáng)勢(shì)插入
我相信樸老師一定是驗(yàn)證過那段代碼的皇帮,也就是說當(dāng)時(shí)他測(cè)試應(yīng)該是正確的。為了印證為什么我測(cè)試的結(jié)果為什么跟樸老師給的結(jié)果存在差異蛋辈,我做了兩件事情属拾,一是在不同的node版本下運(yùn)行這段代碼(樸老師寫那本書的時(shí)候,node最新版本為0.10.13冷溶,而我的版本是4.2.4)渐白,二是去翻閱node的源碼實(shí)現(xiàn),通過底層原理來描述這件事情逞频。
首先纯衍,我測(cè)試了在不同版本下node運(yùn)行的差異:
通過這個(gè)測(cè)試,我們可以發(fā)現(xiàn)苗胀,從設(shè)計(jì)邏輯出發(fā)襟诸,setImmediate每次只執(zhí)行鏈表中的一個(gè)回調(diào)應(yīng)該是早期node版本中是一個(gè)bug,這在后面的版本中修復(fù)了基协。所以歌亲,才出現(xiàn)了樸老師的書里描述的結(jié)果跟實(shí)際測(cè)試的不同的現(xiàn)象。
然后澜驮,我分別對(duì)比了node在這兩個(gè)版本下的代碼的差異:
0.10.13版本的
根據(jù)以上代碼可知陷揪,在0.10.13的代碼中,每次tick處理immediate時(shí)杂穷,只會(huì)取一個(gè)回調(diào)出來進(jìn)行處理
4.x版本的
根據(jù)以上代碼可知悍缠,在4.x版本的代碼中,每次tick處理immediate時(shí)亭畜,會(huì)使用while循環(huán)扮休,把所有的immediate回調(diào)取出來依次進(jìn)行處理。
3拴鸵、setImmediate可以使用clearImmediate清除(沒搞懂這個(gè)到底能干嗎玷坠,誰(shuí)明白請(qǐng)告訴我一下)蜗搔,process.nextTick不能被清除
觀察者優(yōu)先級(jí)
在每次輪訓(xùn)檢查中,各觀察者的優(yōu)先級(jí)分別是:
idle觀察者 > I/O觀察者 > check觀察者八堡。
idle觀察者:process.nextTick
I/O觀察者:一般性的I/O回調(diào)樟凄,如網(wǎng)絡(luò),文件兄渺,數(shù)據(jù)庫(kù)I/O等
check觀察者:setImmediate缝龄,setTimeout
上面的結(jié)果顯示timeout1甚至優(yōu)于immediate執(zhí)行,原因應(yīng)該在于距離下次tick啟動(dòng)至檢查定時(shí)器的時(shí)間超過了10ms挂谍,因此timeout1那個(gè)時(shí)候其實(shí)已經(jīng)超時(shí)了叔壤。
說到這里,順便談個(gè)問題口叙。知乎上曾有人貼過一段關(guān)于setImmediate和setTimeout(xxx,0)的代碼炼绘,得出了一個(gè)這樣的結(jié)論:“而在執(zhí)行setImmedia時(shí),setTimeout是隨機(jī)的插入在setImmediate的順序中的”妄田。我對(duì)這個(gè)結(jié)論是持懷疑態(tài)度的俺亮,一個(gè)像node這樣穩(wěn)定健壯的系統(tǒng)是不太可能允許這種不可控的隨機(jī)性的,我們回過頭去看前面的代碼疟呐,發(fā)現(xiàn)了這樣一行:
意思很明顯脚曾,如果沒有設(shè)置這個(gè)after,或者小于1启具,或者大于TIMEOUT_MAX(2^31-1)本讥,都會(huì)被強(qiáng)制設(shè)置為1ms。也就是說setTimeout(xxx,0)其實(shí)等同于setTimeout(xxx,1)富纸。
那就很容易理解知乎這位作者的給出的代碼為什么是這樣的結(jié)果了囤踩。因此:setTimeout的優(yōu)先級(jí)高于setImmediate,但是因?yàn)閟etTimeout的after被強(qiáng)制修正為1晓褪,這就可能存在下一個(gè)tick觸發(fā)時(shí)堵漱,耗時(shí)尚不足1ms,setTimeout的回調(diào)依然未超時(shí)涣仿,因此setImmediate就先執(zhí)行了勤庐!這可以通過在本次tick中加入一段耗時(shí)較長(zhǎng)的代碼來來保證本次tick耗時(shí)必須超過1ms來檢測(cè):
測(cè)試顯示:不論運(yùn)行多少次,得出的結(jié)果都一樣好港,都是如下:
由此可知愉镰,setTimeout是優(yōu)先于setImmediate被處理的。
總結(jié)
要想真正理解很多why的問題钧汹,光看大量的案例和看文字解釋其實(shí)還是很難理解的丈探,死記硬背也比較難記住。最好的方法還是通過閱讀底層代碼實(shí)現(xiàn)拔莱,并思考為什么這樣設(shè)計(jì)碗降,應(yīng)該就會(huì)好很多隘竭。這些代碼分析并不完整,我個(gè)人的理解也不是非常深入讼渊,很多地方地方可能都沒有講清楚动看。以后應(yīng)該還會(huì)有更多的文章出來進(jìn)行分析。
通過上面的分析爪幻,我也簡(jiǎn)單給出幾個(gè)結(jié)論:
優(yōu)先級(jí)順序:process.nextTick > setTimeout/setInterval > setImmediate
setTimeout需要使用紅黑樹菱皆,且after設(shè)置為0,其實(shí)會(huì)被node強(qiáng)制轉(zhuǎn)換為1挨稿,存在性能上的問題仇轻,建議替換為setImmediate
process.nextTick有一些比較難懂的問題和隱患,從0.8版本開始加入setImmediate叶组,使用時(shí)拯田,建議使用setImmediate