此文首發於 lijing0906.github.iojavascript
本想寫寫Promise
的,可是查閱相關博客的時候發現瀏覽器進程、JS事件循環機制、宏任務和微任務須要提早學習一下,因而有了這篇博客。 參考連接css
區分進程和線程
用個形象的比喻:html
- 工廠之間相互獨立
- 線程是工廠中的工人,多個工人協做完成任務
- 工廠內有一個或多個工人
- 工人之間共享空間
引伸爲計算機線程進程:前端
- 進程是一個工廠,工廠有本身獨立的資源 -> 系統分配的內存(獨立的一塊內存)
- 工廠之間相互獨立 -> 進程之間相互獨立
- 線程是工廠中的工人,多個工人協做完成任務 -> 多個線程在進程中相互協做完成任務
- 工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成
- 工人之間共享空間 -> 同一進程下各個線程之間共享程序的內存空間(如代碼段、數據集、堆等)
瀏覽器是多進程的
- Browser進程,瀏覽器的主進程(負責協調,主控),只有一個,做用有:
- 負責瀏覽器界面的顯示,與用戶交互。如前進、後退等
- 負責各頁面的管理,建立和銷燬其餘進程
- 將Renderer進程獲得的內存中的bitmap繪製到用戶界面上
- 網絡資源的管理,下載等
- 第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才建立
- GPU進程:最多一個,用於3D繪製等
- 瀏覽器渲染進程(瀏覽器內核)(Renderer進程,內部是多線程的):默認每一個Tab頁面一個進程,互不影響,主要用於頁面渲染,腳本執行,事件處理等 **強調:**瀏覽器中打開一個網頁至關於新起了一個進程(進程內有本身的多線程),也有可能多個合併成一個,經過Chrome的
更多工具 -> 任務管理器
能夠查看
瀏覽器多進程的優點
- 避免單個page crash影響整個瀏覽器
- 避免第三方插件crash影響整個瀏覽器
- 多進程充分利用多核優點
- 方便使用沙盒模型隔離插件等進程,提升瀏覽器穩定性 簡單理解:若是瀏覽器是單進程,那麼某個Tab頁或者某個插件崩潰了,就影響整個瀏覽器 固然,內存等資源消耗也會更大,有點空間換時間的意思。
重點來了,瀏覽器內核(渲染進程)
對於前端來講,頁面的渲染、JS的執行、事件的循環都在這個進程中進行。 瀏覽器的渲染進程是多線程的。 瀏覽器的渲染進程包括哪些線程:java
- GUI渲染進程
- 負責渲染瀏覽器界面,解析HTML、CSS,構建DOM樹和RenderObject樹,佈局和繪製
- 當界面須要重繪(Repaint)或因爲某種操做引起迴流(reflow)時,該線程就會執行
- GUI渲染進程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(至關於被凍結了),GUI更新會被保存在一個隊列中,等到JS引擎空閒時當即被執行。
- JS引擎線程
- 也稱JS內核,負責處理JS腳本程序。例如V8引擎
- JS引擎一直等待着任務隊列中任務的到來,而後加以處理,一個Tab頁(Renderer進程)中不管何時都只有一個JS引擎線程在運行JS程序
- GUI渲染進程與JS引擎線程是互斥的,因此若是JS執行的時間過長,頁面渲染就不連貫。
- 事件觸發線程
- 歸屬於瀏覽器而不是JS引擎,用來控制事件循環(能夠理解爲:JS引擎本身都忙不過來,須要瀏覽器另開線程協助)
- 當JS引擎執行代碼塊和setTimeout時(也可來自瀏覽器內核的其餘線程,如鼠標點擊、ajax異步請求等),會將對應事件任務添加到事件線程中
- 當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的尾部,等待JS引擎的處理
- 因爲JS是單線程關係,因此這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時纔會去執行)
- 定時觸發器線程
- 傳說中的
setInterval
和setTimeout
所在的線程
- 瀏覽器定時計數器並非由JS引擎計數的,由於JS引擎是單線程的,若是處於阻塞線程狀態就會影響計時的準確性
- 所以經過定時觸發器線程來計時並觸發定時,計時完畢後,添加到事件隊列中,等待JS引擎空閒後執行
- W3C在HTML標準中規定,要求setTimeout中低於4ms的時間間隔算4ms
- 異步http請求線程
- XMLHttpRequest在鏈接後是經過瀏覽器新開一個線程請求
- 在檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入事件隊列中,再由JS引擎執行
Browser進程和瀏覽器內核(Renderer進程)如何通訊
- Browser進程收到用戶請求,首先須要獲取頁面內容(好比經過網絡下載資源),隨後將該任務經過RendererHost接口傳遞給Renderer進程
- Renderer進程的RendererHost接口收到消息,簡單解釋後,交給渲染線程,而後開始渲染
- 渲染線程接收到請求,加載網頁並渲染網頁,這其中可能須要Browser進程獲取資源和須要GPU進程來幫助渲染
- 固然可能會有JS引擎線程操做DOM(這樣可能會形成迴流並重繪)
- 最後Renderer進程將結果傳遞給Browser進程
- Browser進程接收到結果並將結果繪製出來
梳理瀏覽器渲染流程
簡化前期工做:node
瀏覽器輸入url,瀏覽器Browser主進程接管,開一個下載線程 而後進行http請求(略去DNS查詢,IP尋址等等操做),而後等待響應,獲取內容 獲得內容就將內容經過RendererHost接口轉交給Renderer進程 瀏覽器渲染流程開始git
瀏覽器內核拿到內容後,渲染大概能夠劃分紅如下幾個步驟:github
- 解析html建立dom樹
- 解析css構建render樹(將css解析成樹形結構,而後結合DOM合併成render樹)
- 佈局render樹(layout/reflow),負責各元素尺寸、位置的計算
- 繪製render樹(paint),繪製頁面像素信息
- 瀏覽器將各層的信息發送給GPU,GPU會將各層合成(composite)顯示在頁面上 全部詳細步驟已略去,渲染完畢後就是load事件了,以後就是本身的JS邏輯處理了
從Event Loop談JS的運行機制
理解一個概念:面試
- JS分爲同步任務和異步任務
- 同步任務都在主線程上執行,造成一個執行棧
- 主線程以外,事件觸發線程管理着一個
任務隊列
,只要異步任務有了運行結果,就在任務隊列
之中放置一個事件。
- 一旦
執行棧
中的全部同步任務執行完畢(此時JS引擎空閒),系統就會讀取任務隊列
,將可運行的異步任務添加到可執行棧中,開始執行。
看到這裏,應該就能夠理解了:爲何有時候setTimeout推入的事件不能準時執行?由於可能在它推入到事件列表時,主線程還不空閒,正在執行其它代碼,因此天然有偏差。
事件循環機制進一步補充
上圖大體描述就是:
- 主線程運行時會產生執行棧,
- 棧中的代碼調用某些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中分爲兩種任務類型:macrotask
和microtask
,在ECMAScript中,microtask
稱爲jobs
,macrotask
可稱爲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線程繼續接管,開始下一個宏任務(從事件隊列中獲取)