摘要:問題引入接觸過事件循環的同學大都會糾結一個點,就是在中和執行順序的隨機性。當隊列被執行完,或者執行的回調數量達到上限后,事件循環才會進入下一個階段。嵌套的在下一個事件循環的階段執行回調輸出嵌套的。
問題引入
接觸過事件循環的同學大都會糾結一個點,就是在Node中setTimeout和setImmediate執行順序的隨機性。
比如說下面這段代碼:
setTimeout(() => { console.log("setTimeout"); }, 0); setImmediate(() => { console.log("setImmediate"); })
執行的結果是這樣子的:
為什么會出現這種情況呢?別急,我們先往下看。
瀏覽器中事件循環模型我們都知道,JavaScript是單線程的語言,對I/O的控制是通過異步來實現的,具體是通過“事件循環”機制來實現。
對于JavaScript中的單線程,指的是JavaScript執行在單線程中,而內部I/O任務其實是另有線程池來完成的。
在瀏覽器中,我們討論事件循環,是以“從宏任務隊列中取一個任務執行,再取出微任務隊列中的所有任務”來分析執行代碼的。但是在Node環境中并不適用。具體的瀏覽器事件循環解析:傳送門
在Node中,事件循環的模型和瀏覽器相比大致相同,而最大的不同點在于Node中事件循環分不同的階段。具體我們下面會討論到。本文核心也在這里。
Node中事件循環階段解析下面是事件循環不同階段的示意圖:
每個階段都有一個先進先出的回調隊列要執行。而每個階段都有自己的特殊之處。簡單來說,就是當事件循環進入某個階段后,會執行該階段特定的任意操作,然后才會執行這個階段里的回調。當隊列被執行完,或者執行的回調數量達到上限后,事件循環才會進入下一個階段。
以下是各個階段詳情。
timers一個timer指定一個下限時間而不是準確時間,在達到這個下限時間后執行回調。在指定的時間過后,timers會盡早的執行回調,但是系統調度或者其他回調的執行可能會延遲它們。
從技術上來說,poll階段控制timers什么時候執行,而執行的具體位置在timers。
下限的時間有一個范圍:[1, 2147483647],如果設定的時間不在這個范圍,將被設置為1。
I/O callbacks這個階段執行一些系統操作的回調,比如說TCP連接發生錯誤。
idle, prepare系統內部的一些調用。
poll這是最復雜的一個階段。
poll階段有兩個主要的功能:一是執行下限時間已經達到的timers的回調,一是處理poll隊列里的事件。
注:Node很多API都是基于事件訂閱完成的,這些API的回調應該都在poll階段完成。
以下是Node官網的介紹:
筆者把官網陳述的情況以不同的條件分解,更加的清楚。(如果有誤,師請改正。)
當事件循環進入poll階段:
poll隊列不為空的時候,事件循環肯定是先遍歷隊列并同步執行回調,直到隊列清空或執行回調數達到系統上限。
poll隊列為空的時候,這里有兩種情況。
如果代碼已經被setImmediate()設定了回調,那么事件循環直接結束poll階段進入check階段來執行check隊列里的回調。
如果代碼沒有被設定setImmediate()設定回調:
如果有被設定的timers,那么此時事件循環會檢查timers,如果有一個或多個timers下限時間已經到達,那么事件循環將繞回timers階段,并執行timers的有效回調隊列。
如果沒有被設定timers,這個時候事件循環是阻塞在poll階段等待回調被加入poll隊列。
check這個階段允許在poll階段結束后立即執行回調。如果poll階段空閑,并且有被setImmediate()設定的回調,那么事件循環直接跳到check執行而不是阻塞在poll階段等待回調被加入。
setImmediate()實際上是一個特殊的timer,跑在事件循環中的一個獨立的階段。它使用libuv的API來設定在poll階段結束后立即執行回調。
注:setImmediate()具有最高優先級,只要poll隊列為空,代碼被setImmediate(),無論是否有timers達到下限時間,setImmediate()的代碼都先執行。
close callbacks如果一個socket或handle被突然關掉(比如socket.destroy()),close事件將在這個階段被觸發,否則將通過process.nextTick()觸發。
關于setTimeout和setImmediate代碼重現,我們會發現setTimeout和setImmediate在Node環境下執行是靠“隨緣法則”的。
比如說下面這段代碼:
setTimeout(() => { console.log("setTimeout"); }, 0); setImmediate(() => { console.log("setImmediate"); })
執行的結果是這樣子的:
為什么會這樣子呢?
這里我們要根據前面的那個事件循環不同階段的圖解來說明一下:
首先進入的是timers階段,如果我們的機器性能一般,那么進入timers階段,一毫秒已經過去了(setTimeout(fn, 0)等價于setTimeout(fn, 1)),那么setTimeout的回調會首先執行。
如果沒有到一毫秒,那么在timers階段的時候,下限時間沒到,setTimeout回調不執行,事件循環來到了poll階段,這個時候隊列為空,此時有代碼被setImmediate(),于是先執行了setImmediate()的回調函數,之后在下一個事件循環再執行setTimemout的回調函數。
而我們在執行代碼的時候,進入timers的時間延遲其實是隨機的,并不是確定的,所以會出現兩個函數執行順序隨機的情況。
那我們再來看一段代碼:
var fs = require("fs") fs.readFile(__filename, () => { setTimeout(() => { console.log("timeout"); }, 0); setImmediate(() => { console.log("immediate"); }); });
這里我們就會發現,setImmediate永遠先于setTimeout執行。
原因如下:
fs.readFile的回調是在poll階段執行的,當其回調執行完畢之后,poll隊列為空,而setTimeout入了timers的隊列,此時有代碼被setImmediate(),于是事件循環先進入check階段執行回調,之后在下一個事件循環再在timers階段中執行有效回調。
同樣的,這段代碼也是一樣的道理:
setTimeout(() => { setImmediate(() => { console.log("setImmediate"); }); setTimeout(() => { console.log("setTimeout"); }, 0); }, 0);
以上的代碼在timers階段執行外部的setTimeout回調后,內層的setTimeout和setImmediate入隊,之后事件循環繼續往后面的階段走,走到poll階段的時候發現隊列為空,此時有代碼被setImmedate(),所以直接進入check階段執行響應回調(注意這里沒有去檢測timers隊列中是否有成員到達下限事件,因為setImmediate()優先)。之后在第二個事件循環的timers階段中再去執行相應的回調。
綜上,我們可以總結:
如果兩者都在主模塊中調用,那么執行先后取決于進程性能,也就是隨機。
如果兩者都不在主模塊調用(被一個異步操作包裹),那么setImmediate的回調永遠先執行。
process.nextTick() and Promise對于這兩個,我們可以把它們理解成一個微任務。也就是說,它其實不屬于事件循環的一部分。
那么他們是在什么時候執行呢?
不管在什么地方調用,他們都會在其所處的事件循環最后,事件循環進入下一個循環的階段前執行。
舉個?:
setTimeout(() => { console.log("timeout0"); process.nextTick(() => { console.log("nextTick1"); process.nextTick(() => { console.log("nextTick2"); }); }); process.nextTick(() => { console.log("nextTick3"); }); console.log("sync"); setTimeout(() => { console.log("timeout2"); }, 0); }, 0);
結果是:
再解釋一下:
timers階段執行外層setTimeout的回調,遇到同步代碼先執行,也就有timeout0、sync的輸出。遇到process.nextTick后入微任務隊列,依次nextTick1、nextTick3、nextTick2入隊后出隊輸出。之后,在下一個事件循環的timers階段,執行setTimeout回調輸出timeout2。
最后下面給出兩段代碼,如果能夠理解其執行順序說明你已經理解透徹。
代碼1:
setImmediate(function(){ console.log("setImmediate"); setImmediate(function(){ console.log("嵌套setImmediate"); }); process.nextTick(function(){ console.log("nextTick"); }) }); // setImmediate // nextTick // 嵌套setImmediate
解析:事件循環check階段執行回調函數輸出setImmediate,之后輸出nextTick。嵌套的setImmediate在下一個事件循環的check階段執行回調輸出嵌套的setImmediate。
代碼2:
var fs = require("fs"); function someAsyncOperation (callback) { // 假設這個任務要消耗 95ms fs.readFile("/path/to/file", callback); } var timeoutScheduled = Date.now(); setTimeout(function () { var delay = Date.now() - timeoutScheduled; console.log(delay + "ms have passed since I was scheduled"); }, 100); // someAsyncOperation要消耗 95 ms 才能完成 someAsyncOperation(function () { var startCallback = Date.now(); // 消耗 10ms... while (Date.now() - startCallback < 10) { ; // do nothing } });
解析:事件循環進入poll階段發現隊列為空,并且沒有代碼被setImmediate()。于是在poll階段等待timers下限時間到達。當等到95ms時,fs.readFile首先執行了,它的回調被添加進poll隊列并同步執行,耗時10ms。此時總共時間累積105ms。等到poll隊列為空的時候,事件循環會查看最近到達的timer的下限時間,發現已經到達,再回到timers階段,執行timer的回調。
如果有什么問題,歡迎留言交流探討。
參考鏈接:
https://nodejs.org/en/docs/gu...
https://github.com/creeperyan...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107169.html
摘要:異步在中,是在單線程中執行的沒錯,但是內部完成工作的另有線程池,使用一個主進程和多個線程來模擬異步。在事件循環中,觀察者會不斷的找到線程池中已經完成的請求對象,從中取出回調函數和數據并執行。 1. 介紹 單線程編程會因阻塞I/O導致硬件資源得不到更優的使用。多線程編程也因為編程中的死鎖、狀態同步等問題讓開發人員頭痛。Node在兩者之間給出了它的解決方案:利用單線程,遠離多線程死鎖、狀態...
摘要:概述本篇主要介紹的運行機制單線程事件循環結論先在中利用運行至完成和非阻塞完成單線程下異步任務的處理就是先處理主模塊主線程上的同步任務再處理異步任務異步任務使用事件循環機制完成調度涉及的內容有單線程事件循環同步執行異步執行定時器的事件循環開始 1.概述 本篇主要介紹JavaScript的運行機制:單線程事件循環(Event Loop). 結論先: 在JavaScript中, 利用運行至...
摘要:事件觸發線程主要負責將準備好的事件交給引擎線程執行。它將不同的任務分配給不同的線程,形成一個事件循環,以異步的方式將任務的執行結果返回給引擎。 Fundebug經作者浪里行舟授權首發,未經同意請勿轉載。 前言 本文我們將會介紹 JS 實現異步的原理,并且了解了在瀏覽器和 Node 中 Event Loop 其實是不相同的。 一、線程與進程 1. 概念 我們經常說 JS 是單線程執行的,...
本文涵蓋 面試題的引入 對事件循環面試題執行順序的一些疑問 通過面試題對微任務、事件循環、定時器等對深入理解 結論總結 面試題 面試題如下,大家可以先試著寫一下輸出結果,然后再看我下面的詳細講解,看看會不會有什么出入,如果把整個順序弄清楚 Node.js 的執行順序應該就沒問題了。 async function async1(){ console.log(async1 start) ...
閱讀 2978·2021-11-23 09:51
閱讀 3609·2021-10-13 09:39
閱讀 2493·2021-09-22 15:06
閱讀 881·2019-08-30 15:55
閱讀 3147·2019-08-30 15:44
閱讀 1778·2019-08-30 14:05
閱讀 3434·2019-08-29 15:24
閱讀 2362·2019-08-29 12:44