深入理解JavaScript之徹底弄懂JsEventLoop執行機制

很白的小白發表於2022-06-30

JavaScript單執行緒起源:

JavaScript作為瀏覽器指令碼語言,JavaScript的主要用途是與使用者互動,以及操作DOM,為了避免複雜性,誕生開始,JavaScript就是單執行緒語言。
比如,假定JavaScript同時有兩個執行緒,一個執行緒在某個DOM節點上新增內容,另一個執行緒刪除了這個節點,這時瀏覽器應該以哪個執行緒為準?所以,為了避免複雜性,從一誕生,JavaScript就是單執行緒。

單執行緒及存在的問題:

單執行緒就意味著,所有任務需要排隊,前一個任務結束,才會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等著。

問題的解決--同步、非同步

synchronous(同步任務)和asynchronousk(非同步任務)

  • 同步任務是呼叫立即得到結果的任務,同步任務在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;
  • 非同步任務是呼叫無法立即得到結果的任務,需要額外的操作才能預期結果的任務,非同步任務不進入主執行緒、而進入"任務佇列"(task queue)的任務,只有"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。

macro-task(巨集任務)和micro-task(微任務)

  • 巨集任務:macro-task 可以理解是每次執行棧執行的程式碼就是一個巨集任務(包括每次從事件佇列中獲取一個事件回撥並放到執行棧中執行,每一個巨集任務會從頭到尾將這個任務執行完畢,不會執行其它)包括整體程式碼script,setTimeout,setInterval等
    image.png
  • 微任務:micro-task可以理解是在當前 task 執行結束後立即執行的任務 包括Promise,process.nextTick等
    image.png

    Event Loop事件迴圈

    Event Loop即事件迴圈,是指瀏覽器或Node的一種解決javaScript單執行緒執行時不會阻塞的一種機制,其實巨集任務佇列和微任務佇列的執行,就是事件迴圈的一部分。
    事件迴圈的具體流程如下:

  1. 從巨集任務佇列中,按照入隊順序,找到第一個執行的巨集任務,放入呼叫棧,開始執行;
  2. 執行完該巨集任務下所有同步任務後,即呼叫棧清空後,該巨集任務被推出巨集任務佇列,然後微任務佇列開始按照入隊順序,依次執行其中的微任務,直至微任務佇列清空為止;
  3. 當微任務佇列清空後,一個事件迴圈結束;
  4. 接著從巨集任務佇列中,找到下一個執行的巨集任務,開始第二個事件迴圈,直至巨集任務佇列清空為止。

Js執行機制流程圖

Js執行機制流程圖
注意:

  • 當我們第一次執行的時候,直譯器會將整體程式碼script放入巨集任務佇列中,因此事件迴圈是從第一個巨集任務開始的;因此我在流程圖中將主執行緒單獨分離開。
  • 如果在執行微任務的過程中,產生新的微任務新增到微任務佇列中,也需要一起清空;微任務佇列沒清空之前,是不會執行下一個巨集任務的。

流程圖解釋:

  1. 在Js任務入棧後判斷是否為同步任務,同步任務則立即執行;若為非同步任務則判斷微任務或巨集任務加入相應佇列等待處理。
  2. 在所有同步程式執行結束,立即執行當前微任務佇列中的全部微任務(因為整體的Js程式碼視為一個巨集任務)
  3. 當微佇列清空後,執行巨集任務佇列中的下一個巨集任務,執行結束後,判斷微任務佇列是否存在微任務,存在的話,則清空微任務佇列,開啟下一個迴圈。

Event Loop練習

console.log("a");

setTimeout(function () {
    console.log("b");
}, 0);

new Promise((resolve) => {
    console.log("c");
    resolve();
})
    .then(function () {
        console.log("d");
    })
    .then(function () {
        console.log("e");
    });

console.log("f");

/**
* 輸出結果:a c f d e b
*/
async function async1() {
    console.log("a");
    const res = await async2();
    console.log("b");
}
async function async2() {
    console.log("c");
    return 2;
}
console.log("d");
setTimeout(() => {
    console.log("e");
}, 0);
async1().then(res => {
    console.log("f")
})
new Promise((resolve) => {
    console.log("g");
    resolve();
}).then(() => {
    console.log("h");
});
console.log("i");
/**
* 輸出結果:d a c g i b h f e 
*/

這段程式碼中宣告瞭兩個async方法,

  1. 在整體的Script語句中,首先輸出:d
  2. setTimeout中的語句加入巨集任務佇列;

    巨集任務佇列微任務佇列
    整體的Script
    setTimeout
  3. async1()執行,輸出:a;後async2()執行,輸出:c;await阻斷執行;console.log("b")暫不執行,而去執行主執行緒任務;
  4. 此時async1()未執行結束,.then()方法不會被加入微任務佇列;

    巨集任務佇列微任務佇列
    整體的Script
    setTimeout
  5. 執行new Promise輸出:g;.then(() => {console.log("h");})加入微任務佇列;

    巨集任務佇列微任務佇列
    整體的Scriptconsole.log("h")
    setTimeout

    6.執行console.log("i");語句,輸出:i;此時主執行緒的同步任務已經全部執行結束,返回阻斷處繼續執行;輸出:b;.then(res => {console.log("f")})加入微佇列;

    巨集任務佇列微任務佇列
    setTimeoutconsole.log("h")
    console.log("f")

    7.清空微任務佇列後,執行巨集任務佇列的下一個任務,開啟下一個迴圈;輸出hfe

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
/**
* 輸出結果:1,7,6,8,2,4,3,5,9,11,10,12(node與瀏覽器可能略有不同)
*/

注:

  • 在node中,process.nextTick()優先順序高於Promise.then、catch、finally;
  • 在node中,巨集任務佇列:setImmediate()佇列優先順序高於setTimeout()佇列;
  • 在node14以後,也是每執行一個巨集任務、則清空即時的微任務佇列。

參考文件:

相關文章