您好,登錄后才能下訂單哦!
JavaScript 是單線程運行,異步操作特別重要。
只要用到引擎之外的功能,就需要跟外部交互,從而形成異步操作。由于異步操作實在太多,JavaScript 不得不提供很多異步語法。這就好比,有些人老是受打擊, 他的抗打擊能力必須變得很強,否則他就完蛋了。
Node 的異步語法比瀏覽器更復雜,因為它可以跟內核對話,不得不搞了一個專門的庫 libuv 做這件事。這個庫負責各種回調函數(shù)的執(zhí)行時間,畢竟異步任務最后還是要回到主線程,一個個排隊執(zhí)行。
為了協(xié)調異步任務,Node 居然提供了四個定時器,讓任務可以在指定的時間運行。
前兩個是語言的標準,后兩個是 Node 獨有的。它們的寫法差不多,作用也差不多,不太容易區(qū)別。
你能說出下面代碼的運行結果嗎?
// test.js setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4)); (() => console.log(5))();
運行結果如下。
$ node test.js
如果你能一口說對,可能就不需要再看下去了。本文詳細解釋,Node 怎么處理各種定時器,或者更廣義地說,libuv 庫怎么安排異步任務在主線程上執(zhí)行。
一、同步任務和異步任務
首先,同步任務總是比異步任務更早執(zhí)行。
前面的那段代碼,只有最后一行是同步任務,因此最早執(zhí)行。
(() => console.log(5))();
二、本輪循環(huán)和次輪循環(huán)
異步任務可以分成兩種。
追加在本輪循環(huán)的異步任務
追加在次輪循環(huán)的異步任務
所謂”循環(huán)”,指的是事件循環(huán)(event loop)。這是 JavaScript 引擎處理異步任務的方式,后文會詳細解釋。這里只要理解,本輪循環(huán)一定早于次輪循環(huán)執(zhí)行即可。
Node 規(guī)定,process.nextTick和Promise的回調函數(shù),追加在本輪循環(huán),即同步任務一旦執(zhí)行完成,就開始執(zhí)行它們。而setTimeout、setInterval、setImmediate的回調函數(shù),追加在次輪循環(huán)。
這就是說,文首那段代碼的第三行和第四行,一定比第一行和第二行更早執(zhí)行。
// 下面兩行,次輪循環(huán)執(zhí)行 setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); // 下面兩行,本輪循環(huán)執(zhí)行 process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4));
三、process.nextTick()
process.nextTick這個名字有點誤導,它是在本輪循環(huán)執(zhí)行的,而且是所有異步任務里面最快執(zhí)行的。
Node 執(zhí)行完所有同步任務,接下來就會執(zhí)行process.nextTick的任務隊列。所以,下面這行代碼是第二個輸出結果。
process.nextTick(() => console.log(3));
基本上,如果你希望異步任務盡可能快地執(zhí)行,那就使用process.nextTick。
四、微任務
根據(jù)語言規(guī)格,Promise
對象的回調函數(shù),會進入異步任務里面的”微任務”(microtask
)隊列。
微任務隊列追加在process.nextTick
隊列的后面,也屬于本輪循環(huán)。所以,下面的代碼總是先輸出3,再輸出4。
process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4)); // 3 // 4
注意,只有前一個隊列全部清空以后,才會執(zhí)行下一個隊列。
process.nextTick(() => console.log(1)); Promise.resolve().then(() => console.log(2)); process.nextTick(() => console.log(3)); Promise.resolve().then(() => console.log(4)); // 1 // 3 // 2 // 4
上面代碼中,全部process.nextTick
的回調函數(shù),執(zhí)行都會早于Promise
的。
至此,本輪循環(huán)的執(zhí)行順序就講完了。
同步任務 process.nextTick() 微任務
五、事件循環(huán)的概念
下面開始介紹次輪循環(huán)的執(zhí)行順序,這就必須理解什么是事件循環(huán)(event loop)了。
Node 的官方文檔是這樣介紹的。
“When Node.js starts, it initializes the event loop, processes the provided input script which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.”
這段話很重要,需要仔細讀。它表達了三層意思。
首先,有些人以為,除了主線程,還存在一個單獨的事件循環(huán)線程。不是這樣的,只有一個主線程,事件循環(huán)是在主線程上完成的。
其次,Node 開始執(zhí)行腳本時,會先進行事件循環(huán)的初始化,但是這時事件循環(huán)還沒有開始,會先完成下面的事情。
執(zhí)行process.nextTick()等等
最后,上面這些事情都干完了,事件循環(huán)就正式開始了。
六、事件循環(huán)的六個階段
事件循環(huán)會無限次地執(zhí)行,一輪又一輪。只有異步任務的回調函數(shù)隊列清空了,才會停止執(zhí)行。
每一輪的事件循環(huán),分成六個階段。這些階段會依次執(zhí)行。
timers
I/O callbacks
idle, prepare
poll
check
close callbacks
每個階段都有一個先進先出的回調函數(shù)隊列。只有一個階段的回調函數(shù)隊列清空了,該執(zhí)行的回調函數(shù)都執(zhí)行了,事件循環(huán)才會進入下一個階段。
下面簡單介紹一下每個階段的含義,詳細介紹可以看官方文檔,也可以參考 libuv 的源碼解讀。
(1)timers
這個是定時器階段,處理setTimeout()和setInterval()的回調函數(shù)。進入這個階段后,主線程會檢查一下當前時間,是否滿足定時器的條件。如果滿足就執(zhí)行回調函數(shù),否則就離開這個階段。
(2)I/O callbacks
除了以下操作的回調函數(shù),其他的回調函數(shù)都在這個階段執(zhí)行。
(3)idle, prepare
該階段只供 libuv 內部調用,這里可以忽略。
(4)Poll
這個階段是輪詢時間,用于等待還未返回的 I/O 事件,比如服務器的回應、用戶移動鼠標等等。
這個階段的時間會比較長。如果沒有其他異步任務要處理(比如到期的定時器),會一直停留在這個階段,等待 I/O 請求返回結果。
(5)check
該階段執(zhí)行setImmediate()的回調函數(shù)。
(6)close callbacks
該階段執(zhí)行關閉請求的回調函數(shù),比如socket.on('close', ...)
。
七、事件循環(huán)的示例
下面是來自官方文檔的一個示例。
const fs = require('fs'); const timeoutScheduled = Date.now(); // 異步任務一:100ms 后執(zhí)行的定時器 setTimeout(() => { const delay = Date.now() - timeoutScheduled; console.log(`${delay}ms`); }, 100); // 異步任務二:至少需要 200ms 的文件讀取 fs.readFile('test.js', () => { const startCallback = Date.now(); while (Date.now() - startCallback < 200) { // 什么也不做 } });
上面代碼有兩個異步任務,一個是 100ms 后執(zhí)行的定時器,一個是至少需要 200ms 的文件讀取。請問運行結果是什么?
腳本進入第一輪事件循環(huán)以后,沒有到期的定時器,也沒有已經(jīng)可以執(zhí)行的 I/O 回調函數(shù),所以會進入 Poll 階段,等待內核返回文件讀取的結果。由于讀取小文件一般不會超過 100ms,所以在定時器到期之前,Poll 階段就會得到結果,因此就會繼續(xù)往下執(zhí)行。
第二輪事件循環(huán),依然沒有到期的定時器,但是已經(jīng)有了可以執(zhí)行的 I/O 回調函數(shù),所以會進入 I/O callbacks 階段,執(zhí)行fs.readFile的回調函數(shù)。這個回調函數(shù)需要 200ms,也就是說,在它執(zhí)行到一半的時候,100ms 的定時器就會到期。但是,必須等到這個回調函數(shù)執(zhí)行完,才會離開這個階段。
第三輪事件循環(huán),已經(jīng)有了到期的定時器,所以會在 timers 階段執(zhí)行定時器。最后輸出結果大概是200多毫秒。
八、setTimeout 和 setImmediate
由于setTimeout
在 timers 階段執(zhí)行,而setImmediate
在 check 階段執(zhí)行。所以,setTimeout會早于setImmediate完成。
setTimeout(() => console.log(1)); setImmediate(() => console.log(2));
上面代碼應該先輸出1,再輸出2,但是實際執(zhí)行的時候,結果卻是不確定,有時還會先輸出2,再輸出1。
這是因為setTimeout的第二個參數(shù)默認為0。但是實際上,Node 做不到0毫秒,最少也需要1毫秒,根據(jù)官方文檔,第二個參數(shù)的取值范圍在1毫秒到2147483647毫秒之間。也就是說,setTimeout(f, 0)
等同于setTimeout(f, 1)
。
實際執(zhí)行的時候,進入事件循環(huán)以后,有可能到了1毫秒,也可能還沒到1毫秒,取決于系統(tǒng)當時的狀況。如果沒到1毫秒,那么 timers 階段就會跳過,進入 check 階段,先執(zhí)行setImmediate的回調函數(shù)。
但是,下面的代碼一定是先輸出2,再輸出1。
const fs = require('fs'); fs.readFile('test.js', () => { setTimeout(() => console.log(1)); setImmediate(() => console.log(2)); });
上面代碼會先進入 I/O callbacks 階段,然后是 check 階段,最后才是 timers 階段。因此,setImmediate才會早于setTimeout執(zhí)行。
總結
以上所述是小編給大家介紹的Node 定時器的相關知識,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對億速云網(wǎng)站的支持!
免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內容。