作者:趁你還年輕
轉發鏈接:https://segmentfault.com/a/1190000022950333
前言
看到這些詞仿佛比較讓人摸不著頭腦,其實在我們的日常開發中,早就和它們打過交道了。
我來舉幾個常見的例子:
- 我執行了一段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與多進程、單線程、事件循環、消息隊列、宏任務、微任務或多或少的都有所聯系。
而且也與瀏覽器的運行原理有一些關系,作為每天在瀏覽器里辛勤耕耘的前端工程師們,瀏覽器的運行原理(多進程、單線程、事件循環、消息隊列、宏任務、微任務)可以說是必須要掌握的內容了,不僅對面試有用,對手上負責的開發工作也有很大的幫助。
- 淺談瀏覽器
- 架構瀏覽器可以是哪種架構?
- 如何理解Chrome的多進程架構?
- 前端最核心的渲染進程包含哪些線程?
- 主線程(Main thread)(下載資源、執行js、計算樣式、進行布局、繪制合成)
- 光柵線程(Raster thread)
- 合成線程(Compositor thread)
- 工作線程(Worker thread)
- 淺談單線程jsjs引擎圖什么是單線程js?
- 單線程js屬于瀏覽器的哪個進程?
- js為什么要設計成單線程的?
- 事件循環與消息隊列什么是事件循環?
- 什么是消息隊列?
- 如何實現一個 EventEmitter(支持 on,once,off,emit)?
- 宏任務和微任務哪些屬于宏任務?
- 哪些屬于微任務?
- 事件循環,消息隊列與宏任務、微任務之間的關系是什么?
- 為任務添加和執行流程示意圖
- 瀏覽器頁面循環系統原理圖消息隊列和事件循環setTimeoutXMLHttpRequest宏任務
- 參考資料
淺談Chrome架構
瀏覽器可以是哪種架構?
瀏覽器本質上也是一個軟件,它運行于操作系統之上,一般來說會在特定的一個端口開啟一個進程去運行這個軟件,開啟進程之后,計算機為這個進程分配CPU資源、運行時內存,磁盤空間以及網絡資源等等,通常會為其指定一個PID來代表它。
先來看看我的機器上運行的微信和Chrome的進程詳情:
如果自己設計一個瀏覽器,瀏覽器可以是哪種架構呢?
- 單進程架構(線程間通信)
- 多進程架構(進程間IPC通信)
如果瀏覽器單進程架構的話,需要在一個進程內做到網絡、調度、UI、存儲、GPU、設備、渲染、插件等等任務,通常來說可以為每個任務開啟一個線程,形成單進程多線程的瀏覽器架構。
但是由于這些功能的日益復雜,例如將網絡,存儲,UI放在一個線程中的話,執行效率和性能越來越低下,不能再向下拆分出類似“線程”的子空間。
因此,為了逐漸強化瀏覽器的功能,于是產生了多進程架構的瀏覽器,可以將網絡、調度、UI、存儲、GPU、設備、渲染、插件等等任務分配給多個單獨的進程,在每一個單獨的進程內,又可以拆分出多個子線程,極大程度地強化了瀏覽器。
如何理解Chrome的多進程架構?
Chrome作為瀏覽器界里的一哥,它也是多進程IPC架構的。
Chrome多進程架構主要包括以下4個進程:
- Browser進程(負責地址欄、書簽欄、前進后退、網絡請求、文件訪問等)
- Renderer進程(負責一個Tab內所有和網頁渲染有關的所有事情,是最核心的進程)
- GPU進程(負責GPU相關的任務)
- Plugin進程(負責Chrome插件相關的任務)
Chrome 多進程架構的優缺點優點
- 每一個Tab就是要給單獨的進程
- 由于每個Tab都有自己獨立的Renderer進程,因此某一個Tab出問題不會影響其它Tab
缺點
- Tab間內存不共享,不同進程內存包含相同內容
Chrome多進程架構實錘圖
前端最核心的渲染(Renderer)進程包含哪些線程?
渲染進程主要包括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.主線程主要遍歷布局樹生成層樹
2.柵格線程柵格化磁貼到GPU
3.合成線程將磁貼合成幀并通過IPC傳遞給Browser進程,顯示在屏幕上
圖片引自Chrome官方博客:Inside look at modern web browser (part 3)
淺談單線程js
js引擎圖
什么是單線程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與事件循環的橋梁。
隊列數據結構圖
入隊出隊圖
在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);
if (idx === -1) return;
const listener = this.listeners[idx];
if (listener.isOnce) {
listener.callbacks[0](...args);
this.listeners.splice(idx, 1);
} else {
listener.callbacks.forEach((cb) => {
cb(...args);
});
}
}
once(event, func) {
const callback = (listener) => listener.name === event;
let idx = this.listeners.findIndex(callback);
if (idx !== -1) return;
this.listeners.push({
name: event,
callbacks: [func],
isOnce: true,
});
}
off(event, func) {
if (this.listeners.length === 0) return;
const callback = (listener) => listener.name === event;
let idx = this.listeners.findIndex(callback);
if (idx === -1) return;
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");
// event.emit("onceEvent", "abc", "456");
// 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");
// // off銷毀指定回調
// 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();
微任務隊列會如何表現呢?
圖片引自:極客時間的《瀏覽器工作原理與實踐》
過程可以拆為以下幾步:
- 主線程執行JS Promise.resolve(); removeChild();
- parseHTML宏任務暫停
- Promise和MutationObserver微任務入隊到parseHTML宏任務的微任務隊列
- 微任務1 Promise.resolve()執行
- 微任務2 removeChild();執行
- 微任務隊列為空,parseHTML宏任務繼續執行
- parseHTML宏任務完成,執行下一個宏任務
瀏覽器頁面循環系統原理圖
以下所有圖均來自極客時間《《瀏覽器工作原理與實踐》- 瀏覽器中的頁面循環系統》,可以幫助理解消息隊列,事件循環,宏任務和微任務。
- 消息隊列和事件循環
- setTimeout
- XMLHttpRequest
- 宏任務
消息隊列和事件循環
線程的一次執行
在線程中引入事件循環
渲染進程線程之間發送任務
線程模型:隊列 + 循環
跨進程發送消息
單個任務執行時間過久
setTimeout
長任務導致定時器被延后執行
循環嵌套調用 setTimeout
XMLHttpRequest
消息循環系統調用棧記錄
XMLHttpRequest 工作流程圖
HTTPS 混合內容警告
使用 XMLHttpRequest 混合資源失效
宏任務
宏任務延時無法保證
如果文中有不對的地方,歡迎指正和交流~