淺談Node.js中的定時器
Node.js中定時器的實現(xiàn)
上一篇博文提到,在Node中timer并不是通過新開線程來實現(xiàn)的,而是直接在event loop中完成。下面通過幾個JavaScript的定時器示例以及Node相關(guān)源碼來分析在Node中,timer功能到底是怎么實現(xiàn)的。
JavaScript中定時器功能的特點
無論是Node還是瀏覽器中,都有setTimeout和setInterval這兩個定時器函數(shù),并且其工作特點基本相同,因此下面僅以Node為例進行分析。
我們知道,JavaScript中的定時器并不同于計算機底層的定時中斷。中斷到來時,當前執(zhí)行代碼會被打斷,轉(zhuǎn)去執(zhí)行定時中斷處理函數(shù)。而JavaScript的定時器到時,如果當前執(zhí)行線程沒有正在執(zhí)行的代碼,則執(zhí)行相應(yīng)的回調(diào)函數(shù);如果當前有代碼在執(zhí)行中,JavaScript引擎既不會中斷當前代碼轉(zhuǎn)去執(zhí)行回調(diào),也不會開新的線程執(zhí)行回調(diào),而是當前代碼執(zhí)行完畢之后才去處理。
console.time('A') setTimeout(function () { console.timeEnd('A'); }, 100); var i = 0; for (; i < 100000; i++) { }
執(zhí)行上面的代碼,可以看到最終輸出的時間并不是100ms左右,而是數(shù)秒。這說明在循環(huán)完成之前,定時回調(diào)函數(shù)確實沒有被執(zhí)行,而是推遲到了循環(huán)結(jié)束。實際上在JavaScript代碼執(zhí)行中,所有的事件都無法得到處理,必須等到當前代碼全部完成,才能去處理新的事件。這就是為什么在瀏覽器中運行耗時JavaScript代碼時,瀏覽器會失去響應(yīng)。為了應(yīng)對這種情況,我們可以采取Yielding Processes的技巧,將耗時的代碼分成小塊(chunks),每處理完一塊就執(zhí)行一次setTimeout,約定在一小段時間后才處理下一塊,而在這段空閑時間里,瀏覽器/Node可以去處理排隊中的事件。
補充資料
在JavaScript 高級程序設(shè)計 第三版第22章高級技巧中對高級定時器以及Yielding Processes有較詳細的討論。
Node中的timer實現(xiàn)
libuv對uv_loop_t類型的初始化
上一篇博文提到Node會調(diào)用libuv的uv_run函數(shù)啟動default_loop_ptr進行事件調(diào)度,default_loop_ptr指向一個uv_loop_t類型的變量default_loop_struct。Node啟動時會調(diào)用uv_loop_init(&default_loop_struct)對其進行初始化,uv_loop_init函數(shù)節(jié)選如下:
int uv_loop_init(uv_loop_t* loop) { ... loop->time = 0; uv_update_time(loop); ... }
可以看到loop的time字段先被賦值為0,之后調(diào)用uv_update_time函數(shù),這會將最新的計數(shù)時間賦給loop.time。
初始化完成之后,default_loop_struct.time就有了一個初始值,與時間有關(guān)的操作都會與此值進行比較從而確定是否調(diào)用相應(yīng)回調(diào)函數(shù)。
libuv的事件調(diào)度核心
前面提到uv_run函數(shù)就是libuv庫實現(xiàn)event loop的核心部分,下面是其流程圖:
這里簡述一下上面與定時器相關(guān)的邏輯:
更新當前l(fā)oop的time字段,這個字段標志著當前l(fā)oop概念下的“現(xiàn)在”;
檢查loop是否alive,也就是說檢查loop中是否還有需要處理的任務(wù)(handlers/requests),如果沒有就不必循環(huán)了;
檢查注冊過的timer,如果某一個timer中指定的時間落后于當前時間了,說明該timer已到時,于是執(zhí)行其對應(yīng)的回調(diào)函數(shù);
執(zhí)行一次I/O polling(即阻塞住線程,等待I/O事件發(fā)生),如果在下一個timer到期時還沒有任何I/O完成,則停止等待,執(zhí)行下一個timer的回調(diào)。
如果發(fā)生了I/O事件,則執(zhí)行對應(yīng)的回調(diào);由于執(zhí)行回調(diào)的時間里可能又有timer到期了,這里要再次檢查timer并執(zhí)行回調(diào)。
(實際上(4.)這里比較復(fù)雜,不僅僅是一步操作,這樣描述僅是為了不涉及其他細節(jié),而專注于timer的實現(xiàn)。)
Node會一直調(diào)用uv_run直到loop不再alive。
Node中的timer_wrap與timers
Node中有一個TimerWrap類,被注冊為Node內(nèi)部的timer_wrap模塊。
NODE_MODULE_CONTEXT_AWARE_BUILTIN(timer_wrap, node::TimerWrap::Initialize)
其中TimerWrap類基本上就是對uv_timer_t的一個直接封裝,NODE_MODULE_CONTEXT_AWARE_BUILTIN是Node用于注冊built-in模塊的宏。
經(jīng)過這一步操作,JavaScript就可以拿到這個模塊進行操作了。src/lib/timers.js文件使用JavaScript的形式把timer_wrap的功能封裝起來,并導(dǎo)出了exports.setTimeout, exports.setInterval, exports.setImmediate等函數(shù)。
Node啟動與global初始化
上一篇提到Node啟動時會載入執(zhí)行環(huán)境LoadEnvironment(env),這個函數(shù)中非常重要的一步就是載入src/node.js并執(zhí)行,src/node.js會載入指定的模塊并初始化global和process。當然,setTimeout等函數(shù)也會被src/node.js綁定到global對象上。
以上所述就是本文的全部內(nèi)容了,希望大家能夠喜歡。
相關(guān)文章
解決Node.js使用MySQL出現(xiàn)connect ECONNREFUSED 127.0.0.1:3306的問題
這篇文章主要介紹了解決Node.js使用MySQL出現(xiàn)connect ECONNREFUSED 127.0.0.1:3306報錯的相關(guān)資料,文中將問題描述的很清楚,解決的方法也介紹的很完整,需要的朋友可以參考借鑒,下面來一起看看吧。2017-03-03Node使用koa2實現(xiàn)一個簡單JWT鑒權(quán)的方法
這篇文章主要介紹了Node使用koa2實現(xiàn)一個簡單JWT鑒權(quán)的方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01詳解Node.js amqplib 連接 Rabbit MQ最佳實踐
這篇文章主要介紹了詳解Node.js amqplib 連接 Rabbit MQ最佳實踐,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2019-01-01