瀏覽器的事件環機制

getvalue發表於2018-08-20

今天我們來了解一下瀏覽器的事件環機制。

瀏覽器模型

瀏覽器的事件環機制

  • User Interface(使用者介面)-包括位址列、前進/後退按鈕、書籤選單等
  • Browser engine(瀏覽器引擎)-在使用者介面和呈現引擎之間傳送指令
  • Rendering engine(呈現引擎)-又稱渲染引擎,也被稱為瀏覽器核心,線上程方面又稱為UI執行緒
  • Networking(網路)-用於網路呼叫,比如 HTTP 請求
  • UI Backend(使用者介面後端)-用於繪製基本的視窗小部件(UI執行緒)
  • JavaScript直譯器-用於解析和執行 JavaScript 程式碼(JS執行緒)
  • Data Persistence(資料儲存)-這是持久層。瀏覽器需要在硬碟上儲存各種資料,例如 Cookie

注意:UI執行緒和JS執行緒是互斥的,因為它們兩個共用一個執行緒,即主執行緒。

JS是單執行緒的

為什麼JS要設計成單執行緒的?這是由Javascript這門指令碼語言的用途決定的。JS的主要工作是操作DOM,如果設計成多執行緒的,兩個執行緒對同一個元素進行操作,瀏覽器就不知道該如何處理了。單執行緒的好處是,我操作的時候你等著,等我操作完成後,你再進行操作,避免衝突。

Philip Roberts的演講圖片

這張圖相信大家在很多文章中都見過。

瀏覽器的事件環機制

對於上圖我找到一個更容易理解的,如下圖:

瀏覽器的事件環機制

圖片來源:segmentfault.com/a/119000001…

對圖片的解釋

主執行緒執行的時候,產生堆(heap)和棧(stack)。在對一個呼叫棧中的程式碼進行操作的時候,其他的都要等著。在操作過程中遇到一些類似於setTimeout等非同步操作的時候,會交給瀏覽器的其他模組進行處理。在這些非同步操作達到特定條件(定時器等待指定時間之後,ajax請求返回資料)時,把相應的回撥函式放入指定的“任務佇列”。只要棧中的程式碼執行完畢,主執行緒就會去讀取"任務佇列",依次執行那些事件所對應的回撥函式。

任務佇列

大家可以看到圖片中寫的:巨集任務佇列和微任務佇列。如果大家想詳細瞭解可以看下規範。其中微任務要早於巨集任務執行。

常見任務佇列

  • 巨集任務:script(全域性任務), setTimeout, setInterval, setImmediate, I/O, UI rendering, MessageChannel
  • 微任務:Promise, Object.observer, MutationObserver.

測試程式碼

在html頁面寫如下程式碼:

<script>
    console.log(1);
    setTimeout(function () {
        console.log("4");
    }, 0);
    Promise.resolve().then(() => {
        console.log("3");
    });
    console.log(2);
</script>
<script src="out.js"></script>
<script>
    console.log("a");
    Promise.resolve().then(() => {
        console.log("c");
    });
    setTimeout(function () {
        console.log("d");
    }, 0);
    console.log("b");
</script>

<script>
    console.log("end");
</script>
複製程式碼

其中 out.js程式碼如下:

console.log("out1");
setTimeout(function () {
    console.log("out4");
}, 0);
Promise.resolve().then(() => {
    console.log("out3");
});
console.log("out2");
複製程式碼

輸出結果:

  • 1
  • 2
  • 3
  • out1
  • out2
  • out3
  • a
  • b
  • c
  • end
  • 4
  • out4
  • d

根據程式碼得出結論

前提:script(全域性任務)是巨集任務 分析:當主執行緒遇到上面程式碼時,會把所有的script標籤以及外部的js檔案放入巨集任務佇列中(先後順序就是書寫的順序)。此時主任務佇列中沒有可執行的程式碼。所以就取巨集任務佇列中的第一個巨集任務

console.log(1);
setTimeout(function () {
    console.log("4");
}, 0);
Promise.resolve().then(() => {
    console.log("3");
});
console.log(2);
複製程式碼

先輸出 1;遇到setTimeout交給其它模組執行,在到達指定時間(10毫秒或16毫秒)之後會把回撥函式放到巨集任務佇列最後;遇到Promise同樣交給其它模組執行,達到條件之後放到微任務佇列;再輸出 2。此時巨集任務佇列該執行 out.js檔案了,但是微任務佇列中已經有微任務在排隊了(Promise.resolve().then()中的回撥函式)。微任務要早於巨集任務執行,所以要先輸出 3。再去執行out.js中的程式碼。out.js中的程式碼以及後面script標籤內的程式碼和上面程式碼類似,就不再一一贅述。所以當輸出 c 之後。棧(stack)和微任務佇列中已沒有可以執行的程式碼。剩下的是巨集任務佇列中的程式碼:依次是

<script>
    console.log("end");
</script>

function () {
    console.log("4");
}

function () {
    console.log("out4");
}

function () {
    console.log("d");
}
複製程式碼

然後依次放到主棧中執行,輸出:end 4 out4 d 其它巨集任務和微任務都遵循這個規則,就不一一舉例了。

最後做一下總結:

  • 瀏覽器中的巨集任務:script(全域性任務), setTimeout, setInterval, setImmediate, I/O, UI rendering, MessageChannel

  • 瀏覽器中的微任務:Promise, Object.observer, MutationObserver.

  • 瀏覽器的事件環機制:

    • 1.所有同步任務都在主執行緒上執行,形成一個執行棧;主執行緒之外,還存在一個任務佇列。只要非同步任務有了執行結果,就在任務佇列之中放置一個事件;
    • 2.執行棧執行過程中,遇到非同步操作就交給其他模組處理,只要非同步任務有了執行結果,就在任務佇列之中放置一個事件(巨集任務放到巨集任務佇列,微任務放到微任務佇列);
    • 3.一旦執行棧中的所有同步任務執行完畢,系統就會依次讀取微任務佇列中的全部微任務放到主棧中執行(執行微任務的時候執行第2步);
    • 4.清空微任務佇列之後,讀取一個巨集任務,放到主棧中執行(執行巨集任務的時候執行第2步)。執行完畢後再去清空微任務佇列中的微任務。。。
    • 5.主執行緒不斷重複上面的第3、4步。

參考連結

相關文章