詳談javascript和node的事件循環

Javascript中的事件循環javascript

javascript是一門單線程的非阻塞的腳本語言。單線程,即js代碼在執行的任什麼時候候,都只有一個主線程來處理全部任務。非阻塞,只要指的是執行異步任務(如I/O事件)時,主線程會掛起這個任務,而後在異步任務返回結果的時候再按照必定規則執行相應的回調。java

Web worker 技術所實現的多線程技術也存在諸多限制。如,全部新線程都受到主線程的徹底控制,不能獨立執行。這意味着這些‘線程’其實是主線程的子線程。另外,這些子線程沒有執行I/O操做的權限,只能爲主線程分擔一些如計算等任務。因此嚴格來說,web worker並無改變javascript的單線程本質。node

  1. 執行棧和同步執行

執行棧與存儲對象指針和基礎類型變量的棧是不一樣的。執行棧是指,當調用一個方法時,js會生成與這個方法對應的一個執行環境(context),即執行上下文。這個執行環境中包含:這個執行環境的私有做用域、上層做用域的指向,方法的參數,私有變量以及該做用域的this指向。由於js是單線程的,同一時間只能執行一個方法,也就是說,當一個方法被執行的時候,其餘方法會被排隊到一個單獨的地方,即執行棧。web

當一個腳本第一次執行的時候,js引擎會解析這段代碼,並將其中的同步代碼按照執行順序加入執行棧,而後從頭開始執行。當執行一個方法時,js會向執行棧中添加這個方法的執行環境,而後進入這個執行環境繼續執行其中的代碼。當這個執行環境中的代碼執行完畢並返回結果後,js會退出當前執行環境並撤銷該環境,回到上一個方法的執行環境,這個過程反覆執行,知道執行棧中的代碼所有執行完畢。chrome

案列1:api

function Func1 () {瀏覽器

    console.log(1)多線程

    function Func2 () {異步

        console.log(2)ui

        function Func3 () {

            console.log(3)

        }

        Func3()

      }

      Func2()

}

Func1()

// 1 2 3

同步執行遵循先進後出的規則,在執行Func1時,會向執行棧加入該方法的執行環境,輸出1,而後解析了Func2,執行時加入了Func2的執行環境,輸出2,而後解析Func3並執行,輸出3,Func3執行完畢後會撤銷Func3的執行環境,接着是Func2執行完畢並撤銷Func2的執行環境,最後撤銷Func1的執行環境。該過程若沒有終止,會無限進行直到棧溢出。

  1. 異步執行

方法執行時,異步執行事件掛起加入與執行棧不一樣的另外一個隊列,即事件隊列中,並繼續執行執行棧中的其餘任務。被放入事件隊列不會當即執行其回調,而是等待當前執行棧中的全部任務執行完畢,在主線程出於閒置狀態時,主線程會查找事件隊列是否有任務。若是有,則會取第一個事件並將該事件的回調放入執行棧中執行,而後執行其中的同步代碼,如此反覆就是事件循環。

異步任務由於各任務的不一樣和執行優先級的區別,分爲 宏任務 (macro task) 和 微任務 (micro task)

屬於宏任務的事件:setTimeout(), setInterval()

屬於微任務的事件:new Promise(), new MutaionObserver()(已廢除)

當執行棧爲空時,主線程會優先查看微任務是否有事件。若是沒有,就會執行宏任務中的第一個事件並將對應的回調加入當前執行棧中;若是有,就會依次執行微任務中事件對應的回調,直到微任務隊列爲空,而後再執行宏任務中的第一個事件對應的回調,如此反覆,進入循環。同一次事件循環中,微任務永遠優先宏任務執行。

案列2:

setTimeout(function () {
    console.log(1);
});

new Promise(function(resolve,reject){
    console.log(2)
    resolve(3)
}).then(function(val){
    console.log(val);
})

// 2 3 1

node環境下的事件循環

在node中,事件循環與瀏覽器中的略有不一樣。node中的事件循環的實現是依靠的libuv引擎。node選用chrome的v8引擎做爲解釋器,v8引擎將js代碼解析後會調用node api,而api則是由libuv引擎驅動,所以node中的事件循環是在libuv引擎中執行。

node中,同步代碼執行完,會先清空微任務隊列,輪詢時會清空當前隊列全部任務,纔會切換到下一個隊列,在切換下一個隊列以前也會先清空微任務隊列。

  1. 事件循環模型


(來自:node官網)


  1. 事件循環說明

node的事件循環順序:

外部輸入數據—>poll階段—>檢查階段(check)—>關閉事件回調階段(close callback)—>定時器檢測執行階段(timers)—>I/O事件回調階段(I/O callbacks)—>idle,prepare—>poll…

setTimeout(() => {console.log('setTimeout')} , 0)

setImmediate(() => {console.log('immediate')})

默認狀況下setTimeout()和 setImmediate()不知道哪個會先執行,node執行也須要準備時間。setTimeout()延遲時間設置爲0,實際仍是有4ms的延遲,假設node準備時間在4ms內,定時器沒有執行,poll階段沒有執行setTimeout(),會先執行check中的setImmediate(),等到下一輪詢進入時,poll檢測到定時器已到時,再執行timer中的setTimeout()

隊列中有一個特殊的推遲任務執行的方法process.nextTick再此執行。咱們知道,每一次事件循環都是從微任務開始的,而且每一階段都是按照事件循環順序進行執行。而在每一次的隊列切換以前,都會檢查nextTick queue中是否有事件,如有則優先執行。

案列3:

setImmediate(() => {

    console.log("setImmediate1");

    setTimeout(() => {

        console.log("setTimeout1");

    }, 0);

});

setTimeout(() => {

    process.nextTick(() => console.log("nextTick"));

    console.log("setTimeout2");

    setImmediate(() => {

        console.log("setImmediate2");

    });

}, 0);

// 結果一

// setImmediate1, setTimeout2, setTimeout1, nextTick, setImmediate2

// 結果二

// setTimeout2, nextTick, setImmediate1, setImmediate2, setTimeout1

產生上面兩種結果的緣由,是node準備時間的差別。

案例4:

const fs = require('fs');

fs.readFile(__filename, () => {

    setImmediate(() => {

        console.log("setImmediate1");

        setTimeout(() => {

            console.log("setTimeout1");

        }, 0);

    });

    setTimeout(() => {

        process.nextTick(() => console.log("nextTick"));

        console.log("setTimeout2");

        setImmediate(() => {

            console.log("setImmediate2");

        });

      }, 0);

});

// setImmediate1, setTimeout2, setTimeout1, nextTick, setImmediate2

此時只會有一種結果,由於是在一個I/O事件的回調中,node準備已結束,setTimeout執行須要等待4ms,setImmediate則當即執行,又setTimeout2和setTimeout1在同一個timers隊列中因此按順序執行,以後須要切換到check隊列執行setImmediate2,在切換以前會先檢查nextTick隊列並執行,所以最後輸出nextTick,setImmediate2

注:歡迎你們監督指導,若有疑問或錯誤,請留言一塊兒探討~~

相關文章
相關標籤/搜索