成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

由setTimeout和setImmediate執(zhí)行順序的隨機性窺探Node的事件循環(huán)機制

marek / 2510人閱讀

摘要:問題引入接觸過事件循環(huán)的同學大都會糾結一個點,就是在中和執(zhí)行順序的隨機性。當隊列被執(zhí)行完,或者執(zhí)行的回調(diào)數(shù)量達到上限后,事件循環(huán)才會進入下一個階段。嵌套的在下一個事件循環(huán)的階段執(zhí)行回調(diào)輸出嵌套的。

問題引入

接觸過事件循環(huán)的同學大都會糾結一個點,就是在Node中setTimeoutsetImmediate執(zhí)行順序的隨機性。

比如說下面這段代碼:

setTimeout(() => {
    console.log("setTimeout");
}, 0);
setImmediate(() => {
    console.log("setImmediate");
})

執(zhí)行的結果是這樣子的:

為什么會出現(xiàn)這種情況呢?別急,我們先往下看。

瀏覽器中事件循環(huán)模型

我們都知道,JavaScript是單線程的語言,對I/O的控制是通過異步來實現(xiàn)的,具體是通過“事件循環(huán)”機制來實現(xiàn)。

對于JavaScript中的單線程,指的是JavaScript執(zhí)行在單線程中,而內(nèi)部I/O任務其實是另有線程池來完成的。

在瀏覽器中,我們討論事件循環(huán),是以“從宏任務隊列中取一個任務執(zhí)行,再取出微任務隊列中的所有任務”來分析執(zhí)行代碼的。但是在Node環(huán)境中并不適用。具體的瀏覽器事件循環(huán)解析:傳送門

在Node中,事件循環(huán)的模型和瀏覽器相比大致相同,而最大的不同點在于Node中事件循環(huán)分不同的階段。具體我們下面會討論到。本文核心也在這里。

Node中事件循環(huán)階段解析

下面是事件循環(huán)不同階段的示意圖:

每個階段都有一個先進先出的回調(diào)隊列要執(zhí)行。而每個階段都有自己的特殊之處。簡單來說,就是當事件循環(huán)進入某個階段后,會執(zhí)行該階段特定的任意操作,然后才會執(zhí)行這個階段里的回調(diào)。當隊列被執(zhí)行完,或者執(zhí)行的回調(diào)數(shù)量達到上限后,事件循環(huán)才會進入下一個階段。

以下是各個階段詳情。

timers

一個timer指定一個下限時間而不是準確時間,在達到這個下限時間后執(zhí)行回調(diào)。在指定的時間過后,timers會盡早的執(zhí)行回調(diào),但是系統(tǒng)調(diào)度或者其他回調(diào)的執(zhí)行可能會延遲它們。

從技術上來說,poll階段控制timers什么時候執(zhí)行,而執(zhí)行的具體位置在timers。

下限的時間有一個范圍:[1, 2147483647],如果設定的時間不在這個范圍,將被設置為1。

I/O callbacks

這個階段執(zhí)行一些系統(tǒng)操作的回調(diào),比如說TCP連接發(fā)生錯誤。

idle, prepare

系統(tǒng)內(nèi)部的一些調(diào)用。

poll

這是最復雜的一個階段。

poll階段有兩個主要的功能:一是執(zhí)行下限時間已經(jīng)達到的timers的回調(diào),一是處理poll隊列里的事件。

注:Node很多API都是基于事件訂閱完成的,這些API的回調(diào)應該都在poll階段完成。

以下是Node官網(wǎng)的介紹:

筆者把官網(wǎng)陳述的情況以不同的條件分解,更加的清楚。(如果有誤,師請改正。)

當事件循環(huán)進入poll階段:

poll隊列不為空的時候,事件循環(huán)肯定是先遍歷隊列并同步執(zhí)行回調(diào),直到隊列清空或執(zhí)行回調(diào)數(shù)達到系統(tǒng)上限。

poll隊列為空的時候,這里有兩種情況。

如果代碼已經(jīng)被setImmediate()設定了回調(diào),那么事件循環(huán)直接結束poll階段進入check階段來執(zhí)行check隊列里的回調(diào)。

如果代碼沒有被設定setImmediate()設定回調(diào):

如果有被設定的timers,那么此時事件循環(huán)會檢查timers,如果有一個或多個timers下限時間已經(jīng)到達,那么事件循環(huán)將繞回timers階段,并執(zhí)行timers的有效回調(diào)隊列。

如果沒有被設定timers,這個時候事件循環(huán)是阻塞在poll階段等待回調(diào)被加入poll隊列。

check

這個階段允許在poll階段結束后立即執(zhí)行回調(diào)。如果poll階段空閑,并且有被setImmediate()設定的回調(diào),那么事件循環(huán)直接跳到check執(zhí)行而不是阻塞在poll階段等待回調(diào)被加入。

setImmediate()實際上是一個特殊的timer,跑在事件循環(huán)中的一個獨立的階段。它使用libuvAPI來設定在poll階段結束后立即執(zhí)行回調(diào)。

注:setImmediate()具有最高優(yōu)先級,只要poll隊列為空,代碼被setImmediate(),無論是否有timers達到下限時間,setImmediate()的代碼都先執(zhí)行。

close callbacks

如果一個sockethandle被突然關掉(比如socket.destroy()),close事件將在這個階段被觸發(fā),否則將通過process.nextTick()觸發(fā)。

關于setTimeout和setImmediate

代碼重現(xiàn),我們會發(fā)現(xiàn)setTimeoutsetImmediate在Node環(huán)境下執(zhí)行是靠“隨緣法則”的。

比如說下面這段代碼:

setTimeout(() => {
    console.log("setTimeout");
}, 0);
setImmediate(() => {
    console.log("setImmediate");
})

執(zhí)行的結果是這樣子的:

為什么會這樣子呢?

這里我們要根據(jù)前面的那個事件循環(huán)不同階段的圖解來說明一下:

首先進入的是timers階段,如果我們的機器性能一般,那么進入timers階段,一毫秒已經(jīng)過去了(setTimeout(fn, 0)等價于setTimeout(fn, 1)),那么setTimeout的回調(diào)會首先執(zhí)行。

如果沒有到一毫秒,那么在timers階段的時候,下限時間沒到,setTimeout回調(diào)不執(zhí)行,事件循環(huán)來到了poll階段,這個時候隊列為空,此時有代碼被setImmediate(),于是先執(zhí)行了setImmediate()的回調(diào)函數(shù),之后在下一個事件循環(huán)再執(zhí)行setTimemout的回調(diào)函數(shù)。

而我們在執(zhí)行代碼的時候,進入timers的時間延遲其實是隨機的,并不是確定的,所以會出現(xiàn)兩個函數(shù)執(zhí)行順序隨機的情況。

那我們再來看一段代碼:

var fs = require("fs")

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log("timeout");
    }, 0);
    setImmediate(() => {
        console.log("immediate");
    });
});

這里我們就會發(fā)現(xiàn),setImmediate永遠先于setTimeout執(zhí)行。

原因如下:

fs.readFile的回調(diào)是在poll階段執(zhí)行的,當其回調(diào)執(zhí)行完畢之后,poll隊列為空,而setTimeout入了timers的隊列,此時有代碼被setImmediate(),于是事件循環(huán)先進入check階段執(zhí)行回調(diào),之后在下一個事件循環(huán)再在timers階段中執(zhí)行有效回調(diào)。

同樣的,這段代碼也是一樣的道理:

setTimeout(() => {
    setImmediate(() => {
        console.log("setImmediate");
    });
    setTimeout(() => {
        console.log("setTimeout");
    }, 0);
}, 0);

以上的代碼在timers階段執(zhí)行外部的setTimeout回調(diào)后,內(nèi)層的setTimeoutsetImmediate入隊,之后事件循環(huán)繼續(xù)往后面的階段走,走到poll階段的時候發(fā)現(xiàn)隊列為空,此時有代碼被setImmedate(),所以直接進入check階段執(zhí)行響應回調(diào)(注意這里沒有去檢測timers隊列中是否有成員到達下限事件,因為setImmediate()優(yōu)先)。之后在第二個事件循環(huán)的timers階段中再去執(zhí)行相應的回調(diào)。

綜上,我們可以總結:

如果兩者都在主模塊中調(diào)用,那么執(zhí)行先后取決于進程性能,也就是隨機。

如果兩者都不在主模塊調(diào)用(被一個異步操作包裹),那么setImmediate的回調(diào)永遠先執(zhí)行。

process.nextTick() and Promise

對于這兩個,我們可以把它們理解成一個微任務。也就是說,它其實不屬于事件循環(huán)的一部分。

那么他們是在什么時候執(zhí)行呢?

不管在什么地方調(diào)用,他們都會在其所處的事件循環(huán)最后,事件循環(huán)進入下一個循環(huán)的階段前執(zhí)行。

舉個?:

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階段執(zhí)行外層setTimeout的回調(diào),遇到同步代碼先執(zhí)行,也就有timeout0、sync的輸出。遇到process.nextTick后入微任務隊列,依次nextTick1、nextTick3、nextTick2入隊后出隊輸出。之后,在下一個事件循環(huán)的timers階段,執(zhí)行setTimeout回調(diào)輸出timeout2。

最后

下面給出兩段代碼,如果能夠理解其執(zhí)行順序說明你已經(jīng)理解透徹。

代碼1:

setImmediate(function(){
  console.log("setImmediate");
  setImmediate(function(){
    console.log("嵌套setImmediate");
  });
  process.nextTick(function(){
    console.log("nextTick");
  })
});

// setImmediate
// nextTick
// 嵌套setImmediate

解析:事件循環(huán)check階段執(zhí)行回調(diào)函數(shù)輸出setImmediate,之后輸出nextTick。嵌套的setImmediate在下一個事件循環(huán)的check階段執(zhí)行回調(diào)輸出嵌套的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
  }

});

解析:事件循環(huán)進入poll階段發(fā)現(xiàn)隊列為空,并且沒有代碼被setImmediate()。于是在poll階段等待timers下限時間到達。當?shù)鹊?b>95ms時,fs.readFile首先執(zhí)行了,它的回調(diào)被添加進poll隊列并同步執(zhí)行,耗時10ms。此時總共時間累積105ms。等到poll隊列為空的時候,事件循環(huán)會查看最近到達的timer的下限時間,發(fā)現(xiàn)已經(jīng)到達,再回到timers階段,執(zhí)行timer的回調(diào)。

如果有什么問題,歡迎留言交流探討。

參考鏈接:

https://nodejs.org/en/docs/gu...

https://github.com/creeperyan...

文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/107169.html

相關文章

  • Node事件循環(huán)異步API

    摘要:異步在中,是在單線程中執(zhí)行的沒錯,但是內(nèi)部完成工作的另有線程池,使用一個主進程和多個線程來模擬異步。在事件循環(huán)中,觀察者會不斷的找到線程池中已經(jīng)完成的請求對象,從中取出回調(diào)函數(shù)和數(shù)據(jù)并執(zhí)行。 1. 介紹 單線程編程會因阻塞I/O導致硬件資源得不到更優(yōu)的使用。多線程編程也因為編程中的死鎖、狀態(tài)同步等問題讓開發(fā)人員頭痛。Node在兩者之間給出了它的解決方案:利用單線程,遠離多線程死鎖、狀態(tài)...

    atinosun 評論0 收藏0
  • JavaScript單線程事件循環(huán)(Event Loop)那些事

    摘要:概述本篇主要介紹的運行機制單線程事件循環(huán)結論先在中利用運行至完成和非阻塞完成單線程下異步任務的處理就是先處理主模塊主線程上的同步任務再處理異步任務異步任務使用事件循環(huán)機制完成調(diào)度涉及的內(nèi)容有單線程事件循環(huán)同步執(zhí)行異步執(zhí)行定時器的事件循環(huán)開始 1.概述 本篇主要介紹JavaScript的運行機制:單線程事件循環(huán)(Event Loop). 結論先: 在JavaScript中, 利用運行至...

    Shisui 評論0 收藏0
  • 瀏覽器與Node事件循環(huán)(Event Loop)有何區(qū)別?

    摘要:事件觸發(fā)線程主要負責將準備好的事件交給引擎線程執(zhí)行。它將不同的任務分配給不同的線程,形成一個事件循環(huán),以異步的方式將任務的執(zhí)行結果返回給引擎。 Fundebug經(jīng)作者浪里行舟授權首發(fā),未經(jīng)同意請勿轉(zhuǎn)載。 前言 本文我們將會介紹 JS 實現(xiàn)異步的原理,并且了解了在瀏覽器和 Node 中 Event Loop 其實是不相同的。 一、線程與進程 1. 概念 我們經(jīng)常說 JS 是單線程執(zhí)行的,...

    TANKING 評論0 收藏0
  • 用一道大廠面試題帶你搞懂事件循環(huán)機制

    本文涵蓋 面試題的引入 對事件循環(huán)面試題執(zhí)行順序的一些疑問 通過面試題對微任務、事件循環(huán)、定時器等對深入理解 結論總結 面試題 面試題如下,大家可以先試著寫一下輸出結果,然后再看我下面的詳細講解,看看會不會有什么出入,如果把整個順序弄清楚 Node.js 的執(zhí)行順序應該就沒問題了。 async function async1(){ console.log(async1 start) ...

    ShowerSun 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<