淺談瀏覽器架構、單線程js、事件循環、消息隊列、宏任務和微任務

瀏覽器原理.jpg

關鍵詞:多進程、單線程、事件循環、消息隊列、宏任務、微任務css

看到這些詞彷彿比較讓人摸不着頭腦,其實在咱們的平常開發中,早就和他們打過交道了。前端

我來舉幾個常見的例子:git

  • 我執行了一段js,頁面就卡了挺久纔有響應
  • 我觸發了一個按鈕的click事件,click事件處理器作出了響應
  • 我用setTimeout(callback, 1000)給代碼加了1s的延時,1秒裏發生了不少事情,而後功能正常了
  • 我用setInterval(callback, 100)給代碼加了100ms的時間輪訓,直到期待的那個變量出現再執行後續的代碼,而且結合setTimeout刪除這個定時器
  • 我用Promise,async/await順序執行了異步代碼
  • 我用EventEmitter、new Vue()作事件廣播訂閱
  • 我用MutationObserver監聽了DOM更新
  • 我手寫了一個Event類作事件的廣播訂閱
  • 我用CustomEvent建立了自定義事件
  • 我·······

其實上面舉的這些click, setTimeout, setInterval, Promise,async/await, EventEmitter, MutationObserver, Event類, CustomEvent多進程、單線程、事件循環、消息隊列、宏任務、微任務或多或少的都有所聯繫。github

並且也與瀏覽器的運行原理有一些關係,做爲天天在瀏覽器裏辛勤耕耘的前端工程師們,瀏覽器的運行原理(多進程、單線程、事件循環、消息隊列、宏任務、微任務)能夠說是必需要掌握的內容了,不只對面試有用,對手上負責的開發工做也有很大的幫助。web

  • 淺談瀏覽器架構面試

    • 瀏覽器能夠是哪一種架構?
    • 如何理解Chrome的多進程架構?
    • 前端最核心的渲染進程包含哪些線程?segmentfault

      • 主線程(Main thread)(下載資源、執行js、計算樣式、進行佈局、繪製合成)
      • 光柵線程(Raster thread)
      • 合成線程(Compositor thread)
      • 工做線程(Worker thread)
  • 淺談單線程js瀏覽器

    • js引擎圖
    • 什麼是單線程js?
    • 單線程js屬於瀏覽器的哪一個進程?
    • js爲何要設計成單線程的?
  • 事件循環與消息隊列微信

    • 什麼是事件循環?
    • 什麼是消息隊列?
    • 如何實現一個 EventEmitter(支持 on,once,off,emit)?
  • 宏任務和微任務網絡

    • 哪些屬於宏任務?
    • 哪些屬於微任務?
    • 事件循環,消息隊列與宏任務、微任務之間的關係是什麼?
    • 微任務添加和執行流程示意圖
  • 瀏覽器頁面循環系統原理圖

    • 消息隊列和事件循環
    • setTimeout
    • XMLHttpRequest
    • 宏任務
  • 參考資料

淺談Chrome架構

瀏覽器能夠是哪一種架構?

瀏覽器本質上也是一個軟件,它運行於操做系統之上,通常來講會在特定的一個端口開啓一個進程去運行這個軟件,開啓進程以後,計算機爲這個進程分配CPU資源、運行時內存,磁盤空間以及網絡資源等等,一般會爲其指定一個PID來表明它。

先來看看個人機器上運行的微信和Chrome的進程詳情

軟件 CPU(%) 線程 PID 內存 端口
微信 0.1 46 587 555MB 124301
Chrome 7.9 48 481 603MB 1487

若是本身設計一個瀏覽器,瀏覽器能夠是那種架構呢?

  • 單進程架構(線程間通訊)
  • 多進程架構(進程間IPC通訊)

若是瀏覽器單進程架構的話,須要在一個進程內作到網絡、調度、UI、存儲、GPU、設備、渲染、插件等等任務,一般來講能夠爲每一個任務開啓一個線程,造成單進程多線程的瀏覽器架構。

可是因爲這些功能的日益複雜,例如將網絡,存儲,UI放在一個線程中的話,執行效率和性能愈來愈地下,不能再向下拆分出相似「線程」的子空間

所以,爲了逐漸強化瀏覽器的功能,因而產生了多進程架構的瀏覽器,能夠將網絡、調度、UI、存儲、GPU、設備、渲染、插件等等任務分配給多個單獨的進程,在每個單獨的進程內,又能夠拆分出多個子線程,極大程度地強化了瀏覽器。

如何理解Chrome的多進程架構?

Chrome做爲瀏覽器屆裏的一哥,他也是多進程IPC架構的。
image

Chrome多進程架構主要包括如下4個進程:

  • Browser進程(負責地址欄、書籤欄、前進後退、網絡請求、文件訪問等)
  • Renderer進程(負責一個Tab內全部和網頁渲染有關的全部事情,是最核心的進程
  • GPU進程(負責GPU相關的任務)
  • Plugin進程(負責Chrome插件相關的任務)

Chrome 多進程架構的優缺點
優勢

  • 每個Tab就是要給單獨的進程
  • 因爲每一個Tab都有本身獨立的Renderer進程,所以某一個Tab出問題不會影響其它Tab

缺點

  • Tab間內存不共享,不一樣進程內存包含相同內容

Chrome多進程架構實錘圖
image

前端最核心的渲染(Renderer)進程包含哪些線程?

image

渲染進程主要包括4個線程:

  • 主線程(Main thread)(下載資源、執行js、計算樣式、進行佈局、繪製合成)
  • 光柵線程(Raster thread)
  • 合成線程(Compositor thread)
  • 工做線程(Worker thread)

渲染進程的主線程知識點:

  • 下載資源:主線程能夠經過Browser進程的network線程下載圖片,css,js等渲染DOM須要的資源文件
  • 執行JS:主線程在遇到<script>標籤時,會下載而且執行js,執行js時,爲了不改變DOM的結構,解析HTML停滯,js執行完成後繼續解析HTML。正是由於JS執行會阻塞UI渲染,而JS又是瀏覽器的一哥,所以瀏覽器經常被看作是單線程的。
  • 計算樣式:主線程會基於CSS選擇器或者瀏覽器默認樣式去進行樣式計算,最終生成Computed Style
  • 進行佈局:主線程計算好樣式之後,能夠肯定元素的位置信息以及盒模型信息,對元素進行佈局
  • 進行繪製:主線程根據前後順序以及層級關係對元素進行渲染,一般會生成多個圖層
  • 最終合成:主線程將渲染後的多個frame(幀)合成,相似flash的幀動畫和PS的圖層

渲染進程的主線程細節能夠查閱Chrome官方的博客:Inside look at modern web browser (part 3)Rendering Performance

渲染進程的合成線程知識點:

  • 瀏覽器滾動時,合成線程會建立一個新的合成幀發送給GPU
  • 合成線程工做與主線程無關,不用等待樣式計算或者JS的執行,所以合成線程相關的動畫比涉及到主線程從新計算樣式和js的動畫更加流暢

下面來看下主線程、合成線程和光柵線程一塊兒做用的過程
1.主線程主要遍歷佈局樹生成層樹
image
2.柵格線程柵格化磁貼到GPU
image
3.合成線程將磁貼合成幀並經過IPC傳遞給Browser進程,顯示在屏幕上
image

圖片引自Chrome官方博客:Inside look at modern web browser (part 3)

淺談單線程js

js引擎圖

應用程序(實現) 方言和最後版本 ECMAScript版本
Google Chrome,V8引擎 JavaScript ECMA-262,版本6
Mozilla Firefox,Gecko排版引擎,SpiderMonkey和Rhino JavaScript 1.8.5 ECMA-262,版本6
Safari,Nitro引擎 JavaScript ECMA-262,版本6
Microsoft Edge,Chakra引擎 JavaScript EMCA-262,版本6
Opera,Carakan引擎(改用V8以前) 一些JavaScript 1.5特性及一些JScript擴展[12] ECMA-262,版本5.1
KHTML排版引擎,KDE項目的Konqueror JavaScript 1.5 ECMA-262,版本3
Adobe Acrobat JavaScript 1.5 ECMA-262,版本3
OpenLaszlo JavaScript 1.4 ECMA-262,版本3
Max/MSP JavaScript 1.5 ECMA-262,版本3
ANT Galio 3 JavaScript 1.5附帶RMAI擴展 ECMA-262,版本3

什麼是單線程js?

若是仔細閱讀過第一部分「談談瀏覽器架構」的話,這個答案其實已經很是顯而易見了。
在」前端最核心的渲染進程包含哪些線程?「這裏咱們提到了主線程(Main thread)(下載資源、執行js、計算樣式、進行佈局、繪製合成,注意其中的執行js,這裏其實已經明確告訴了咱們Chrome中JavaScript運行的位置。

那麼Chrome中JavaScript運行的位置在哪裏呢?

渲染進程(Renderer Process)中的主線程(Main Thread)

單線程js屬於瀏覽器的哪一個進程?

單線程的js -> 主線程(Main Thread)-> 渲染進程(Renderer Process)

js爲何要設計成單線程的?

其實更爲嚴謹的表述是:「瀏覽器中的js執行和UI渲染是在一個線程中順序發生的。」

這是由於在渲染進程的主線程在解析HTML生成DOM樹的過程當中,若是此時執行JS,主線程會主動暫停解析HTML,先去執行JS,等JS解析完成後,再繼續解析HTML。

那麼爲何要「主線程會主動暫停解析HTML,先去執行JS,再繼續解析HTML呢」?

這是主線程在解析HTML生成DOM樹的過程當中會執行style,layout,render以及composite的操做,而JS能夠操做DOM,CSSOM,會影響到主線程在解析HTML的最終渲染結果,最終頁面的渲染結果將變得不可預見。

若是主線程一邊解析HTML進行渲染,JS同時在操做DOM或者CSSOM,結果會分爲如下狀況:

  • 以主線程解析HTML的渲染結果爲準
  • 以JS同時在操做DOM或者CSSOM的渲染結果爲準

考慮到最終頁面的渲染效果的一致性,因此js在瀏覽器中的實現,被設計成爲了JS執行阻塞UI渲染型。

事件循環

什麼是事件循環?

事件循環英文名叫作Event Loop,是一個在前端屆老生常談的話題。
我也簡單說一下我對事件循環的認識:

事件循環能夠拆爲「事件」+「循環」。
先來聊聊「事件」:

若是你有必定的前端開發經驗,對於下面的「事件」必定不陌生:

  • click、mouseover等等交互事件
  • 事件冒泡、事件捕獲、事件委託等等
  • addEventListener、removeEventListener()
  • CustomEvent(自定義事件實現自定義交互)
  • EventEmitter、EventBus(on,emit,once,off,這種東西常常出面試題)
  • 第三方庫的事件系統

有事件,就有事件處理器:在事件處理器中,咱們會應對這個事件作一些特殊操做。

那麼瀏覽器怎麼知道有事件發生了呢?怎麼知道用戶對某個button作了一次click呢?

若是咱們的主線程只是靜態的,沒有循環的話,能夠用js僞代碼將其表述爲:

function mainThread() {
     console.log("Hello World!");
     console.log("Hello JavaScript!");
}
mainThread();

執行完一次mainThread()以後,這段代碼就無效了,mainThread並非一種激活狀態,對於I/O事件是沒有辦法捕獲到的。

所以對事件加入了「循環」,將渲染進程的主線程變爲激活狀態,能夠用js僞代碼表述以下:

// click event
function clickTrigger() {
    return "我點擊按鈕了"
}
// 能夠是while循環
function mainThread(){
    while(true){
        if(clickTrigger()) { console.log(「通知click事件監聽器」) }
        clickTrigger = null;
     }
}
mainThread();

也能夠是for循環

for(;;){
    if(clickTrigger()) { console.log(「通知click事件監聽器」) }
    clickTrigger = null;
}

在事件監聽器中作出響應:

button.addEventListener('click', ()=>{
    console.log("多虧了事件循環,我(瀏覽器)才能知道用戶作了什麼操做");
})

什麼是消息隊列?

消息隊列能夠拆爲「消息」+「隊列」。
消息能夠理解爲用戶I/O;隊列就是先進先出的數據結構。
而消息隊列,則是用於鏈接用戶I/O與事件循環的橋樑。

隊列數據結構圖

image

入隊出隊圖

image

在js中,如何發現出隊列FIFO的特性?

下面這個結構你們都熟悉,瞬間體現出隊列FIFO的特性。

// 定義一個隊列
let queue = [1,2,3];
// 入隊
queue.push(4); // queue[1,2,3,4]
// 出隊
queue.shift(); // 1 queue [2,3,4]

假設用戶作出了"click button1","click button3","click button 2"的操做。
事件隊列定義爲:

const taskQueue = ["click button1","click button3","click button 2"];
while(taskQueue.length>0){
    taskQueue.shift(); // 任務依次出隊
}

任務依次出隊:
"click button1"
"click button3"
"click button 2"

此時因爲mainThread有事件循環,它會被瀏覽器渲染進程的主線程事件循環系統捕獲,並在對應的事件處理器作出響應。

button1.addEventListener('click', ()=>{
    console.log("click button1");
})
button2.addEventListener('click', ()=>{
    console.log("click button 2");
})
button3.addEventListener('click', ()=>{
   console.log("click button3")
})

依次打印:"click button1","click button3","click button 2"。

所以,能夠將消息隊列理解爲鏈接用戶I/O操做和瀏覽器事件循環系統的任務隊列

如何實現一個 EventEmitter(支持 on,once,off,emit)?

/**
 * 說明:簡單實現一個事件訂閱機制,具備監聽on和觸發emit方法
 * 示例:
 * on(event, func){ ... }
 * emit(event, ...args){ ... }
 * once(event, func){ ... }
 * off(event, func){ ... }
 * const event = new EventEmitter();
 * event.on('someEvent', (...args) => {
 *     console.log('some_event triggered', ...args);
 * });
 * event.emit('someEvent', 'abc', '123');
 * event.once('someEvent', (...args) => {
 *     console.log('some_event triggered', ...args);
 * });
 * event.off('someEvent', callbackPointer); // callbackPointer爲回調指針,不能是匿名函數
 */

class EventEmitter {
  constructor() {
    this.listeners = [];
  }
  on(event, func) {
    const callback = () => (listener) => listener.name === event;
    const idx = this.listeners.findIndex(callback);
    if (idx === -1) {
      this.listeners.push({
        name: event,
        callbacks: [func],
      });
    } else {
      this.listeners[idx].callbacks.push(func);
    }
  }
  emit(event, ...args) {
    if (this.listeners.length === 0) return;
    const callback = () => (listener) => listener.name === event;
    const idx = this.listeners.findIndex(callback);
    this.listeners[idx].callbacks.forEach((cb) => {
      cb(...args);
    });
  }
  once(event, func) {
    const callback = () => (listener) => listener.name === event;
    let idx = this.listeners.findIndex(callback);
    if (idx === -1) {
      this.listeners.push({
        name: event,
        callbacks: [func],
      });
    }
  }
  off(event, func) {
    if (this.listeners.length === 0) return;
    const callback = () => (listener) => listener.name === event;
    let idx = this.listeners.findIndex(callback);
    if (idx !== -1) {
      let callbacks = this.listeners[idx].callbacks;
      for (let i = 0; i < callbacks.length; i++) {
        if (callbacks[i] === func) {
          callbacks.splice(i, 1);
          break;
        }
      }
    }
  }
}

// let event = new EventEmitter();
// let onceCallback = (...args) => {
//   console.log("once_event triggered", ...args);
// };
// let onceCallback1 = (...args) => {
//   console.log("once_event 1 triggered", ...args);
// };
// // once僅監聽一次
// event.once("onceEvent", onceCallback);
// event.once("onceEvent", onceCallback1);
// event.emit("onceEvent", "abc", "123");

// // off銷燬指定回調
// let onCallback = (...args) => {
//   console.log("on_event triggered", ...args);
// };
// let onCallback1 = (...args) => {
//   console.log("on_event 1 triggered", ...args);
// };
// event.on("onEvent", onCallback);
// event.on("onEvent", onCallback1);
// event.emit("onEvent", "abc", "123");

// event.off("onEvent", onCallback);
// event.emit("onEvent", "abc", "123");

宏任務和微任務

  • 哪些屬於宏任務?
  • 哪些屬於微任務?
  • 事件循環,消息隊列與宏任務、微任務之間的關係是什麼?
  • 微任務添加和執行流程示意圖

哪些屬於宏任務?

  • setTimeout
  • setInterval
  • setImmediate
  • requestAnimationFrame
  • I/O
  • UI渲染

哪些屬於微任務?

  • Promise
  • MutationObserver
  • process.nextTick
  • queueMicrotask

事件循環,消息隊列與宏任務、微任務之間的關係是什麼?

  • 宏任務入隊消息隊列,能夠將消息隊列理解爲宏任務隊列
  • 每一個宏任務內有一個微任務隊列,執行過程當中微任務入隊當前宏任務的微任務隊列
  • 宏任務微任務隊列爲空時纔會執行下一個宏任務
  • 事件循環捕獲隊列出隊的宏任務和微任務並執行

事件循環會不斷地處理消息隊列出隊的任務,而宏任務指的就是入隊到消息隊列中的任務,每一個宏任務都有一個微任務隊列,宏任務在執行過程當中,若是此時產生微任務,那麼會將產生的微任務入隊到當前的微任務隊列中,在當前宏任務的主要任務完成後,會依次出隊並執行微任務隊列中的任務,直到當前微任務隊列爲空纔會進行下一個宏任務。

微任務添加和執行流程示意圖

假設在執行解析HTML這個宏任務的過程當中,產生了Promise和MutationObserver這兩個微任務。

// parse HTML···
Promise.resolve();
removeChild();

微任務隊列會如何表現呢?

image

image

圖片引自:極客時間的《瀏覽器工做原理與實踐》

過程能夠拆爲如下幾步:

  1. 主線程執行JS Promise.resolve(); removeChild();
  2. parseHTML宏任務暫停
  3. Promise和MutationObserver微任務入隊到parseHTML宏任務的微任務隊列
  4. 微任務1 Promise.resolve()執行
  5. 微任務2 removeChild();執行
  6. 微任務隊列爲空,parseHTML宏任務繼續執行
  7. parseHTML宏任務完成,執行下一個宏任務

瀏覽器頁面循環系統原理圖

如下全部圖均來自極客時間《《瀏覽器工做原理與實踐》- 瀏覽器中的頁面循環系統》,能夠幫助理解消息隊列,事件循環,宏任務和微任務。

  • 消息隊列和事件循環
  • setTimeout
  • XMLHttpRequest
  • 宏任務

消息隊列和事件循環

線程的一次執行
image
在線程中引入事件循環
image
渲染進程線程之間發送任務
image

image
線程模型:隊列 + 循環
image
跨進程發送消息
image
單個任務執行時間太久
image

setTimeout

長任務致使定時器被延後執行
image
循環嵌套調用 setTimeout
image

XMLHttpRequest

消息循環系統調用棧記錄
image
XMLHttpRequest 工做流程圖
image
HTTPS 混合內容警告
image
使用 XMLHttpRequest 混合資源失效
image

宏任務

宏任務延時沒法保證
image

參考資料

若是文中有不對的地方,歡迎指正和交流~

期待和你們交流,共同進步,歡迎你們加入我建立的與前端開發密切相關的技術討論小組:

努力成爲優秀前端工程師!
相關文章
相關標籤/搜索