(好文)一文講解瀏覽器運(yùn)行渲染機(jī)制、JS 任務(wù)隊(duì)列及事件循環(huán)
你是不是有過以下困難:
?多個(gè)方法互相嵌套,但是最終還是蒙對了?不是很明白為什么瀏覽器有時(shí)候會卡死?事件循環(huán)好像知道那么點(diǎn),但是就是講不出來為啥?……
本篇文章就把你的問題給一一解答,當(dāng)然這些東西想完弄清楚,肯定離不開進(jìn)程,線程,瀏覽器內(nèi)核,渲染,事件循環(huán),任務(wù)隊(duì)列等,我們就一個(gè)一個(gè)的來看,它們到底是怎么工作的。
進(jìn)程和線程
舉個(gè)例子,一個(gè)工廠,它有自己獨(dú)立的資源,工廠和工廠之間相互獨(dú)立,各自做各自的事情。一個(gè)場子可以有很多工人,工人可以 單個(gè)作業(yè) 也可以 協(xié)同作業(yè),工人做的事情,都只會在自己的工廠內(nèi),并且共享這個(gè)工廠的空間。
我們現(xiàn)在把概念放到進(jìn)程上,一個(gè)進(jìn)程就相當(dāng)于一個(gè)工廠,工廠里的資源就相當(dāng)于系統(tǒng)分配的獨(dú)立內(nèi)存,多個(gè)工廠各自做各自的事情就相當(dāng)于進(jìn)程之間相互獨(dú)立,一個(gè)工廠有很多工人就相當(dāng)于一個(gè)進(jìn)程可以有很多線程,工人的作業(yè)就相當(dāng)于線程完成任務(wù),工人共享這個(gè)工廠的空間,就相當(dāng)于一個(gè)進(jìn)程下面的線程之間可以共享程序的內(nèi)存。

在 windows 的任務(wù)管理器中 CPU 和 內(nèi)存 可以把每個(gè)進(jìn)程的占用看的很清楚,當(dāng)然 Mac OS 從活動監(jiān)視器中也可以看到。所以:進(jìn)程是 cpu 資源分配的最小單位,線程是cpu調(diào)度的最小單位。


大家所說的多線程和單線程,都是只在一個(gè)進(jìn)程內(nèi)的多和單!
瀏覽器
構(gòu)成
前提:頁面是跑在瀏覽器上的,也就是說瀏覽器是頁面的載體,瀏覽器會制定一套規(guī)則,頁面滿足了這個(gè)規(guī)則然后才可以在到瀏覽器上正常運(yùn)行。
瀏覽器本質(zhì)上其實(shí)是一個(gè)軟件,它運(yùn)行在一個(gè)操作系統(tǒng)上(windows 或 MacOS 或 其他),一般來說操作系統(tǒng)會開一個(gè)端口去運(yùn)行這個(gè)軟件,也就是為這個(gè)進(jìn)程分配了CPU,內(nèi)存 和 磁盤空間等。
那瀏覽器是單進(jìn)程還是多進(jìn)程呢?我們看一下:

可見它是個(gè)多個(gè)進(jìn)程的瀏覽器!
在 Chrome 多進(jìn)程架構(gòu)里,它包括了四個(gè)進(jìn)程:
?Browser進(jìn)程(負(fù)責(zé)地址欄、書簽欄、前進(jìn)后退、網(wǎng)絡(luò)請求、文件訪問等)?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ù))
如果你打開它的任務(wù)管理器,你會發(fā)現(xiàn):

上圖我們可以看出:一個(gè)標(biāo)簽頁就是一個(gè)進(jìn)程,甚至一個(gè)擴(kuò)展程序就是一個(gè)進(jìn)程!在瀏覽器中打開一個(gè)網(wǎng)頁就相當(dāng)于新開了一個(gè)進(jìn)程。
但是:在這里瀏覽器有自己的優(yōu)化機(jī)制,有時(shí)候打開多個(gè)標(biāo)簽頁,進(jìn)程會合并,所以每一個(gè)標(biāo)簽頁對應(yīng)一個(gè)進(jìn)程不是絕對的。
這樣的多進(jìn)程分配的好處是:
?如果一個(gè)頁面掛了,不會影響其他頁面,甚至影響到整個(gè)瀏覽器?避免安裝的三方插件等影響了瀏覽器全局?多進(jìn)程充分利用了多核的優(yōu)勢?把插件,擴(kuò)展程序等全部隔離,提高穩(wěn)定性
當(dāng)然,缺點(diǎn)很就明顯了,內(nèi)存消耗大,確實(shí)有點(diǎn)像空間換時(shí)間的意思。
請求,響應(yīng)
接下來我們看下瀏覽器是如何通過輸入內(nèi)容來請求成功的。
1.
當(dāng)用戶在地址欄輸入內(nèi)容時(shí),UI線程首先問的是“這是搜索查詢還是URL?”。在Chrome瀏覽器中,地址欄也是搜索輸入字段,因此UI線程需要解析并決定是將您發(fā)送到搜索引擎還是請求的網(wǎng)站。

2.
當(dāng)用戶按下Enter鍵時(shí),UI線程會發(fā)起網(wǎng)絡(luò)調(diào)用以獲取網(wǎng)站內(nèi)容。加載微調(diào)框顯示在選項(xiàng)卡的角上,并且網(wǎng)絡(luò)線程通過相應(yīng)的協(xié)議(例如DNS查找和為請求建立TLS連接)。
此時(shí),網(wǎng)絡(luò)線程可能會收到服務(wù)器重定向標(biāo)頭,例如HTTP301。在這種情況下,網(wǎng)絡(luò)線程與服務(wù)器正在請求重定向的UI線程進(jìn)行通信。然后,將啟動另一個(gè)URL請求。

3.
一旦有響應(yīng)了,網(wǎng)絡(luò)線程將在必要時(shí)查看流的前幾個(gè)字節(jié)。響應(yīng)的Content-Type標(biāo)頭應(yīng)說明它是什么數(shù)據(jù)類型,但是由于可能丟失或錯誤, 因此在此處進(jìn)行MIME Type檢查。
如果響應(yīng)是HTML文件,則下一步是將數(shù)據(jù)傳遞到渲染器進(jìn)程,但是如果是zip文件或其他文件,則意味著這是下載請求,因此它們需要將數(shù)據(jù)傳遞到下載管理器。

4.
網(wǎng)絡(luò)線程從安全站點(diǎn)詢問響應(yīng)數(shù)據(jù)是否為HTML,并進(jìn)行安全檢查。

在這個(gè)時(shí)候,瀏覽器已經(jīng)拿到響應(yīng)了,接下來就開始進(jìn)行渲染了。
5.
一旦完成所有檢查,并且Network線程確信瀏覽器應(yīng)導(dǎo)航到請求的站點(diǎn),則Network線程將告知UI線程數(shù)據(jù)已準(zhǔn)備就緒。然后,UI線程找到一個(gè)渲染器進(jìn)程來進(jìn)行網(wǎng)頁渲染。

6.
現(xiàn)在已經(jīng)準(zhǔn)備好數(shù)據(jù)和渲染器進(jìn)程,將IPC從瀏覽器進(jìn)程發(fā)送到渲染器進(jìn)程以提交導(dǎo)航。它還會傳遞數(shù)據(jù)流,因此渲染器進(jìn)程可以繼續(xù)接收HTML數(shù)據(jù)。一旦瀏覽器進(jìn)程聽到確認(rèn)已在渲染器進(jìn)程中進(jìn)行提交的確認(rèn),導(dǎo)航即完成,文檔加載階段開始。
此時(shí),地址欄已更新,安全指示符和站點(diǎn)設(shè)置UI反映了新頁面的站點(diǎn)信息。選項(xiàng)卡的會話歷史記錄將被更新,因此后退/前進(jìn)按鈕將逐步瀏覽剛剛導(dǎo)航到的站點(diǎn)。為了方便在關(guān)閉選項(xiàng)卡或窗口時(shí)恢復(fù)選項(xiàng)卡/會話,會話歷史記錄存儲在磁盤上。


到這里為止,瀏覽器的請求和響應(yīng)就完成了。那在響應(yīng)之后如何渲染呢,我們接著往下看
渲染
先說幾個(gè)渲染進(jìn)程內(nèi)將要工作的線程:
?主線程(Main thread):下載資源、執(zhí)行js、計(jì)算樣式、進(jìn)行布局、繪制合成?光柵線程(Raster thread)?合成線程(Compositor thread)?工作線程(Worker thread)
在下面的渲染過程中,其實(shí)就是這四個(gè)進(jìn)程的互相配合,我們一起來看下吧。
1.
當(dāng)渲染過程接收提交消息用于導(dǎo)航和開始接收HTML數(shù)據(jù),主線程開始解析文本串(HTML),使之成為一個(gè) Document Object Model ,也就是 DOM。
2.
網(wǎng)站有用到圖片,CSS 和JavaScript的話,這些東西需要從網(wǎng)絡(luò)或者緩存中加載,主線程可以邊請求,邊預(yù)加載構(gòu)建DOM。

3.
當(dāng)HTML解析器找到 <script> 標(biāo)簽后,將會暫停HTML解析,并且必須加載、解析和執(zhí)行 JavaScript的代碼。為什么?因?yàn)镴avaScript 可以使用諸如 document.write() 更改整個(gè)DOM結(jié)構(gòu)!所以開發(fā)人員在寫代碼的時(shí)候可以在 <script> 標(biāo)簽上加 async 或者 defer 屬性。然后瀏覽器將會異步加載并運(yùn)行JavaScript,不會阻止解析。
4.
主線程解析CSS樣式,并把CSS樣式一一對應(yīng)到DOM節(jié)點(diǎn)上,注意,此時(shí)CSS頁面還沒有生效,只是樣式和節(jié)點(diǎn)綁定了關(guān)系。

5.
接下來CSS根據(jù)DOM節(jié)點(diǎn),會生成類似于DOM結(jié)構(gòu)的一個(gè)布局樹,僅包含了頁面上可見內(nèi)容的信息,如果有 display: none 等,則該元素不屬于布局樹。如果有p::before {content:"123"} 等偽類的存在,就算它不在DOM中,也會包含在布局樹中。

在此繪制步驟中,主線程遍歷布局樹以創(chuàng)建繪制記錄。繪畫記錄是繪畫過程的注釋,例如“先是背景,然后是文本,然后是矩形”,類似 canvas。
注意:在渲染的時(shí)候,每個(gè)步驟前面操作的結(jié)果都用于創(chuàng)建新數(shù)據(jù),如果布局樹發(fā)生了更改,文檔受影響的部分就會重新繪制,也就是 重繪,開發(fā)過程中要盡量避免這一現(xiàn)象。
6.
至此瀏覽器知道了:文檔的結(jié)構(gòu),每個(gè)DOM元素的樣式,頁面的幾何形狀以及繪制的順序。把這些東西換轉(zhuǎn)為屏幕上像素我們稱之為 光柵化。在現(xiàn)代瀏覽器中執(zhí)行這一行為的過程,稱為 **合成(Compositing)**,就是把頁面各個(gè)部分分成若干層,分別進(jìn)行柵格化,然后合成器線程的單獨(dú)線程中進(jìn)行合成,一個(gè)層可以稱之為一個(gè) layer。

7.
層分好了并確定了順序之后,主線程就把這個(gè)信息提交給合成線程,然后合成器線程把每個(gè)圖層?xùn)鸥窕?,發(fā)送給柵格線程,柵格線程把它們存儲在GPU內(nèi)存內(nèi)。

8.
最終,合成線程將柵格化的塊合成幀,并通過IPC傳遞給瀏覽器進(jìn)程,顯示在屏幕上。

至此,瀏覽器的請求,響應(yīng)和渲染過程結(jié)束!
(一半了,稍微休息一下,我們再繼續(xù)!)
JS單線程
回顧一下,瀏覽器的渲染進(jìn)程中,主線程里包括了執(zhí)行JS,那也就意味著:
JS在瀏覽器的 渲染進(jìn)程(Rendered Process) 的 主線程(Main Thread) 內(nèi)!

記住:JS是被設(shè)計(jì)成單線程的!
JavaScript的單線程,與它的用途有關(guān)。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復(fù)雜的同步問題。比如,假定JavaScript同時(shí)有兩個(gè)線程,一個(gè)線程在某個(gè)DOM節(jié)點(diǎn)上添加內(nèi)容,另一個(gè)線程刪除了這個(gè)節(jié)點(diǎn),這時(shí)瀏覽器應(yīng)該以哪個(gè)線程為準(zhǔn)?—— 阮一峰
所以敘述出來就是:JS邏輯 和 UI渲染 是在一個(gè)線程中順序發(fā)生的,二者同一時(shí)間只可以存在一個(gè)。繼續(xù)回顧一下上面渲染所提到的,HTML解析器必須等待JS運(yùn)行,JS是可以操作DOM 和 布局樹的,會干擾到主線程在解析HTML的順序,從而影響結(jié)果,所以為了頁面的渲染統(tǒng)一,JS被設(shè)計(jì)成了 執(zhí)行阻塞UI渲染型。
同時(shí)也反映出了一個(gè)問題:JS過多會造成頁面卡頓,因?yàn)樽卟幌氯チ?。所以JS的邏輯一定不能冗余。
任務(wù)隊(duì)列
既然JS是單線程,也就意味著里面的邏輯是排隊(duì)運(yùn)行的,后一個(gè)任務(wù)必須等前一個(gè)結(jié)束才可以運(yùn)行。這樣就會出一個(gè)問題,有沒有一種可能是掛起不那么重要的任務(wù),先走重要的,等結(jié)束之后再執(zhí)行掛起的任務(wù)呢?
按照這個(gè)說法的話,所有任務(wù)就可以分成:同步任務(wù)(sync) 和 異步任務(wù)(async) ,同步任務(wù)就是主線程里面的排隊(duì)進(jìn)行,異步任務(wù)就是不進(jìn)入主線程,進(jìn)入一個(gè) “任務(wù)隊(duì)列(task queue)” 的地方呆著,看著主線程里的任務(wù)進(jìn)行,一旦發(fā)現(xiàn)主線程的同步任務(wù)執(zhí)行完了,就通知主線程,說我這里的異步任務(wù)可以執(zhí)行了,該任務(wù)才會進(jìn)入主線程執(zhí)行。
所以有沒有發(fā)現(xiàn),那些鼠標(biāo)點(diǎn)擊事件,頁面滾動,回調(diào)函數(shù),http請求……其實(shí)就在任務(wù)隊(duì)列里面。
事件循環(huán)(Event Loop)
概念
有了任務(wù)隊(duì)列的存在,就會有事件循環(huán)的存在,因?yàn)槿蝿?wù)隊(duì)列中可能有很多任務(wù),一個(gè)在任務(wù)隊(duì)列的任務(wù)進(jìn)入到主線程后,任務(wù)隊(duì)列依然會看著主線程,看看剛進(jìn)去的這個(gè)有沒有執(zhí)行完畢,畢竟任務(wù)隊(duì)列里還有很多沒執(zhí)行的任務(wù),所以主線程去讀取任務(wù)隊(duì)列是循環(huán)不斷的,也就叫做了 事件循環(huán)。
這里放張網(wǎng)圖,基本上一看就明白了(參考自Philip Roberts的演講《Help, I'm stuck in an event-loop[1]》)

定時(shí)器
這個(gè)有點(diǎn)特殊,單獨(dú)講一下。
定時(shí)器不是個(gè)異步事件,是一個(gè)定時(shí)事件,但是仍屬于一個(gè)回調(diào)操作,是被放在任務(wù)隊(duì)列中的。
就算定時(shí)器被設(shè)置的時(shí)間是0,它也仍然會在主線程邏輯走完之后(此時(shí)棧清空了),再執(zhí)行,所以時(shí)間是0的定時(shí)器,它可以被理解為希望盡早的執(zhí)行。
需要注意的是,setTimeout()只是將事件插入了"任務(wù)隊(duì)列",必須等到當(dāng)前代碼(執(zhí)行棧)執(zhí)行完,主線程才會去執(zhí)行它指定的回調(diào)函數(shù)。要是當(dāng)前代碼耗時(shí)很長,有可能要等很久,所以并沒有辦法保證,回調(diào)函數(shù)一定會在setTimeout()指定的時(shí)間執(zhí)行?!钜环?/p>
微任務(wù)(MicroTask)和宏任務(wù)(MacroTask)
這段參考Tasks, microtasks, queues and schedules[2],一位谷歌開發(fā)者人員用實(shí)例講述了任務(wù)執(zhí)行順序,并帶有在線Demo,強(qiáng)烈建議過一遍(英語不好就逐句翻譯)。
在JS中,主線程的任務(wù)叫 宏任務(wù)(MacroTask) ,宏任務(wù)執(zhí)行完畢后,立即執(zhí)行的任務(wù)叫 微任務(wù)(MicroTask) 。
宏任務(wù):
?主線程已經(jīng)存在了的任務(wù)叫宏任務(wù),從任務(wù)隊(duì)列中進(jìn)入主線程的任務(wù)也叫宏任務(wù),一個(gè)宏任務(wù)執(zhí)行過程中,從頭到尾不會執(zhí)行其他的東西?瀏覽器會在一個(gè)宏任務(wù)結(jié)束后,在下一個(gè)宏任務(wù)開始前,對頁面進(jìn)行重新渲染
微任務(wù):
?當(dāng)前宏任務(wù)執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)叫微任務(wù),也就是說它在前宏任務(wù)之后,后宏任務(wù)之前,渲染之前!?它的速度比定時(shí)器要快,因?yàn)椴挥玫却秩荆〞r(shí)器是宏任務(wù)?在一個(gè)宏任務(wù)執(zhí)行結(jié)束后,所有的微任務(wù)都會執(zhí)行完畢(渲染前)
基于上面的概念,我們可以給常用的任務(wù)分下類:
?宏任務(wù):主代碼,setTimeout,setInterval,setImmediate,requestAnimationFrame,I/O,UI渲染?微任務(wù):Promise,process.nextTick,MutationObserve,queueMicrotask
當(dāng)然 Vue 中的 nextTick 也就屬于微任務(wù)了,最后放一張圖幫助一下理解:

參考資料:
JavaScript 運(yùn)行機(jī)制詳解:再談Event Loop[3]
Event Loops[4]
淺談瀏覽器架構(gòu)、單線程js、事件循環(huán)、消息隊(duì)列、宏任務(wù)和微任務(wù)[5]
瀏覽器多進(jìn)程到JS單線程,JS運(yùn)行機(jī)制最全面的一次梳理[6]
Inside look at modern web browser (part 2)[7]
Inside look at modern web browser (part 3)[8]
overview-of-the-parsing-model[9]
上最全!圖解瀏覽器的工作原理[10]
可以加超級貓的 wx:CB834301747 ,一起閑聊前端。
微信搜 “前端GitHub”,回復(fù) “電子書” 即可以獲得 160 本前端精華書籍哦。
往期精文
