瀏覽器進程、JS事件循環機制、宏任務和微任務

此文首發於 lijing0906.github.iojavascript

本想寫寫Promise的,可是查閱相關博客的時候發現瀏覽器進程、JS事件循環機制、宏任務和微任務須要提早學習一下,因而有了這篇博客。 參考連接css

區分進程和線程

用個形象的比喻:html

  • 進程是一個工廠,工廠有本身獨立的資源
  • 工廠之間相互獨立
  • 線程是工廠中的工人,多個工人協做完成任務
  • 工廠內有一個或多個工人
  • 工人之間共享空間

引伸爲計算機線程進程:前端

  • 進程是一個工廠,工廠有本身獨立的資源 -> 系統分配的內存(獨立的一塊內存)
  • 工廠之間相互獨立 -> 進程之間相互獨立
  • 線程是工廠中的工人,多個工人協做完成任務 -> 多個線程在進程中相互協做完成任務
  • 工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成
  • 工人之間共享空間 -> 同一進程下各個線程之間共享程序的內存空間(如代碼段、數據集、堆等)

瀏覽器是多進程的

  1. Browser進程,瀏覽器的主進程(負責協調,主控),只有一個,做用有:
    • 負責瀏覽器界面的顯示,與用戶交互。如前進、後退等
    • 負責各頁面的管理,建立和銷燬其餘進程
    • 將Renderer進程獲得的內存中的bitmap繪製到用戶界面上
    • 網絡資源的管理,下載等
  2. 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才建立
  3. GPU進程:最多一個,用於3D繪製等
  4. 瀏覽器渲染進程(瀏覽器內核)(Renderer進程,內部是多線程的):默認每一個Tab頁面一個進程,互不影響,主要用於頁面渲染,腳本執行,事件處理等 **強調:**瀏覽器中打開一個網頁至關於新起了一個進程(進程內有本身的多線程),也有可能多個合併成一個,經過Chrome的更多工具 -> 任務管理器能夠查看

瀏覽器多進程的優點

  • 避免單個page crash影響整個瀏覽器
  • 避免第三方插件crash影響整個瀏覽器
  • 多進程充分利用多核優點
  • 方便使用沙盒模型隔離插件等進程,提升瀏覽器穩定性 簡單理解:若是瀏覽器是單進程,那麼某個Tab頁或者某個插件崩潰了,就影響整個瀏覽器 固然,內存等資源消耗也會更大,有點空間換時間的意思。

重點來了,瀏覽器內核(渲染進程)

對於前端來講,頁面的渲染、JS的執行、事件的循環都在這個進程中進行。 瀏覽器的渲染進程是多線程的。 瀏覽器的渲染進程包括哪些線程:java

  1. GUI渲染進程
    • 負責渲染瀏覽器界面,解析HTML、CSS,構建DOM樹和RenderObject樹,佈局和繪製
    • 當界面須要重繪(Repaint)或因爲某種操做引起迴流(reflow)時,該線程就會執行
    • GUI渲染進程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(至關於被凍結了),GUI更新會被保存在一個隊列中,等到JS引擎空閒時當即被執行。
  2. JS引擎線程
    • 也稱JS內核,負責處理JS腳本程序。例如V8引擎
    • JS引擎一直等待着任務隊列中任務的到來,而後加以處理,一個Tab頁(Renderer進程)中不管何時都只有一個JS引擎線程在運行JS程序
    • GUI渲染進程與JS引擎線程是互斥的,因此若是JS執行的時間過長,頁面渲染就不連貫。
  3. 事件觸發線程
    • 歸屬於瀏覽器而不是JS引擎,用來控制事件循環(能夠理解爲:JS引擎本身都忙不過來,須要瀏覽器另開線程協助)
    • 當JS引擎執行代碼塊和setTimeout時(也可來自瀏覽器內核的其餘線程,如鼠標點擊、ajax異步請求等),會將對應事件任務添加到事件線程中
    • 當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的尾部,等待JS引擎的處理
    • 因爲JS是單線程關係,因此這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時纔會去執行)
  4. 定時觸發器線程
    • 傳說中的setIntervalsetTimeout所在的線程
    • 瀏覽器定時計數器並非由JS引擎計數的,由於JS引擎是單線程的,若是處於阻塞線程狀態就會影響計時的準確性
    • 所以經過定時觸發器線程來計時並觸發定時,計時完畢後,添加到事件隊列中,等待JS引擎空閒後執行
    • W3C在HTML標準中規定,要求setTimeout中低於4ms的時間間隔算4ms
  5. 異步http請求線程
    • XMLHttpRequest在鏈接後是經過瀏覽器新開一個線程請求
    • 在檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入事件隊列中,再由JS引擎執行

Browser進程和瀏覽器內核(Renderer進程)如何通訊

  • Browser進程收到用戶請求,首先須要獲取頁面內容(好比經過網絡下載資源),隨後將該任務經過RendererHost接口傳遞給Renderer進程
  • Renderer進程的RendererHost接口收到消息,簡單解釋後,交給渲染線程,而後開始渲染
    • 渲染線程接收到請求,加載網頁並渲染網頁,這其中可能須要Browser進程獲取資源和須要GPU進程來幫助渲染
    • 固然可能會有JS引擎線程操做DOM(這樣可能會形成迴流並重繪)
    • 最後Renderer進程將結果傳遞給Browser進程
  • Browser進程接收到結果並將結果繪製出來
    browserandrenderer

梳理瀏覽器渲染流程

簡化前期工做:node

瀏覽器輸入url,瀏覽器Browser主進程接管,開一個下載線程 而後進行http請求(略去DNS查詢,IP尋址等等操做),而後等待響應,獲取內容 獲得內容就將內容經過RendererHost接口轉交給Renderer進程 瀏覽器渲染流程開始git

瀏覽器內核拿到內容後,渲染大概能夠劃分紅如下幾個步驟:github

  1. 解析html建立dom樹
  2. 解析css構建render樹(將css解析成樹形結構,而後結合DOM合併成render樹)
  3. 佈局render樹(layout/reflow),負責各元素尺寸、位置的計算
  4. 繪製render樹(paint),繪製頁面像素信息
  5. 瀏覽器將各層的信息發送給GPU,GPU會將各層合成(composite)顯示在頁面上 全部詳細步驟已略去,渲染完畢後就是load事件了,以後就是本身的JS邏輯處理了
    renderflow

從Event Loop談JS的運行機制

理解一個概念:面試

  • JS分爲同步任務和異步任務
  • 同步任務都在主線程上執行,造成一個執行棧
  • 主線程以外,事件觸發線程管理着一個任務隊列,只要異步任務有了運行結果,就在任務隊列之中放置一個事件。
  • 一旦執行棧中的全部同步任務執行完畢(此時JS引擎空閒),系統就會讀取任務隊列,將可運行的異步任務添加到可執行棧中,開始執行。
    evenloop
    看到這裏,應該就能夠理解了:爲何有時候setTimeout推入的事件不能準時執行?由於可能在它推入到事件列表時,主線程還不空閒,正在執行其它代碼,因此天然有偏差。

事件循環機制進一步補充

evenloop2
上圖大體描述就是:

  • 主線程運行時會產生執行棧,
  • 棧中的代碼調用某些api時,它們會在事件隊列中添加各類事件(當知足觸發條件後,如ajax請求完畢)
  • 而棧中的代碼執行完畢,就會讀取事件隊列中的事件,去執行那些回調
  • 如此循環
  • 注意,老是要等待棧中的代碼執行完畢後纔會去讀取事件隊列中的事件

事件循環進階:macrotask與microtask

先看一道面試題:ajax

console.log('script start');
setTimeout(function() {
    console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
    console.log('promise1');
}).then(function() {
    console.log('promise2');
});
console.log('script end');
複製代碼

打印順序:

'script start'
    'script end'
    'promise1'
    'promise2'
    'setTimeout'
複製代碼

爲何呢?由於Promise裏有了一個一個新的概念:microtask。 或者,進一步,JS中分爲兩種任務類型:macrotaskmicrotask,在ECMAScript中,microtask稱爲jobsmacrotask可稱爲task 它們的定義?區別?簡單點能夠按以下理解:

  • macrotask(又稱之爲宏任務),能夠理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)
    • 每個task會從頭至尾將這個任務執行完畢,不會執行其它
    • 瀏覽器爲了可以使得JS內部task與DOM任務可以有序的執行,會在一個task執行結束後,在下一個 task 執行開始前,對頁面進行從新渲染 (task->渲染->task->...
  • microtask(又稱爲微任務),能夠理解是在當前task執行結束後當即執行的任務
    • 也就是說,在當前task任務後,下一個task以前,在渲染以前
    • 因此它的響應速度相比setTimeout(setTimeout是task)會更快,由於無需等渲染
    • 也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的全部microtask都執行完畢(在渲染前) 分別是怎樣的場景會造成macrotask和microtask呢?
  • macrotask:主代碼塊,setTimeout,setInterval等(能夠看到,事件隊列中的每個事件都是一個macrotask)
  • microtask:Promise,process.nextTick等 補充:在node環境下,process.nextTick的優先級高於Promise,也就是能夠簡單理解爲:在宏任務結束後會先執行微任務隊列中的nextTickQueue部分,而後纔會執行微任務中的Promise部分。 再根據線程來理解下:
  • macrotask中的事件都是放在一個事件隊列中的,而這個隊列由事件觸發線程維護
  • microtask中的全部微任務都是添加到微任務隊列(Job Queues)中,等待當前macrotask執行完畢後執行,而這個隊列由JS引擎線程維護 (這點由本身理解+推測得出,由於它是在主線程下無縫執行的) 因此,總結下運行機制
  • 執行一個宏任務(棧中沒有就從事件隊列中獲取)
  • 執行過程當中若是遇到微任務,就將它添加到微任務的任務隊列中
  • 宏任務執行完畢後,當即執行當前微任務隊列中的全部微任務(依次執行)
  • 當前宏任務執行完畢,開始檢查渲染,而後GUI線程接管渲染
  • 渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)
    macmic
相關文章
相關標籤/搜索