詳談javascript和node的事件迴圈

柳菁發表於2018-09-26

Javascript中的事件迴圈

javascript是一門單執行緒的非阻塞的指令碼語言。單執行緒,即js程式碼在執行的任何時候,都只有一個主執行緒來處理所有任務。非阻塞,只要指的是執行非同步任務(如I/O事件)時,主執行緒會掛起這個任務,然後在非同步任務返回結果的時候再按照一定規則執行相應的回撥。

Web worker 技術所實現的多執行緒技術也存在諸多限制。如,所有新執行緒都受到主執行緒的完全控制,不能獨立執行。這意味著這些‘執行緒’實際上是主執行緒的子執行緒。另外,這些子執行緒沒有執行I/O操作的許可權,只能為主執行緒分擔一些如計算等任務。所以嚴格來講,web worker並沒有改變javascript的單執行緒本質。

  1. 執行棧和同步執行

執行棧與儲存物件指標和基礎型別變數的棧是不同的。執行棧是指,當呼叫一個方法時,js會生成與這個方法對應的一個執行環境(context),即執行上下文。這個執行環境中包含:這個執行環境的私有作用域、上層作用域的指向,方法的引數,私有變數以及該作用域的this指向。因為js是單執行緒的,同一時間只能執行一個方法,也就是說,當一個方法被執行的時候,其他方法會被排隊到一個單獨的地方,即執行棧。

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

案列1:

function Func1 () {

    console.log(1)

    function Func2 () {

        console.log(2)

        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. 事件迴圈模型

詳談javascript和node的事件迴圈

(來自:node官網)

詳談javascript和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

注:歡迎大家監督指導,如有疑問或錯誤,請留言一起探討~~


相關文章