博客 / 詳情

返回

知識訂正:瀏覽器工作原理與事件循環

  現代瀏覽器的複雜程度如同操作系統,只有日益完善的機制才能應對現今越來越複雜的網頁交互。筆者前文曾述JS單線程引起的思考,如今看來錯漏百出,知識內容早已過時。基於現在的知識積累,如今再發一文作為勘誤,希望能加深印象,有所收貨。
  如同上文的“JS單線程”,筆者之前所學還是片面的知識,JS的單線程在哪個進程之內,交互操作,代碼執行瀏覽器線程更側重誰都是一知半解。現在重新系統學習了一遍知識後,有了新的理解。
  首先從瀏覽器的工作原理來一步步解釋原因。

一.瀏覽器的進程模型

1.進程與線程:

  程序運⾏需要有它⾃⼰專屬的內存空間,可以把這塊內存空間簡單的理解為進程。每個應⽤⾄少有⼀個進程,進程之間相互獨⽴,即使要通信,也需要雙⽅同意。⼀個進程⾄少有⼀個線程,所以在進程開啓後會⾃動創建⼀個線程來運⾏代碼,該線程稱之為主線程。如果程序需要同時執⾏多塊代碼,主線程就會啓動更多的線程來執⾏代碼,所以⼀個進程中可以包含多個線程。為了避免相互影響,為了減少連環崩潰的⼏率,當啓動瀏覽器後,它會⾃動啓動多個進程。
  其中,最主要的進程有:
image.png

  1. 瀏覽器進程
    主要負責界⾯顯示(非網頁頁面顯示,如標籤頁樣子,前進後退刷新按鈕,導航欄等)、⽤户交互(如點擊按鈕,滾動滾動條等)、⼦進程管理等。瀏覽器進程內部會啓動多個線程處理不同的任務。
  2. ⽹絡進程
    負責加載⽹絡資源。⽹絡進程內部會啓動多個線程來處理不同的⽹絡任務。(如加載html,css,js,圖片,字體等內容都是由改進程去完成。那當我們的頁面足夠複雜,足夠大時,如何去加載這些網絡資源,使頁面更快進入渲染進程呢?具體見:)
  3. 渲染進程(重點)
    渲染進程啓動後,會開啓⼀個渲染主線程,主線程負責執⾏ HTML、CSS、JS 代碼。默認情況下,瀏覽器會為每個標籤⻚開啓⼀個新的渲染進程,以保證不同的標籤⻚之間不相互影響。

瀏覽器允許用户配置Renderer進程被創建的方式,簡單介紹一下幾種模型:

  1. process-per-site-instance:為每個頁面都創建一個Renderer進程,不管這些頁面是不是來自同一個域。是 Chrome 默認使用的模式,也就是幾乎所有的用户都在用的模式。
  2. process-per-site:屬於同一個域的共享同一個進程,不屬於一個域的創建不同的進程。
  3. process-per-tab:為每個標籤頁創建一個進程。
  4. single-process:不為任何頁面創建進程,所有渲染工作都在Browser進程中進行,他們是Browser進程中的多個線程。

2.渲染主進程

  渲染主線程需要處理的任務包括但不限於:

    * 解析 HTML
    * 解析 CSS
    * 計算樣式
    * 佈局
    * 處理圖層
    * 每秒把⻚⾯畫 60 次
    * 執⾏全局 JS 代碼
    * 執⾏事件處理函數
    * 執⾏計時器的回調函數
    * .....

  渲染主線程需要處理諸如此類如此多的任務,為了確保穩定的運行,就需要一種機制來做任務調度,因此,消息隊列/事件隊列應運而生。
image.png

  在最開始的時候,渲染主線程會進入一個無限循環。
  每一次循環會檢查消息隊列中是否有任務存在。如果有,先執行完微任務隊列裏的任務,再去執行宏任務隊列裏的一個任務,執行完一個宏任務後進入下一次循環;如果沒有,則進入休眠狀態。
  其他所有線程(包括其他進程的線程)可以隨時向消息隊列添加任務。新任務會加到消息隊列的末尾。在添加新任務時,如果主線程是休眠狀態,則會將其喚醒以繼續循環拿取任務。這樣一來,就可以讓每個任務有條不紊的、持續的進行下去了。
  整個過程,被稱之為事件循環 event loop消息循環 message loop )。

  在實際的代碼運行中,主要會遇到三種異步操作,如:

  • 計時完成後需要執行的任務 —— setTimeout、setInterval(計時線程)
  • 網絡通信完成後需要執行的任務 – XHR、Fetch(網絡線程)
  • 用户操作後需要執行的任務 – addEventListener(交互線程)

  這時,瀏覽器通過 異步 方式來解決上述三種方式可能導致的阻塞問題。 如下圖所示:
image.png
  當存在 setTimeout 時,將其代碼放入“計時線程”內開始計時,渲染主線程繼續運行同步代碼,若渲染主線程沒有任務時,則從消息隊列(事件隊列)內拿取任務來運行,而計時結束後,其回調函數會放入消息隊列(事件隊列)尾端,等待主線程拿取。
注:當遇到同步延時操作時,瀏覽器無法像異步操作那樣調用其他線程防止阻塞,只能等待渲染主線程運行完代碼後繼續執行下一步操作,這段時間會造成阻塞導致頁面卡死。

  • 一道面試問答題
如何理解 JS 的異步?

JS是一門單線程的語言,這是因為它運行在瀏覽器的渲染主線程中,而渲染主線程只有一個。

而渲染主線程承擔着諸多的工作,渲染頁面、執行 JS 都在其中運行。

如果使用同步的方式,就極有可能導致主線程產生阻塞,從而導致消息隊列中的很多其他任務無法得到執行。這樣一來,一方面會導致繁忙的主線程白白的消耗時間,另一方面導致頁面無法及時更新,給用户造成卡死現象。

所以瀏覽器採用異步的方式來避免。具體做法是當某些任務發生時,比如計時器、網絡、事件監聽,主線程將任務交給其他線程去處理,自身立即結束任務的執行,轉而執行後續代碼。當其他線程完成時,將事先傳遞的回調函數包裝成任務,加入到消息隊列的末尾排隊,等待主線程調度執行。

在這種異步模式下,瀏覽器永不阻塞,從而最大限度的保證了單線程的流暢運行。

  上述異步操作會產生任務,任務是沒有優先級的,在消息隊列(事件隊列)中先進先出。但是消息隊列有優先級。

  • 每個任務都有⼀個任務類型,同⼀個類型的任務必須在⼀個隊列,不同類型的任務可以分屬於不同的隊列。在⼀次事件循環中,瀏覽器可以根據實際情況從不同的隊列中取出任務執⾏。
  • 瀏覽器必須準備好⼀個微隊列,微隊列中的任務優先所有其他任務執⾏。

  在⽬前 chrome 的實現中,⾄少包含了下⾯的隊列:

  • 延時隊列:⽤於存放計時器到達後的回調任務,優先級「」。
  • 交互隊列:⽤於存放⽤户操作後產⽣的事件處理任務,優先級「」。
  • 微(任務)隊列:⽤户存放需要最快執⾏的任務,優先級「最⾼」。

注:添加任務到微隊列的主要⽅式主要是使⽤ PromiseMutationObserver

  因此,沒有宏(任務)隊列,已經沒有宏(任務)概念。做了更清晰的劃分。當上述代碼產生的任務會被放入對應的隊列內被渲染主線程按優先級高低依次取出,直至隊列內沒有任務,且渲染主線程也沒有任務為止,渲染主線程會進入休眠主題,添加新任務時,渲染主線程會被喚醒繼續循環拿取任務來執行。

  • 幾道面試題:

1.常見的輸出題

function a() {
  //fn1
  console.log(1);
  Promise.resolve().then(() => {
      // fn2
    console.log(2);
  });
}
setTimeout(() => {
  // fn3
  console.log(3);
  Promise.resolve().then(a);
}, 0);
Promise.resolve().then(() => {
  // fn4
  console.log(4);
});

console.log(5);

// 執行結果輸出: 5 4 3 1 2

2.闡述⼀下 JS 的事件循環

    事件循環⼜叫做消息循環,是瀏覽器渲染主線程的⼯作⽅式。
    在 Chrome 的源碼中,它開啓⼀個不會結束的 for 循環,每次循環從消息
隊列中取出第⼀個任務執⾏,⽽其他線程只需要在合適的時候將任務加⼊到隊列
末尾即可。
    過去把消息隊列簡單分為宏隊列和微隊列,這種説法⽬前已⽆法滿⾜複雜的
瀏覽器環境,取⽽代之的是⼀種更加靈活多變的處理⽅式。
    根據 W3C 官⽅的解釋,每個任務有不同的類型,同類型的任務必須在同⼀
個隊列,不同的任務可以屬於不同的隊列。不同任務隊列有不同的優先級,在
⼀次事件循環中,由瀏覽器⾃⾏決定取哪⼀個隊列的任務。但瀏覽器必須有⼀個
微隊列,微隊列的任務⼀定具有最⾼的優先級,必須優先調度執⾏。

3.JS 中的計時器能做到精確計時嗎?為什麼?

答:不⾏,因為:
1. 計算機硬件沒有原⼦鍾,⽆法做到精確計時。
2. 操作系統的計時函數本身就有少量偏差,由於 JS 的計時器最終調⽤的
是操作系統的函數,也就攜帶了這些偏差。
3. 按照 W3C 的標準,瀏覽器實現計時器時,如果嵌套層級超過 5 層,
則會帶有 4 毫秒的最少時間,這樣在計時時間少於 4 毫秒時⼜帶來
了偏差。
4. 受事件循環的影響,計時器的回調函數只能在主線程空閒時運⾏,因此
⼜帶來了偏差。
user avatar zzd41 頭像 lesini 頭像 pangsir8983 頭像 william_wang_5f4c69a02c77b 頭像 tofrankie 頭像 fyuanlove 頭像 yangkaiqiang 頭像 amsterdam_5caf807441f49 頭像 harryfyodor 頭像 lawler61 頭像 zpfei 頭像 mianduijifengba_59b206479620f 頭像
13 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.