首頁 > 軟體

一篇文章讓你搞清楚JavaScript事件迴圈

2022-06-09 14:00:06

前言

非同步函數也是有執行順序的。本質上來說,JavaScript是單執行緒語言,不管是在瀏覽器中還是nodejs環境下。瀏覽器在執行js程式碼和渲染DOM節點都是在同一個執行緒中,執行js程式碼就無法渲染DOM,渲染DOM的時候就無法執行js程式碼。如果按照這種同步方式執行,頁面的渲染將會出現白屏甚至是報錯,特別是遇到一些耗時比較長的網路請求或者js程式碼,因此在實際開發中一般是通過非同步的方式解決。

什麼是非同步?js是一步一步執行程式碼的,遇到alert這種阻塞程式碼時,js將會停止往下執行直到阻塞程式碼執行完畢。非同步就是將函數放在單獨的非同步佇列中,不會產生阻塞,js可以繼續往下執行,等到同步程式碼執行完畢後再執行非同步佇列中的函數。因此,js會先執行完同步程式碼,才會執行非同步程式碼。非同步函數之間,雖然都是非同步,但是還是有相對的執行順序。

非同步函數的執行主要依靠事件迴圈來處理,本文重點探討非同步的分類(宏任務、微任務)、事件迴圈以及非同步函數的執行順序。

宏任務

宏任務,也可簡單的說成是任務,在下一輪DOM渲染之後執行。常見的宏任務有:

  • setTimeout:設定一個定時器,該定時器會在設定的延遲時間到期後執行一個函數或者指定的程式碼塊。值得注意的是,setTimeout不一定會在延遲時間到達後就立即執行函數,而是會判斷執行佇列中是否還有函數沒有處理,如果沒有了並且棧為空,setTimeout才會在延遲時間到達後執行函數。

    // setTimeout 延遲執行不等於到期時立即執行
    let now = new Date().getSeconds();
    setTimeout(() => {
        console.log('this is setTimeout 0');
    }, 0);
    setTimeout(() => {
        console.log('this is setTimeout 200');
    }, 200);
    while(true) {
        if (new Date().getSeconds() - now >= 2) {
            console.log('break out while loop');
            break;
        }
    }

    執行結果

  • break out while loop
    this is setTimeout 0
    this is setTimeout 200

    先執行同步程式碼,再執行非同步。setTimeout(() => {}, 0)表示0毫秒後立即執行函數,但是當前執行佇列中還有未處理完的while迴圈,因此需要等到while迴圈執行完畢後,才會根據延遲到期時間執行函數。

  • setInterval:設定定時器,表示在固定的時間間隔內,重複執行某一函數或者特定的程式碼塊。注意使用setInterval有最小延遲時間限制以及確保執行時間要小於間隔時間,如果執行時間無法確定,則應採用遞迴呼叫setTimeout的方式代替。

  • 網路請求:只要是指XMLHttpRequest等網路請求

微任務

微任務,在下一輪DOM渲染之前執行,微任務比宏任務更早執。常見的微任務有:

  • promise:表示一個非同步操作最終的結果和返回值,可能會失敗,也可能成功。非同步函數在執行時,什麼時候返回結果是不可預料的,Promise把非同步操作的返回值和函數關聯起來,保證在非同步執行結束後會執行對應的函數,並通過函數返回操作值。這種效果就類似於把非同步程式碼“同步執行”。
  • queueMicrotask:將函數新增到微任務隊
console.log('start');
// 微任務佇列
Promise.resolve().then(() => {
    console.log('promise then');
});
queueMicrotask(() => {
    console.log('queueMicrotask');
});
console.log('end');

執行結果

start
end
promise then
queueMicrotask

事件迴圈

因為有非同步操作的存在,所以出現了事件迴圈,如果都是同步操作,一行一行執行程式碼,事件迴圈也就失去了用武之地。在瞭解事件迴圈前,還需要補充js的執行過程:

js在執行程式碼時,遇到函數就會將其新增到呼叫棧中,每一幀都會儲存當前函數的引數和區域性變數,當一個函數執行完畢,則會從呼叫棧中彈出,直到棧被清空,那麼程式也就執行完畢。在執行的過程中,需要的參照資料都是從堆中獲取。

在實際開發中,往往是同步程式碼和非同步程式碼都有。在js執行時,還是從第一行程式碼開始執行,遇到函數就將其新增到棧中,然後執行同步操作;如果遇到非同步函數,則根據其型別,宏任務就新增到宏任務佇列,微任務新增到微任務佇列。直到同步程式碼執行完畢,則開始執行非同步操作。

非同步操作後於同步操作,非同步操作內部也是分先後順序的。總的來說:

  • 微任務先於宏任務執行
  • 微任務與微任務之間根據先後順序執行,宏任務與宏任務之間根據延遲時間順序執行
  • 微任務在下一輪DOM渲染前執行,宏任務在下一輪DOM渲染之後執行
  • 每個任務的執行都是一次出棧操作,直到棧被清空

微任務比宏任務先執行

console.log('start');
// 宏任務佇列
setTimeout(() => {
    console.log('setTimeout');
});
// 微任務佇列
Promise.resolve().then(() => {
    console.log('promise then');
});
console.log('end');
// 執行結果
start
end
promise then
setTimeout

微任務在下一輪DOM渲染前執行,宏任務在之後執行

let div = document.createElement('div');
div.innerHTML = 'hello world';
document.body.appendChild(div);
let divList = document.getElementByTagName('div');
console.log('同步任務 length ---', list.length);
console.log('start');
setTimeout(() => {
    console.log('setTimeout length ---', list.length);
    alert('宏任務 setTimeout 阻塞'); // 使用alert阻塞js執行
});
Promise.resolve().then(() => {
    console.log('promise then length ---', list.length);
    alert('微任務 promise then 阻塞);
});
console.log('end');

事件迴圈

event loop會持續監聽是否有非同步操作,如果有則新增到對應的佇列中,等待執行。例如在宏任務中新增微任務,或者在微任務中新增宏任務,當前任務執行完後,可能還會有新的任務新增到事件迴圈中。

宏任務與微任務

  • 微任務中建立宏任務

        new Promise((resolve) => {
          console.log('promise 1');
          setTimeout(() => {
            console.log('setTimeout 1');
          }, 500);
          resolve();
        }).then(() => {
          console.log('promise then');
          setTimeout(() => {
            console.log('setTimeout 2');
          }, 0);
        });
        new Promise((resolve) => {
          console.log('promise 2');
          resolve();
        })

    執行結果

  • promise 1
    promise 2
    promise then
    setTimeout 2
    setTimeout 1

    解析

    js執行程式碼,遇到兩個Promise,則分別新增到微任務佇列,同步程式碼執行完畢。

    在微任務佇列中根據先進先出,第一個Promise先執行,遇到setTimeout,則新增到宏任務佇列,resolve()返回執行結果並執行then,事件迴圈將其繼續新增到微任務佇列;第一個Promise執行完畢,執行第二個Promise。

    繼續執行微任務佇列,直到清空佇列。遇到setTimeout,並將其新增到宏任務佇列

    宏任務佇列現在有兩個任務待執行,由於第二個setTimeout的延遲事件更小,則優先執行第二個;如果相等,則按照順序執行。

    繼續執行宏任務佇列,直到清空佇列。

  • 宏任務中建立微任務

        setTimeout(() => {
          console.log('setTimeout 1');
          new Promise((resolve) => {
            console.log('promise 1');
            resolve();
          }).then(() => {
            console.log('promise then');
          })
        }, 500);
        setTimeout(() => {
          console.log('setTimeout 2');
          new Promise((resolve) => {
            console.log('promise 2');
            resolve();
          })
        }, 0);

    執行結果

  • setTimeout 2
    promise 2
    setTimeout 1
    promise 1
    promise then

    解析

    js執行程式碼,遇到兩個setTimeout,將其新增到宏任務佇列,同步程式碼執行完畢

    先檢查微任務佇列中是否有待處理的,剛開始肯定沒有,因此直接執行宏任務佇列中的任務。第二個為零延遲,需要優先執行。遇到Promise,將其新增到微任務佇列。第一個宏任務執行完畢

    在執行第二個宏任務時,微任務佇列中已經存在待處理的,因此需要先執行微任務。

    微任務執行完畢,並且延遲時間到期,第一個setTimeout開始執行。遇到Promise,將其新增到微任務佇列中

    執行微任務佇列中的Promise,執行完畢後遇到then,則將其繼續新增到微任務佇列

    直到所有微任務執行完畢

  • 宏任務中建立宏任務

        setTimeout(() => {
          console.log('setTimeout 1');
          setTimeout(() => {
            console.log('setTimeout 2');
          }, 500);
          setTimeout(() => {
            console.log('setTimeout 3');
          }, 500);
          setTimeout(() => {
            console.log('setTimeout 4');
          }, 100);
        }, 0);

    執行結果

  • setTimeout 1
    setTimeout 4
    setTimeout 2
    setTimeout 3

    解析

    宏任務中建立宏任務,執行順序一般來說是按照先後順序的。對於setTImeout來說,延遲時間相同,則按照先後順序執行;延遲時間不同,則按照延遲時間的大小先後順序執行

  • 微任務中建立微任務

        new Promise((resolve) => {
          console.log('promise 1');
          new Promise((resolve) => {
            console.log('promise 2');
            resolve();
          });
          new Promise((resolve) => {
            console.log('promise 3');
            resolve();
          })
          resolve();
        })

    執行結果

  • promise 1
    promise 2
    promise 3

    解析

    微任務中建立微任務,執行順序一般來說是按照先後順序執行的。

總結

  • 同步程式碼直接執行,非同步程式碼新增到宏任務佇列或者微任務佇列
  • 微任務在下一輪DOM渲染前執行,宏任務在下一輪DOM渲染之後執行
  • 事件迴圈持續監聽
  • 如果存在非同步操作,需要將關聯程式碼放在非同步函數中執行;或者將非同步函數轉為同步操作
  • 如果程式碼層次比較複雜,同步、非同步程式碼混雜,一定要理清程式碼的執行順序。避免因為非同步,導致程式碼出現難以察覺的bug

參考資料

到此這篇關於JavaScript事件迴圈的文章就介紹到這了,更多相關JS事件迴圈內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com