作者:趁你還年輕
轉(zhuǎn)發(fā)鏈接:https://segmentfault.com/a/1190000022950333
前言
看到這些詞仿佛比較讓人摸不著頭腦,其實(shí)在我們的日常開發(fā)中,早就和它們打過交道了。
我來舉幾個(gè)常見的例子:
- 我執(zhí)行了一段js,頁面就卡了挺久才有響應(yīng)
- 我觸發(fā)了一個(gè)按鈕的click事件,click事件處理器做出了響應(yīng)
- 我用setTimeout(callback, 1000)給代碼加了1s的延時(shí),1秒里發(fā)生了很多事情,然后功能正常了
- 我用setInterval(callback, 100)給代碼加了100ms的時(shí)間輪訓(xùn),直到期待的那個(gè)變量出現(xiàn)再執(zhí)行后續(xù)的代碼,并且結(jié)合setTimeout刪除這個(gè)定時(shí)器
- 我用Promise,async/await順序執(zhí)行了異步代碼
- 我用EventEmitter、new Vue()做事件廣播訂閱
- 我用MutationObserver監(jiān)聽了DOM更新
- 我手寫了一個(gè)Event類做事件的廣播訂閱
- 我用CustomEvent創(chuàng)建了自定義事件
- 我·······
其實(shí)上面舉的這些click, setTimeout, setInterval, Promise,async/await, EventEmitter, MutationObserver, Event類, CustomEvent與多進(jìn)程、單線程、事件循環(huán)、消息隊(duì)列、宏任務(wù)、微任務(wù)或多或少的都有所聯(lián)系。
而且也與瀏覽器的運(yùn)行原理有一些關(guān)系,作為每天在瀏覽器里辛勤耕耘的前端工程師們,瀏覽器的運(yùn)行原理(多進(jìn)程、單線程、事件循環(huán)、消息隊(duì)列、宏任務(wù)、微任務(wù))可以說是必須要掌握的內(nèi)容了,不僅對(duì)面試有用,對(duì)手上負(fù)責(zé)的開發(fā)工作也有很大的幫助。
- 淺談瀏覽器
- 架構(gòu)瀏覽器可以是哪種架構(gòu)?
- 如何理解Chrome的多進(jìn)程架構(gòu)?
- 前端最核心的渲染進(jìn)程包含哪些線程?
- 主線程(Main thread)(下載資源、執(zhí)行js、計(jì)算樣式、進(jìn)行布局、繪制合成)
- 光柵線程(Raster thread)
- 合成線程(Compositor thread)
- 工作線程(Worker thread)
- 淺談單線程jsjs引擎圖什么是單線程js?
- 單線程js屬于瀏覽器的哪個(gè)進(jìn)程?
- js為什么要設(shè)計(jì)成單線程的?
- 事件循環(huán)與消息隊(duì)列什么是事件循環(huán)?
- 什么是消息隊(duì)列?
- 如何實(shí)現(xiàn)一個(gè) EventEmitter(支持 on,once,off,emit)?
- 宏任務(wù)和微任務(wù)哪些屬于宏任務(wù)?
- 哪些屬于微任務(wù)?
- 事件循環(huán),消息隊(duì)列與宏任務(wù)、微任務(wù)之間的關(guān)系是什么?
- 為任務(wù)添加和執(zhí)行流程示意圖
- 瀏覽器頁面循環(huán)系統(tǒng)原理圖消息隊(duì)列和事件循環(huán)setTimeoutXMLHttpRequest宏任務(wù)
- 參考資料
淺談Chrome架構(gòu)
瀏覽器可以是哪種架構(gòu)?
瀏覽器本質(zhì)上也是一個(gè)軟件,它運(yùn)行于操作系統(tǒng)之上,一般來說會(huì)在特定的一個(gè)端口開啟一個(gè)進(jìn)程去運(yùn)行這個(gè)軟件,開啟進(jìn)程之后,計(jì)算機(jī)為這個(gè)進(jìn)程分配CPU資源、運(yùn)行時(shí)內(nèi)存,磁盤空間以及網(wǎng)絡(luò)資源等等,通常會(huì)為其指定一個(gè)PID來代表它。
先來看看我的機(jī)器上運(yùn)行的微信和Chrome的進(jìn)程詳情:

如果自己設(shè)計(jì)一個(gè)瀏覽器,瀏覽器可以是哪種架構(gòu)呢?
- 單進(jìn)程架構(gòu)(線程間通信)
- 多進(jìn)程架構(gòu)(進(jìn)程間IPC通信)
如果瀏覽器單進(jìn)程架構(gòu)的話,需要在一個(gè)進(jìn)程內(nèi)做到網(wǎng)絡(luò)、調(diào)度、UI、存儲(chǔ)、GPU、設(shè)備、渲染、插件等等任務(wù),通常來說可以為每個(gè)任務(wù)開啟一個(gè)線程,形成單進(jìn)程多線程的瀏覽器架構(gòu)。
但是由于這些功能的日益復(fù)雜,例如將網(wǎng)絡(luò),存儲(chǔ),UI放在一個(gè)線程中的話,執(zhí)行效率和性能越來越低下,不能再向下拆分出類似“線程”的子空間。
因此,為了逐漸強(qiáng)化瀏覽器的功能,于是產(chǎn)生了多進(jìn)程架構(gòu)的瀏覽器,可以將網(wǎng)絡(luò)、調(diào)度、UI、存儲(chǔ)、GPU、設(shè)備、渲染、插件等等任務(wù)分配給多個(gè)單獨(dú)的進(jìn)程,在每一個(gè)單獨(dú)的進(jìn)程內(nèi),又可以拆分出多個(gè)子線程,極大程度地強(qiáng)化了瀏覽器。
如何理解Chrome的多進(jìn)程架構(gòu)?
Chrome作為瀏覽器界里的一哥,它也是多進(jìn)程IPC架構(gòu)的。

Chrome多進(jìn)程架構(gòu)主要包括以下4個(gè)進(jìn)程:
- Browser進(jìn)程(負(fù)責(zé)地址欄、書簽欄、前進(jìn)后退、網(wǎng)絡(luò)請(qǐng)求、文件訪問等)
- Renderer進(jìn)程(負(fù)責(zé)一個(gè)Tab內(nèi)所有和網(wǎng)頁渲染有關(guān)的所有事情,是最核心的進(jìn)程)
- GPU進(jìn)程(負(fù)責(zé)GPU相關(guān)的任務(wù))
- Plugin進(jìn)程(負(fù)責(zé)Chrome插件相關(guān)的任務(wù))
Chrome 多進(jìn)程架構(gòu)的優(yōu)缺點(diǎn)優(yōu)點(diǎn)
- 每一個(gè)Tab就是要給單獨(dú)的進(jìn)程
- 由于每個(gè)Tab都有自己獨(dú)立的Renderer進(jìn)程,因此某一個(gè)Tab出問題不會(huì)影響其它Tab
缺點(diǎn)
- Tab間內(nèi)存不共享,不同進(jìn)程內(nèi)存包含相同內(nèi)容
Chrome多進(jìn)程架構(gòu)實(shí)錘圖

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

渲染進(jìn)程主要包括4個(gè)線程:
- 主線程(Main thread)(下載資源、執(zhí)行js、計(jì)算樣式、進(jìn)行布局、繪制合成)
- 光柵線程(Raster thread)
- 合成線程(Compositor thread)
- 工作線程(Worker thread)
渲染進(jìn)程的主線程知識(shí)點(diǎn):
- 下載資源:主線程可以通過Browser進(jìn)程的network線程下載圖片,css,js等渲染DOM需要的資源文件
- 執(zhí)行JS:主線程在遇到<script>標(biāo)簽時(shí),會(huì)下載并且執(zhí)行js,執(zhí)行js時(shí),為了避免改變DOM的結(jié)構(gòu),解析html停止,js執(zhí)行完成后繼續(xù)解析HTML。正是因?yàn)镴S執(zhí)行會(huì)阻塞UI渲染,而JS又是瀏覽器的一哥,因此瀏覽器常常被看做是單線程的。
- 計(jì)算樣式:主線程會(huì)基于CSS選擇器或者瀏覽器默認(rèn)樣式去進(jìn)行樣式計(jì)算,最終生成Computed Style
- 進(jìn)行布局:主線程計(jì)算好樣式以后,可以確定元素的位置信息以及盒模型信息,對(duì)元素進(jìn)行布局
- 進(jìn)行繪制:主線程根據(jù)先后順序以及層級(jí)關(guān)系對(duì)元素進(jìn)行渲染,通常會(huì)生成多個(gè)圖層
- 最終合成:主線程將渲染后的多個(gè)frame(幀)合成,類似flash的幀動(dòng)畫和PS的圖層
渲染進(jìn)程的主線程細(xì)節(jié)可以查閱Chrome官方的博客:Inside look at modern web browser (part 3)和Rendering Performance
渲染進(jìn)程的合成線程知識(shí)點(diǎn):
- 瀏覽器滾動(dòng)時(shí),合成線程會(huì)創(chuàng)建一個(gè)新的合成幀發(fā)送給GPU
- 合成線程工作與主線程無關(guān),不用等待樣式計(jì)算或者JS的執(zhí)行,因此合成線程相關(guān)的動(dòng)畫比涉及到主線程重新計(jì)算樣式和js的動(dòng)畫更加流暢
下面來看下主線程、合成線程和光柵線程一起作用的過程1.主線程主要遍歷布局樹生成層樹

2.柵格線程?hào)鸥窕刨N到GPU

3.合成線程將磁貼合成幀并通過IPC傳遞給Browser進(jìn)程,顯示在屏幕上

圖片引自Chrome官方博客:Inside look at modern web browser (part 3)
淺談單線程js
js引擎圖

什么是單線程js?
如果仔細(xì)閱讀過第一部分“談?wù)劄g覽器架構(gòu)”的話,這個(gè)答案其實(shí)已經(jīng)非常顯而易見了。在”前端最核心的渲染進(jìn)程包含哪些線程?“這里我們提到了主線程(Main thread)(下載資源、執(zhí)行js、計(jì)算樣式、進(jìn)行布局、繪制合成,注意其中的執(zhí)行js,這里其實(shí)已經(jīng)明確告訴了我們Chrome中JAVAScript運(yùn)行的位置。
那么Chrome中JavaScript運(yùn)行的位置在哪里呢?
渲染進(jìn)程(Renderer Process)中的主線程(Main Thread)
單線程js屬于瀏覽器的哪個(gè)進(jìn)程?
單線程的js -> 主線程(Main Thread)-> 渲染進(jìn)程(Renderer Process)
js為什么要設(shè)計(jì)成單線程的?
其實(shí)更為嚴(yán)謹(jǐn)?shù)谋硎鍪牵?ldquo;瀏覽器中的js執(zhí)行和UI渲染是在一個(gè)線程中順序發(fā)生的。”
這是因?yàn)樵阡秩具M(jìn)程的主線程在解析HTML生成DOM樹的過程中,如果此時(shí)執(zhí)行JS,主線程會(huì)主動(dòng)暫停解析HTML,先去執(zhí)行JS,等JS解析完成后,再繼續(xù)解析HTML。
那么為什么要“主線程會(huì)主動(dòng)暫停解析HTML,先去執(zhí)行JS,再繼續(xù)解析HTML呢”?
這是主線程在解析HTML生成DOM樹的過程中會(huì)執(zhí)行style,layout,render以及composite的操作,而JS可以操作DOM,CSSOM,會(huì)影響到主線程在解析HTML的最終渲染結(jié)果,最終頁面的渲染結(jié)果將變得不可預(yù)見。
如果主線程一邊解析HTML進(jìn)行渲染,JS同時(shí)在操作DOM或者CSSOM,結(jié)果會(huì)分為以下情況:
- 以主線程解析HTML的渲染結(jié)果為準(zhǔn)
- 以JS同時(shí)在操作DOM或者CSSOM的渲染結(jié)果為準(zhǔn)
考慮到最終頁面的渲染效果的一致性,所以js在瀏覽器中的實(shí)現(xiàn),被設(shè)計(jì)成為了JS執(zhí)行阻塞UI渲染型。
事件循環(huán)
什么是事件循環(huán)?
事件循環(huán)英文名叫做Event Loop,是一個(gè)在前端界老生常談的話題。我也簡(jiǎn)單說一下我對(duì)事件循環(huán)的認(rèn)識(shí):
事件循環(huán)可以拆為“事件”+“循環(huán)”。先來聊聊“事件”:
如果你有一定的前端開發(fā)經(jīng)驗(yàn),對(duì)于下面的“事件”一定不陌生:
- click、mouseover等等交互事件
- 事件冒泡、事件捕獲、事件委托等等
- addEventListener、removeEventListener()
- CustomEvent(自定義事件實(shí)現(xiàn)自定義交互)
- EventEmitter、EventBus(on,emit,once,off,這種東西經(jīng)常出面試題)
- 第三方庫(kù)的事件系統(tǒng)
有事件,就有事件處理器:在事件處理器中,我們會(huì)應(yīng)對(duì)這個(gè)事件做一些特殊操作。
那么瀏覽器怎么知道有事件發(fā)生了呢?怎么知道用戶對(duì)某個(gè)button做了一次click呢?
如果我們的主線程只是靜態(tài)的,沒有循環(huán)的話,可以用js偽代碼將其表述為:
function mainThread() {
console.log("Hello World!");
console.log("Hello JavaScript!");
}
mainThread();
執(zhí)行完一次mainThread()之后,這段代碼就無效了,mainThread并不是一種激活狀態(tài),對(duì)于I/O事件是沒有辦法捕獲到的。
因此對(duì)事件加入了“循環(huán)”,將渲染進(jìn)程的主線程變?yōu)榧せ顮顟B(tài),可以用js偽代碼表述如下:
// click event
function clickTrigger() {
return "我點(diǎn)擊按鈕了"
}
// 可以是while循環(huán)
function mainThread(){
while(true){
if(clickTrigger()) { console.log(“通知click事件監(jiān)聽器”) }
clickTrigger = null;
}
}
mainThread();
也可以是for循環(huán)
for(;;){
if(clickTrigger()) { console.log(“通知click事件監(jiān)聽器”) }
clickTrigger = null;
}
在事件監(jiān)聽器中做出響應(yīng):
button.addEventListener('click', ()=>{
console.log("多虧了事件循環(huán),我(瀏覽器)才能知道用戶做了什么操作");
})
什么是消息隊(duì)列?
消息隊(duì)列可以拆為“消息”+“隊(duì)列”。消息可以理解為用戶I/O;隊(duì)列就是先進(jìn)先出的數(shù)據(jù)結(jié)構(gòu)。而消息隊(duì)列,則是用于連接用戶I/O與事件循環(huán)的橋梁。
隊(duì)列數(shù)據(jù)結(jié)構(gòu)圖

入隊(duì)出隊(duì)圖

在js中,如何發(fā)現(xiàn)出隊(duì)列FIFO的特性?
下面這個(gè)結(jié)構(gòu)大家都熟悉,瞬間體現(xiàn)出隊(duì)列FIFO的特性。
// 定義一個(gè)隊(duì)列
let queue = [1,2,3];
// 入隊(duì)
queue.push(4); // queue[1,2,3,4]
// 出隊(duì)
queue.shift(); // 1 queue [2,3,4]
假設(shè)用戶做出了"click button1","click button3","click button 2"的操作。事件隊(duì)列定義為:
const taskQueue = ["click button1","click button3","click button 2"];
while(taskQueue.length>0){
taskQueue.shift(); // 任務(wù)依次出隊(duì)
}
任務(wù)依次出隊(duì):"click button1""click button3""click button 2"
此時(shí)由于mainThread有事件循環(huán),它會(huì)被瀏覽器渲染進(jìn)程的主線程事件循環(huán)系統(tǒng)捕獲,并在對(duì)應(yīng)的事件處理器做出響應(yīng)。
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"。
因此,可以將消息隊(duì)列理解為連接用戶I/O操作和瀏覽器事件循環(huán)系統(tǒng)的任務(wù)隊(duì)列。
如何實(shí)現(xiàn)一個(gè) EventEmitter(支持 on,once,off,emit)?
/**
* 說明:簡(jiǎn)單實(shí)現(xiàn)一個(gè)事件訂閱機(jī)制,具有監(jiān)聽on和觸發(fā)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為回調(diào)指針,不能是匿名函數(shù)
*/
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僅監(jiān)聽一次
// 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銷毀指定回調(diào)
// event.off("onEvent", onCallback);
// event.emit("onEvent", "abc", "123");
宏任務(wù)和微任務(wù)
- 哪些屬于宏任務(wù)?
- 哪些屬于微任務(wù)?
- 事件循環(huán),消息隊(duì)列與宏任務(wù)、微任務(wù)之間的關(guān)系是什么?
- 為任務(wù)添加和執(zhí)行流程示意圖
哪些屬于宏任務(wù)?
- setTimeout
- setInterval
- setImmediate
- requestAnimationFrame
- I/O
- UI渲染
哪些屬于微任務(wù)?
- Promise
- MutationObserver
- process.nextTick
- queueMicrotask
事件循環(huán),消息隊(duì)列與宏任務(wù)、微任務(wù)之間的關(guān)系是什么?
- 宏任務(wù)入隊(duì)消息隊(duì)列,可以將消息隊(duì)列理解為宏任務(wù)隊(duì)列
- 每個(gè)宏任務(wù)內(nèi)有一個(gè)微任務(wù)隊(duì)列,執(zhí)行過程中微任務(wù)入隊(duì)當(dāng)前宏任務(wù)的微任務(wù)隊(duì)列
- 宏任務(wù)微任務(wù)隊(duì)列為空時(shí)才會(huì)執(zhí)行下一個(gè)宏任務(wù)
- 事件循環(huán)捕獲隊(duì)列出隊(duì)的宏任務(wù)和微任務(wù)并執(zhí)行
事件循環(huán)會(huì)不斷地處理消息隊(duì)列出隊(duì)的任務(wù),而宏任務(wù)指的就是入隊(duì)到消息隊(duì)列中的任務(wù),每個(gè)宏任務(wù)都有一個(gè)微任務(wù)隊(duì)列,宏任務(wù)在執(zhí)行過程中,如果此時(shí)產(chǎn)生微任務(wù),那么會(huì)將產(chǎn)生的微任務(wù)入隊(duì)到當(dāng)前的微任務(wù)隊(duì)列中,在當(dāng)前宏任務(wù)的主要任務(wù)完成后,會(huì)依次出隊(duì)并執(zhí)行微任務(wù)隊(duì)列中的任務(wù),直到當(dāng)前微任務(wù)隊(duì)列為空才會(huì)進(jìn)行下一個(gè)宏任務(wù)。
為任務(wù)添加和執(zhí)行流程示意圖
假設(shè)在執(zhí)行解析HTML這個(gè)宏任務(wù)的過程中,產(chǎn)生了Promise和MutationObserver這兩個(gè)微任務(wù)。
// parse HTML···
Promise.resolve();
removeChild();
微任務(wù)隊(duì)列會(huì)如何表現(xiàn)呢?


圖片引自:極客時(shí)間的《瀏覽器工作原理與實(shí)踐》
過程可以拆為以下幾步:
- 主線程執(zhí)行JS Promise.resolve(); removeChild();
- parseHTML宏任務(wù)暫停
- Promise和MutationObserver微任務(wù)入隊(duì)到parseHTML宏任務(wù)的微任務(wù)隊(duì)列
- 微任務(wù)1 Promise.resolve()執(zhí)行
- 微任務(wù)2 removeChild();執(zhí)行
- 微任務(wù)隊(duì)列為空,parseHTML宏任務(wù)繼續(xù)執(zhí)行
- parseHTML宏任務(wù)完成,執(zhí)行下一個(gè)宏任務(wù)
瀏覽器頁面循環(huán)系統(tǒng)原理圖
以下所有圖均來自極客時(shí)間《《瀏覽器工作原理與實(shí)踐》- 瀏覽器中的頁面循環(huán)系統(tǒng)》,可以幫助理解消息隊(duì)列,事件循環(huán),宏任務(wù)和微任務(wù)。
- 消息隊(duì)列和事件循環(huán)
- setTimeout
- XMLHttpRequest
- 宏任務(wù)
消息隊(duì)列和事件循環(huán)
線程的一次執(zhí)行

在線程中引入事件循環(huán)

渲染進(jìn)程線程之間發(fā)送任務(wù)


線程模型:隊(duì)列 + 循環(huán)

跨進(jìn)程發(fā)送消息

單個(gè)任務(wù)執(zhí)行時(shí)間過久

setTimeout
長(zhǎng)任務(wù)導(dǎo)致定時(shí)器被延后執(zhí)行

循環(huán)嵌套調(diào)用 setTimeout

XMLHttpRequest
消息循環(huán)系統(tǒng)調(diào)用棧記錄

XMLHttpRequest 工作流程圖

HTTPS 混合內(nèi)容警告

使用 XMLHttpRequest 混合資源失效

宏任務(wù)
宏任務(wù)延時(shí)無法保證

如果文中有不對(duì)的地方,歡迎指正和交流~