關鍵詞:多進程、單線程、事件循環、消息隊列、宏任務、微任務
css
看到這些詞彷彿比較讓人摸不着頭腦,其實在咱們的平常開發中,早就和他們打過交道了。前端
我來舉幾個常見的例子:git
其實上面舉的這些click, setTimeout, setInterval, Promise,async/await, EventEmitter, MutationObserver, Event類, CustomEvent
與多進程、單線程、事件循環、消息隊列、宏任務、微任務
或多或少的都有所聯繫。github
並且也與瀏覽器的運行原理有一些關係,做爲天天在瀏覽器裏辛勤耕耘的前端工程師們,瀏覽器的運行原理(多進程、單線程、事件循環、消息隊列、宏任務、微任務)能夠說是必需要掌握的內容了,不只對面試有用,對手上負責的開發工做也有很大的幫助。web
淺談瀏覽器架構面試
前端最核心的渲染進程包含哪些線程?segmentfault
淺談單線程js瀏覽器
事件循環與消息隊列微信
宏任務和微任務網絡
瀏覽器頁面循環系統原理圖
瀏覽器本質上也是一個軟件,它運行於操做系統之上,通常來講會在特定的一個端口開啓一個進程去運行這個軟件,開啓進程以後,計算機爲這個進程分配CPU資源、運行時內存,磁盤空間以及網絡資源等等,一般會爲其指定一個PID來表明它。
先來看看個人機器上運行的微信和Chrome的進程詳情:
軟件 | CPU(%) | 線程 | PID | 內存 | 端口 |
---|---|---|---|---|---|
微信 | 0.1 | 46 | 587 | 555MB | 124301 |
Chrome | 7.9 | 48 | 481 | 603MB | 1487 |
若是本身設計一個瀏覽器,瀏覽器能夠是那種架構呢?
若是瀏覽器單進程架構的話,須要在一個進程內作到網絡、調度、UI、存儲、GPU、設備、渲染、插件等等任務,一般來講能夠爲每一個任務開啓一個線程,造成單進程多線程的瀏覽器架構。
可是因爲這些功能的日益複雜,例如將網絡,存儲,UI放在一個線程中的話,執行效率和性能愈來愈地下,不能再向下拆分出相似「線程」的子空間。
所以,爲了逐漸強化瀏覽器的功能,因而產生了多進程架構的瀏覽器,能夠將網絡、調度、UI、存儲、GPU、設備、渲染、插件等等任務分配給多個單獨的進程,在每個單獨的進程內,又能夠拆分出多個子線程,極大程度地強化了瀏覽器。
Chrome做爲瀏覽器屆裏的一哥,他也是多進程IPC架構的。
Chrome多進程架構主要包括如下4個進程:
Chrome 多進程架構的優缺點
優勢
缺點
Chrome多進程架構實錘圖
渲染進程主要包括4個線程:
渲染進程的主線程知識點:
<script>
標籤時,會下載而且執行js,執行js時,爲了不改變DOM的結構,解析HTML停滯,js執行完成後繼續解析HTML。正是由於JS執行會阻塞UI渲染,而JS又是瀏覽器的一哥,所以瀏覽器經常被看作是單線程的。 渲染進程的主線程細節能夠查閱Chrome官方的博客:Inside look at modern web browser (part 3)和Rendering Performance
渲染進程的合成線程知識點:
下面來看下主線程、合成線程和光柵線程一塊兒做用的過程
1.主線程主要遍歷佈局樹生成層樹
2.柵格線程柵格化磁貼到GPU
3.合成線程將磁貼合成幀並經過IPC傳遞給Browser進程,顯示在屏幕上
圖片引自Chrome官方博客:Inside look at modern web browser (part 3)
應用程序(實現) | 方言和最後版本 | 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 |
若是仔細閱讀過第一部分「談談瀏覽器架構」的話,這個答案其實已經很是顯而易見了。
在」前端最核心的渲染進程包含哪些線程?「這裏咱們提到了主線程(Main thread)(下載資源、執行js、計算樣式、進行佈局、繪製合成,注意其中的執行js,這裏其實已經明確告訴了咱們Chrome中JavaScript運行的位置。
那麼Chrome中JavaScript運行的位置在哪裏呢?
渲染進程(Renderer Process)中的主線程(Main Thread)
單線程的js -> 主線程(Main Thread)-> 渲染進程(Renderer Process)
其實更爲嚴謹的表述是:「瀏覽器中的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,結果會分爲如下狀況:
考慮到最終頁面的渲染效果的一致性,因此js在瀏覽器中的實現,被設計成爲了JS執行阻塞UI渲染型。
事件循環英文名叫作Event Loop,是一個在前端屆老生常談的話題。
我也簡單說一下我對事件循環的認識:
事件循環能夠拆爲「事件」+「循環」。
先來聊聊「事件」:
若是你有必定的前端開發經驗,對於下面的「事件」必定不陌生:
有事件,就有事件處理器:在事件處理器中,咱們會應對這個事件作一些特殊操做。
那麼瀏覽器怎麼知道有事件發生了呢?怎麼知道用戶對某個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與事件循環的橋樑。
下面這個結構你們都熟悉,瞬間體現出隊列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操做和瀏覽器事件循環系統的任務隊列。
/** * 說明:簡單實現一個事件訂閱機制,具備監聽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");
事件循環會不斷地處理消息隊列出隊的任務,而宏任務指的就是入隊到消息隊列中的任務,每一個宏任務都有一個微任務隊列,宏任務在執行過程當中,若是此時產生微任務,那麼會將產生的微任務入隊到當前的微任務隊列中,在當前宏任務的主要任務完成後,會依次出隊並執行微任務隊列中的任務,直到當前微任務隊列爲空纔會進行下一個宏任務。
假設在執行解析HTML這個宏任務的過程當中,產生了Promise和MutationObserver這兩個微任務。
// parse HTML··· Promise.resolve(); removeChild();
微任務隊列會如何表現呢?
圖片引自:極客時間的《瀏覽器工做原理與實踐》
過程能夠拆爲如下幾步:
Promise.resolve(); removeChild();
如下全部圖均來自極客時間《《瀏覽器工做原理與實踐》- 瀏覽器中的頁面循環系統》,能夠幫助理解消息隊列,事件循環,宏任務和微任務。
線程的一次執行
在線程中引入事件循環
渲染進程線程之間發送任務
線程模型:隊列 + 循環
跨進程發送消息
單個任務執行時間太久
長任務致使定時器被延後執行
循環嵌套調用 setTimeout
消息循環系統調用棧記錄
XMLHttpRequest 工做流程圖
HTTPS 混合內容警告
使用 XMLHttpRequest 混合資源失效
宏任務延時沒法保證
若是文中有不對的地方,歡迎指正和交流~
期待和你們交流,共同進步,歡迎你們加入我建立的與前端開發密切相關的技術討論小組:
- 微信公衆號: 生活在瀏覽器裏的咱們 / excellent_developers
- Github博客: 趁你還年輕233的我的博客
- SegmentFault專欄:趁你還年輕,作個優秀的前端工程師
- Leetcode討論微信羣:Z2Fva2FpMjAxMDA4MDE=(加我微信拉你進羣)
努力成爲優秀前端工程師!