----------正文開始----------
最近發(fā)現(xiàn)有不少介紹JS單線程運(yùn)行機(jī)制的文章,但是發(fā)現(xiàn)很多都僅僅是介紹某一部分的知識(shí)赋朦,而且各個(gè)地方的說法還不統(tǒng)一,容易造成困惑缰盏。
因此準(zhǔn)備梳理這塊知識(shí)點(diǎn),結(jié)合已有的認(rèn)知,基于網(wǎng)上的大量參考資料娇唯,
從瀏覽器多進(jìn)程到JS單線程漱凝,將JS引擎的運(yùn)行機(jī)制系統(tǒng)的梳理一遍感论。
展現(xiàn)形式:由于是屬于系統(tǒng)梳理型润努,就沒有由淺入深了倚聚,而是從頭到尾的梳理知識(shí)體系砌些,
重點(diǎn)是將關(guān)鍵節(jié)點(diǎn)的知識(shí)點(diǎn)串聯(lián)起來纵东,而不是僅僅剖析某一部分知識(shí)。
內(nèi)容是:從瀏覽器進(jìn)程啥寇,再到瀏覽器內(nèi)核運(yùn)行偎球,再到JS引擎單線程,再到JS事件循環(huán)機(jī)制辑甜,從頭到尾系統(tǒng)的梳理一遍衰絮,擺脫碎片化,形成一個(gè)知識(shí)體系
目標(biāo)是:看完這篇文章后磷醋,對(duì)瀏覽器多進(jìn)程猫牡,JS單線程,JS事件循環(huán)機(jī)制這些都能有一定理解邓线,
有一個(gè)知識(shí)體系骨架淌友,而不是似懂非懂的感覺。
另外骇陈,本文適合有一定經(jīng)驗(yàn)的前端人員震庭,新手請(qǐng)規(guī)避,避免受到過多的概念沖擊你雌∑髁可以先存起來,有了一定理解后再看婿崭,也可以分成多批次觀看拨拓,避免過度疲勞。
大綱
區(qū)分進(jìn)程和線程
瀏覽器是多進(jìn)程的
瀏覽器都包含哪些進(jìn)程逛球?
瀏覽器多進(jìn)程的優(yōu)勢(shì)
重點(diǎn)是瀏覽器內(nèi)核(渲染進(jìn)程)
Browser進(jìn)程和瀏覽器內(nèi)核(Renderer進(jìn)程)的通信過程
梳理瀏覽器內(nèi)核中線程之間的關(guān)系
GUI渲染線程與JS引擎線程互斥
JS阻塞頁面加載
WebWorker千元,JS的多線程?
WebWorker與SharedWorker
簡(jiǎn)單梳理下瀏覽器渲染流程
load事件與DOMContentLoaded事件的先后
css加載是否會(huì)阻塞dom樹渲染颤绕?
普通圖層和復(fù)合圖層
從Event Loop談JS的運(yùn)行機(jī)制
事件循環(huán)機(jī)制進(jìn)一步補(bǔ)充
單獨(dú)說說定時(shí)器
setTimeout而不是setInterval
事件循環(huán)進(jìn)階:macrotask與microtask
寫在最后的話
區(qū)分進(jìn)程和線程
線程和進(jìn)程區(qū)分不清幸海,是很多新手都會(huì)犯的錯(cuò)誤,沒有關(guān)系奥务。這很正常物独。先看看下面這個(gè)形象的比喻:
- 進(jìn)程是一個(gè)工廠,工廠有它的獨(dú)立資源- 工廠之間相互獨(dú)立- 線程是工廠中的工人氯葬,多個(gè)工人協(xié)作完成任務(wù)- 工廠內(nèi)有一個(gè)或多個(gè)工人- 工人之間共享空間復(fù)制代碼
再完善完善概念:
- 工廠的資源 -> 系統(tǒng)分配的內(nèi)存(獨(dú)立的一塊內(nèi)存)- 工廠之間的相互獨(dú)立 -> 進(jìn)程之間相互獨(dú)立- 多個(gè)工人協(xié)作完成任務(wù) -> 多個(gè)線程在進(jìn)程中協(xié)作完成任務(wù)- 工廠內(nèi)有一個(gè)或多個(gè)工人 -> 一個(gè)進(jìn)程由一個(gè)或多個(gè)線程組成- 工人之間共享空間 -> 同一進(jìn)程下的各個(gè)線程之間共享程序的內(nèi)存空間(包括代碼段容诬、數(shù)據(jù)集、堆等)復(fù)制代碼
然后再鞏固下:
如果是windows電腦中蛤铜,可以打開任務(wù)管理器,可以看到有一個(gè)后臺(tái)進(jìn)程列表秽澳。對(duì),那里就是查看進(jìn)程的地方戏羽,而且可以看到每個(gè)進(jìn)程的內(nèi)存資源信息以及cpu占有率担神。
所以,應(yīng)該更容易理解了:進(jìn)程是cpu資源分配的最小單位(系統(tǒng)會(huì)給它分配內(nèi)存)
最后始花,再用較為官方的術(shù)語描述一遍:
進(jìn)程是cpu資源分配的最小單位(是能擁有資源和獨(dú)立運(yùn)行的最小單位)
線程是cpu調(diào)度的最小單位(線程是建立在進(jìn)程的基礎(chǔ)上的一次程序運(yùn)行單位妄讯,一個(gè)進(jìn)程中可以有多個(gè)線程)
tips
不同進(jìn)程之間也可以通信,不過代價(jià)較大
現(xiàn)在酷宵,一般通用的叫法:單線程與多線程亥贸,都是指在一個(gè)進(jìn)程內(nèi)的單和多。(所以核心還是得屬于一個(gè)進(jìn)程才行)
瀏覽器是多進(jìn)程的
理解了進(jìn)程與線程了區(qū)別后浇垦,接下來對(duì)瀏覽器進(jìn)行一定程度上的認(rèn)識(shí):(先看下簡(jiǎn)化理解)
瀏覽器是多進(jìn)程的
瀏覽器之所以能夠運(yùn)行炕置,是因?yàn)橄到y(tǒng)給它的進(jìn)程分配了資源(cpu、內(nèi)存)
簡(jiǎn)單點(diǎn)理解溜族,每打開一個(gè)Tab頁讹俊,就相當(dāng)于創(chuàng)建了一個(gè)獨(dú)立的瀏覽器進(jìn)程。
關(guān)于以上幾點(diǎn)的驗(yàn)證煌抒,請(qǐng)?jiān)俚谝粡垐D:
圖中打開了Chrome瀏覽器的多個(gè)標(biāo)簽頁仍劈,然后可以在Chrome的任務(wù)管理器中看到有多個(gè)進(jìn)程(分別是每一個(gè)Tab頁面有一個(gè)獨(dú)立的進(jìn)程,以及一個(gè)主進(jìn)程)寡壮。感興趣的可以自行嘗試下贩疙,如果再多打開一個(gè)Tab頁,進(jìn)程正常會(huì)+1以上
**注意:**在這里瀏覽器應(yīng)該也有自己的優(yōu)化機(jī)制况既,有時(shí)候打開多個(gè)tab頁后这溅,可以在Chrome任務(wù)管理器中看到,有些進(jìn)程被合并了
(所以每一個(gè)Tab標(biāo)簽對(duì)應(yīng)一個(gè)進(jìn)程并不一定是絕對(duì)的)
瀏覽器都包含哪些進(jìn)程棒仍?
知道了瀏覽器是多進(jìn)程后悲靴,再來看看它到底包含哪些進(jìn)程:(為了簡(jiǎn)化理解,僅列舉主要進(jìn)程)
1.Browser進(jìn)程:瀏覽器的主進(jìn)程(負(fù)責(zé)協(xié)調(diào)莫其、主控)癞尚,只有一個(gè)。作用有
負(fù)責(zé)瀏覽器界面顯示乱陡,與用戶交互浇揩。如前進(jìn),后退等
負(fù)責(zé)各個(gè)頁面的管理憨颠,創(chuàng)建和銷毀其他進(jìn)程
將Renderer進(jìn)程得到的內(nèi)存中的Bitmap胳徽,繪制到用戶界面上
網(wǎng)絡(luò)資源的管理积锅,下載等
2.第三方插件進(jìn)程:每種類型的插件對(duì)應(yīng)一個(gè)進(jìn)程,僅當(dāng)使用該插件時(shí)才創(chuàng)建
3.GPU進(jìn)程:最多一個(gè)养盗,用于3D繪制等
4.瀏覽器渲染進(jìn)程(瀏覽器內(nèi)核)(Renderer進(jìn)程缚陷,內(nèi)部是多線程的):默認(rèn)每個(gè)Tab頁面一個(gè)進(jìn)程,互不影響爪瓜。主要作用為
頁面渲染蹬跃,腳本執(zhí)行,事件處理等
強(qiáng)化記憶:在瀏覽器中打開一個(gè)網(wǎng)頁相當(dāng)于新起了一個(gè)進(jìn)程(進(jìn)程內(nèi)有自己的多線程)
當(dāng)然铆铆,瀏覽器有時(shí)會(huì)將多個(gè)進(jìn)程合并(譬如打開多個(gè)空白標(biāo)簽頁后,會(huì)發(fā)現(xiàn)多個(gè)空白標(biāo)簽頁被合并成了一個(gè)進(jìn)程)丹喻,如圖
另外薄货,可以通過Chrome的更多工具 -> 任務(wù)管理器自行驗(yàn)證
瀏覽器多進(jìn)程的優(yōu)勢(shì)
相比于單進(jìn)程瀏覽器,多進(jìn)程有如下優(yōu)點(diǎn):
避免單個(gè)page crash影響整個(gè)瀏覽器
避免第三方插件crash影響整個(gè)瀏覽器
多進(jìn)程充分利用多核優(yōu)勢(shì)
方便使用沙盒模型隔離插件等進(jìn)程碍论,提高瀏覽器穩(wěn)定性
簡(jiǎn)單點(diǎn)理解:如果瀏覽器是單進(jìn)程谅猾,那么某個(gè)Tab頁崩潰了,就影響了整個(gè)瀏覽器鳍悠,體驗(yàn)有多差税娜;同理如果是單進(jìn)程,插件崩潰了也會(huì)影響整個(gè)瀏覽器藏研;而且多進(jìn)程還有其它的諸多優(yōu)勢(shì)敬矩。。蠢挡。
當(dāng)然弧岳,內(nèi)存等資源消耗也會(huì)更大,有點(diǎn)空間換時(shí)間的意思业踏。
重點(diǎn)是瀏覽器內(nèi)核(渲染進(jìn)程)
重點(diǎn)來了禽炬,我們可以看到,上面提到了這么多的進(jìn)程勤家,那么腹尖,對(duì)于普通的前端操作來說,最終要的是什么呢伐脖?答案是渲染進(jìn)程
可以這樣理解热幔,頁面的渲染,JS的執(zhí)行晓殊,事件的循環(huán)断凶,都在這個(gè)進(jìn)程內(nèi)進(jìn)行。接下來重點(diǎn)分析這個(gè)進(jìn)程
請(qǐng)牢記巫俺,瀏覽器的渲染進(jìn)程是多線程的(這點(diǎn)如果不理解认烁,請(qǐng)回頭看進(jìn)程和線程的區(qū)分)
終于到了線程這個(gè)概念了??,好親切。那么接下來看看它都包含了哪些線程(列舉一些主要常駐線程):
1.GUI渲染線程
負(fù)責(zé)渲染瀏覽器界面却嗡,解析HTML舶沛,CSS,構(gòu)建DOM樹和RenderObject樹窗价,布局和繪制等如庭。
當(dāng)界面需要重繪(Repaint)或由于某種操作引發(fā)回流(reflow)時(shí),該線程就會(huì)執(zhí)行
注意撼港,GUI渲染線程與JS引擎線程是互斥的坪它,當(dāng)JS引擎執(zhí)行時(shí)GUI線程會(huì)被掛起(相當(dāng)于被凍結(jié)了),GUI更新會(huì)被保存在一個(gè)隊(duì)列中等到JS引擎空閑時(shí)立即被執(zhí)行帝牡。
2.JS引擎線程
也稱為JS內(nèi)核往毡,負(fù)責(zé)處理Javascript腳本程序。(例如V8引擎)
JS引擎線程負(fù)責(zé)解析Javascript腳本靶溜,運(yùn)行代碼开瞭。
JS引擎一直等待著任務(wù)隊(duì)列中任務(wù)的到來,然后加以處理罩息,一個(gè)Tab頁(renderer進(jìn)程)中無論什么時(shí)候都只有一個(gè)JS線程在運(yùn)行JS程序
同樣注意嗤详,GUI渲染線程與JS引擎線程是互斥的,所以如果JS執(zhí)行的時(shí)間過長(zhǎng)瓷炮,這樣就會(huì)造成頁面的渲染不連貫葱色,導(dǎo)致頁面渲染加載阻塞。
3.事件觸發(fā)線程
歸屬于瀏覽器而不是JS引擎崭别,用來控制事件循環(huán)(可以理解冬筒,JS引擎自己都忙不過來,需要瀏覽器另開線程協(xié)助)
當(dāng)JS引擎執(zhí)行代碼塊如setTimeOut時(shí)(也可來自瀏覽器內(nèi)核的其他線程,如鼠標(biāo)點(diǎn)擊茅主、AJAX異步請(qǐng)求等)舞痰,會(huì)將對(duì)應(yīng)任務(wù)添加到事件線程中
當(dāng)對(duì)應(yīng)的事件符合觸發(fā)條件被觸發(fā)時(shí),該線程會(huì)把事件添加到待處理隊(duì)列的隊(duì)尾诀姚,等待JS引擎的處理
注意响牛,由于JS的單線程關(guān)系,所以這些待處理隊(duì)列中的事件都得排隊(duì)等待JS引擎處理(當(dāng)JS引擎空閑時(shí)才會(huì)去執(zhí)行)
4.定時(shí)觸發(fā)器線程
傳說中的setInterval與setTimeout所在線程
瀏覽器定時(shí)計(jì)數(shù)器并不是由JavaScript引擎計(jì)數(shù)的,(因?yàn)镴avaScript引擎是單線程的, 如果處于阻塞線程狀態(tài)就會(huì)影響記計(jì)時(shí)的準(zhǔn)確)
因此通過單獨(dú)線程來計(jì)時(shí)并觸發(fā)定時(shí)(計(jì)時(shí)完畢后赫段,添加到事件隊(duì)列中呀打,等待JS引擎空閑后執(zhí)行)
注意,W3C在HTML標(biāo)準(zhǔn)中規(guī)定糯笙,規(guī)定要求setTimeout中低于4ms的時(shí)間間隔算為4ms贬丛。
5.異步http請(qǐng)求線程
在XMLHttpRequest在連接后是通過瀏覽器新開一個(gè)線程請(qǐng)求
將檢測(cè)到狀態(tài)變更時(shí),如果設(shè)置有回調(diào)函數(shù)给涕,異步線程就產(chǎn)生狀態(tài)變更事件豺憔,將這個(gè)回調(diào)再放入事件隊(duì)列中额获。再由JavaScript引擎執(zhí)行。
看到這里恭应,如果覺得累了抄邀,可以先休息下,這些概念需要被消化昼榛,畢竟后續(xù)將提到的事件循環(huán)機(jī)制就是基于事件觸發(fā)線程的境肾,所以如果僅僅是看某個(gè)碎片化知識(shí),可能會(huì)有一種似懂非懂的感覺胆屿。要完成的梳理一遍才能快速沉淀奥喻,不易遺忘。放張圖鞏固下吧:
再說一點(diǎn)非迹,為什么JS引擎是單線程的衫嵌?額,這個(gè)問題其實(shí)應(yīng)該沒有標(biāo)準(zhǔn)答案彻秆,譬如,可能僅僅是因?yàn)橛捎诙嗑€程的復(fù)雜性结闸,譬如多線程操作一般要加鎖唇兑,因此最初設(shè)計(jì)時(shí)選擇了單線程。桦锄。扎附。
Browser進(jìn)程和瀏覽器內(nèi)核(Renderer進(jìn)程)的通信過程
看到這里,首先结耀,應(yīng)該對(duì)瀏覽器內(nèi)的進(jìn)程和線程都有一定理解了留夜,那么接下來,再談?wù)劄g覽器的Browser進(jìn)程(控制進(jìn)程)是如何和內(nèi)核通信的图甜,
這點(diǎn)也理解后碍粥,就可以將這部分的知識(shí)串聯(lián)起來,從頭到尾有一個(gè)完整的概念黑毅。
如果自己打開任務(wù)管理器嚼摩,然后打開一個(gè)瀏覽器,就可以看到:任務(wù)管理器中出現(xiàn)了兩個(gè)進(jìn)程(一個(gè)是主控進(jìn)程矿瘦,一個(gè)則是打開Tab頁的渲染進(jìn)程)枕面,然后在這前提下,看下整個(gè)的過程:(簡(jiǎn)化了很多)
Browser進(jìn)程收到用戶請(qǐng)求缚去,首先需要獲取頁面內(nèi)容(譬如通過網(wǎng)絡(luò)下載資源)潮秘,隨后將該任務(wù)通過RendererHost接口傳遞給Render進(jìn)程
Renderer進(jìn)程的Renderer接口收到消息,簡(jiǎn)單解釋后易结,交給渲染線程枕荞,然后開始渲染
渲染線程接收請(qǐng)求柜候,加載網(wǎng)頁并渲染網(wǎng)頁,這其中可能需要Browser進(jìn)程獲取資源和需要GPU進(jìn)程來幫助渲染
當(dāng)然可能會(huì)有JS線程操作DOM(這樣可能會(huì)造成回流并重繪)
最后Render進(jìn)程將結(jié)果傳遞給Browser進(jìn)程
Browser進(jìn)程接收到結(jié)果并將結(jié)果繪制出來
這里繪一張簡(jiǎn)單的圖:(很簡(jiǎn)化)
看完這一整套流程买猖,應(yīng)該對(duì)瀏覽器的運(yùn)作有了一定理解了改橘,這樣有了知識(shí)架構(gòu)的基礎(chǔ)后,后續(xù)就方便往上填充內(nèi)容玉控。
這塊再往深處講的話就涉及到瀏覽器內(nèi)核源碼解析了飞主,不屬于本文范圍。
如果這一塊要深挖高诺,建議去讀一些瀏覽器內(nèi)核源碼解析文章碌识,或者可以先看看參考下來源中的第一篇文章,寫的不錯(cuò)
梳理瀏覽器內(nèi)核中線程之間的關(guān)系
到了這里虱而,已經(jīng)對(duì)瀏覽器的運(yùn)行有了一個(gè)整體的概念筏餐,接下來,先簡(jiǎn)單梳理一些概念
GUI渲染線程與JS引擎線程互斥
由于JavaScript是可操縱DOM的牡拇,如果在修改這些元素屬性同時(shí)渲染界面(即JS線程和UI線程同時(shí)運(yùn)行)魁瞪,那么渲染線程前后獲得的元素?cái)?shù)據(jù)就可能不一致了。
因此為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果惠呼,瀏覽器設(shè)置GUI渲染線程與JS引擎為互斥的關(guān)系导俘,當(dāng)JS引擎執(zhí)行時(shí)GUI線程會(huì)被掛起,
GUI更新則會(huì)被保存在一個(gè)隊(duì)列中等到JS引擎線程空閑時(shí)立即被執(zhí)行剔蹋。
JS阻塞頁面加載
從上述的互斥關(guān)系旅薄,可以推導(dǎo)出,JS如果執(zhí)行時(shí)間過長(zhǎng)就會(huì)阻塞頁面泣崩。
譬如少梁,假設(shè)JS引擎正在進(jìn)行巨量的計(jì)算,此時(shí)就算GUI有更新矫付,也會(huì)被保存到隊(duì)列中凯沪,等待JS引擎空閑后執(zhí)行。
然后技即,由于巨量計(jì)算著洼,所以JS引擎很可能很久很久后才能空閑,自然會(huì)感覺到巨卡無比而叼。
所以身笤,要盡量避免JS執(zhí)行時(shí)間過長(zhǎng),這樣就會(huì)造成頁面的渲染不連貫葵陵,導(dǎo)致頁面渲染加載阻塞的感覺液荸。
WebWorker,JS的多線程脱篙?
前文中有提到JS引擎是單線程的娇钱,而且JS執(zhí)行時(shí)間過長(zhǎng)會(huì)阻塞頁面伤柄,那么JS就真的對(duì)cpu密集型計(jì)算無能為力么?
所以文搂,后來HTML5中支持了Web Worker适刀。
MDN的官方解釋是:
Web Worker為Web內(nèi)容在后臺(tái)線程中運(yùn)行腳本提供了一種簡(jiǎn)單的方法。線程可以執(zhí)行任務(wù)而不干擾用戶界面一個(gè)worker是使用一個(gè)構(gòu)造函數(shù)創(chuàng)建的一個(gè)對(duì)象(e.g. Worker()) 運(yùn)行一個(gè)命名的JavaScript文件 這個(gè)文件包含將在工作線程中運(yùn)行的代碼; workers 運(yùn)行在另一個(gè)全局上下文中,不同于當(dāng)前的window因此煤蹭,使用window快捷方式獲取當(dāng)前全局的范圍 (而不是self) 在一個(gè) Worker 內(nèi)將返回錯(cuò)誤復(fù)制代碼
這樣理解下:
創(chuàng)建Worker時(shí)笔喉,JS引擎向?yàn)g覽器申請(qǐng)開一個(gè)子線程(子線程是瀏覽器開的,完全受主線程控制硝皂,而且不能操作DOM)
JS引擎線程與worker線程間通過特定的方式通信(postMessage API常挚,需要通過序列化對(duì)象來與線程交互特定的數(shù)據(jù))
所以,如果有非常耗時(shí)的工作稽物,請(qǐng)單獨(dú)開一個(gè)Worker線程奄毡,這樣里面不管如何翻天覆地都不會(huì)影響JS引擎主線程,
只待計(jì)算出結(jié)果后贝或,將結(jié)果通信給主線程即可吼过,perfect!
而且注意下,JS引擎是單線程的咪奖,這一點(diǎn)的本質(zhì)仍然未改變那先,Worker可以理解是瀏覽器給JS引擎開的外掛,專門用來解決那些大量計(jì)算問題赡艰。
其它,關(guān)于Worker的詳解就不是本文的范疇了斤葱,因此不再贅述慷垮。
WebWorker與SharedWorker
既然都到了這里,就再提一下SharedWorker(避免后續(xù)將這兩個(gè)概念搞混)
WebWorker只屬于某個(gè)頁面揍堕,不會(huì)和其他頁面的Render進(jìn)程(瀏覽器內(nèi)核進(jìn)程)共享
所以Chrome在Render進(jìn)程中(每一個(gè)Tab頁就是一個(gè)render進(jìn)程)創(chuàng)建一個(gè)新的線程來運(yùn)行Worker中的JavaScript程序料身。
SharedWorker是瀏覽器所有頁面共享的,不能采用與Worker同樣的方式實(shí)現(xiàn)衩茸,因?yàn)樗浑`屬于某個(gè)Render進(jìn)程芹血,可以為多個(gè)Render進(jìn)程共享使用
所以Chrome瀏覽器為SharedWorker單獨(dú)創(chuàng)建一個(gè)進(jìn)程來運(yùn)行JavaScript程序,在瀏覽器中每個(gè)相同的JavaScript只存在一個(gè)SharedWorker進(jìn)程楞慈,不管它被創(chuàng)建多少次幔烛。
看到這里,應(yīng)該就很容易明白了囊蓝,本質(zhì)上就是進(jìn)程和線程的區(qū)別饿悬。SharedWorker由獨(dú)立的進(jìn)程管理,WebWorker只是屬于render進(jìn)程下的一個(gè)線程
簡(jiǎn)單梳理下瀏覽器渲染流程
本來是直接計(jì)劃開始談JS運(yùn)行機(jī)制的聚霜,但想了想狡恬,既然上述都一直在談瀏覽器珠叔,直接跳到JS可能再突兀,因此弟劲,中間再補(bǔ)充下瀏覽器的渲染流程(簡(jiǎn)單版本)
為了簡(jiǎn)化理解祷安,前期工作直接省略成:(要展開的或完全可以寫另一篇超長(zhǎng)文)
- 瀏覽器輸入url,瀏覽器主進(jìn)程接管兔乞,開一個(gè)下載線程汇鞭,然后進(jìn)行 http請(qǐng)求(略去DNS查詢,IP尋址等等操作)报嵌,然后等待響應(yīng)虱咧,獲取內(nèi)容,隨后將內(nèi)容通過RendererHost接口轉(zhuǎn)交給Renderer進(jìn)程- 瀏覽器渲染流程開始復(fù)制代碼
瀏覽器器內(nèi)核拿到內(nèi)容后锚国,渲染大概可以劃分成以下幾個(gè)步驟:
1.解析html建立dom樹
2.解析css構(gòu)建render樹(將CSS代碼解析成樹形的數(shù)據(jù)結(jié)構(gòu)腕巡,然后結(jié)合DOM合并成render樹)
3.布局render樹(Layout/reflow),負(fù)責(zé)各元素尺寸血筑、位置的計(jì)算
4.繪制render樹(paint)绘沉,繪制頁面像素信息
5.瀏覽器會(huì)將各層的信息發(fā)送給GPU,GPU會(huì)將各層合成(composite)豺总,顯示在屏幕上车伞。
所有詳細(xì)步驟都已經(jīng)略去,渲染完畢后就是load事件了喻喳,之后就是自己的JS邏輯處理了
既然略去了一些詳細(xì)的步驟另玖,那么就提一些可能需要注意的細(xì)節(jié)把。
這里重繪參考來源中的一張圖:(參考來源第一篇)
load事件與DOMContentLoaded事件的先后
上面提到表伦,渲染完畢后會(huì)觸發(fā)load事件谦去,那么你能分清楚load事件與DOMContentLoaded事件的先后么?
很簡(jiǎn)單蹦哼,知道它們的定義就可以了:
當(dāng) DOMContentLoaded 事件觸發(fā)時(shí)鳄哭,僅當(dāng)DOM加載完成,不包括樣式表纲熏,圖片妆丘。
(譬如如果有async加載的腳本就不一定完成)
當(dāng) onload 事件觸發(fā)時(shí),頁面上所有的DOM局劲,樣式表勺拣,腳本,圖片都已經(jīng)加載完成了鱼填。
(渲染完畢了)
所以宣脉,順序是:DOMContentLoaded -> load
css加載是否會(huì)阻塞dom樹渲染?
這里說的是頭部引入css的情況
首先剔氏,我們都知道:css是由單獨(dú)的下載線程異步下載的塑猖。
然后再說下幾個(gè)現(xiàn)象:
css加載不會(huì)阻塞DOM樹解析(異步加載時(shí)DOM照常構(gòu)建)
但會(huì)阻塞render樹渲染(渲染時(shí)需等css加載完畢竹祷,因?yàn)閞ender樹需要css信息)
這可能也是瀏覽器的一種優(yōu)化機(jī)制。
因?yàn)槟慵虞dcss的時(shí)候羊苟,可能會(huì)修改下面DOM節(jié)點(diǎn)的樣式塑陵,
如果css加載不阻塞render樹渲染的話,那么當(dāng)css加載完之后蜡励,
render樹可能又得重新重繪或者回流了令花,這就造成了一些沒有必要的損耗。
所以干脆就先把DOM樹的結(jié)構(gòu)先解析完凉倚,把可以做的工作做完兼都,然后等你css加載完之后,
在根據(jù)最終的樣式來渲染render樹稽寒,這種做法性能方面確實(shí)會(huì)比較好一點(diǎn)扮碧。
普通圖層和復(fù)合圖層
渲染步驟中就提到了composite概念。
可以簡(jiǎn)單的這樣理解杏糙,瀏覽器渲染的圖層一般包含兩大類:普通圖層以及復(fù)合圖層
首先慎王,普通文檔流內(nèi)可以理解為一個(gè)復(fù)合圖層(這里稱為默認(rèn)復(fù)合層,里面不管添加多少元素宏侍,其實(shí)都是在同一個(gè)復(fù)合圖層中)
其次赖淤,absolute布局(fixed也一樣),雖然可以脫離普通文檔流谅河,但它仍然屬于默認(rèn)復(fù)合層咱旱。
然后,可以通過硬件加速的方式绷耍,聲明一個(gè)新的復(fù)合圖層莽龟,它會(huì)單獨(dú)分配資源(當(dāng)然也會(huì)脫離普通文檔流,這樣一來锨天,不管這個(gè)復(fù)合圖層中怎么變化,也不會(huì)影響默認(rèn)復(fù)合層里的回流重繪)
可以簡(jiǎn)單理解下:GPU中剃毒,各個(gè)復(fù)合圖層是單獨(dú)繪制的病袄,所以互不影響,這也是為什么某些場(chǎng)景硬件加速效果一級(jí)棒
可以Chrome源碼調(diào)試 -> More Tools -> Rendering -> Layer borders中看到赘阀,黃色的就是復(fù)合圖層信息
如下圖益缠。可以驗(yàn)證上述的說法
如何變成復(fù)合圖層(硬件加速)
將該元素變成一個(gè)復(fù)合圖層基公,就是傳說中的硬件加速技術(shù)
最常用的方式:translate3d幅慌、translateZ
opacity屬性/過渡動(dòng)畫(需要?jiǎng)赢媹?zhí)行的過程中才會(huì)創(chuàng)建合成層,動(dòng)畫沒有開始或結(jié)束后元素還會(huì)回到之前的狀態(tài))
will-chang屬性(這個(gè)比較偏僻)轰豆,一般配合opacity與translate使用(而且經(jīng)測(cè)試胰伍,除了上述可以引發(fā)硬件加速的屬性外齿诞,其它屬性并不會(huì)變成復(fù)合層),作用是提前告訴瀏覽器要變化骂租,這樣瀏覽器會(huì)開始做一些優(yōu)化工作(這個(gè)最好用完后就釋放)
等元素
其它祷杈,譬如以前的flash插件
absolute和硬件加速的區(qū)別
可以看到,absolute雖然可以脫離普通文檔流渗饮,但是無法脫離默認(rèn)復(fù)合層但汞。
所以,就算absolute中信息改變時(shí)不會(huì)改變普通文檔流中render樹互站,
但是私蕾,瀏覽器最終繪制時(shí),是整個(gè)復(fù)合層繪制的胡桃,所以absolute中信息的改變踩叭,仍然會(huì)影響整個(gè)復(fù)合層的繪制。
(瀏覽器會(huì)重繪它标捺,如果復(fù)合層中內(nèi)容多懊纳,absolute帶來的繪制信息變化過大,資源消耗是非常嚴(yán)重的)
而硬件加速直接就是在另一個(gè)復(fù)合層了(另起爐灶)亡容,所以它的信息改變不會(huì)影響默認(rèn)復(fù)合層
(當(dāng)然了嗤疯,內(nèi)部肯定會(huì)影響屬于自己的復(fù)合層),僅僅是引發(fā)最后的合成(輸出視圖)
復(fù)合圖層的作用闺兢?
一般一個(gè)元素開啟硬件加速后會(huì)變成復(fù)合圖層茂缚,可以獨(dú)立于普通文檔流中,改動(dòng)后可以避免整個(gè)頁面重繪屋谭,提升性能
但是盡量不要大量使用復(fù)合圖層脚囊,否則由于資源消耗過度,頁面反而會(huì)變的更卡
硬件加速時(shí)請(qǐng)使用index
使用硬件加速時(shí)桐磁,盡可能的使用index悔耘,防止瀏覽器默認(rèn)給后續(xù)的元素創(chuàng)建復(fù)合層渲染
具體的原理時(shí)這樣的:webkit CSS3中,如果這個(gè)元素添加了硬件加速我擂,并且index層級(jí)比較低衬以,
那么在這個(gè)元素的后面其它元素(層級(jí)比這個(gè)元素高的,或者相同的校摩,并且releative或absolute屬性相同的)看峻,
會(huì)默認(rèn)變?yōu)閺?fù)合層渲染,如果處理不當(dāng)會(huì)極大的影響性能
簡(jiǎn)單點(diǎn)理解衙吩,其實(shí)可以認(rèn)為是一個(gè)隱式合成的概念:如果a是一個(gè)復(fù)合圖層互妓,而且b在a上面,那么b也會(huì)被隱式轉(zhuǎn)為一個(gè)復(fù)合圖層,這點(diǎn)需要特別注意
另外冯勉,這個(gè)問題可以在這個(gè)地址看到重現(xiàn)(原作者分析的挺到位的澈蚌,直接上鏈接):
從Event Loop談JS的運(yùn)行機(jī)制
到此時(shí),已經(jīng)是屬于瀏覽器頁面初次渲染完畢后的事情珠闰,JS引擎的一些運(yùn)行機(jī)制分析惜浅。
注意,這里不談可執(zhí)行上下文伏嗜,VO坛悉,scop chain等概念(這些完全可以整理成另一篇文章了),這里主要是結(jié)合Event Loop來談JS代碼是如何執(zhí)行的承绸。
讀這部分的前提是已經(jīng)知道了JS引擎是單線程裸影,而且這里會(huì)用到上文中的幾個(gè)概念:(如果不是很理解,可以回頭溫習(xí))
JS引擎線程
事件觸發(fā)線程
定時(shí)觸發(fā)器線程
然后再理解一個(gè)概念:
JS分為同步任務(wù)和異步任務(wù)
同步任務(wù)都在主線程上執(zhí)行军熏,形成一個(gè)執(zhí)行棧
主線程之外轩猩,事件觸發(fā)線程管理著一個(gè)任務(wù)隊(duì)列,只要異步任務(wù)有了運(yùn)行結(jié)果荡澎,就在任務(wù)隊(duì)列之中放置一個(gè)事件均践。
一旦執(zhí)行棧中的所有同步任務(wù)執(zhí)行完畢(此時(shí)JS引擎空閑),系統(tǒng)就會(huì)讀取任務(wù)隊(duì)列摩幔,將可運(yùn)行的異步任務(wù)添加到可執(zhí)行棧中彤委,開始執(zhí)行。
看圖:
看到這里或衡,應(yīng)該就可以理解了:為什么有時(shí)候setTimeout推入的事件不能準(zhǔn)時(shí)執(zhí)行焦影?因?yàn)榭赡茉谒迫氲绞录斜頃r(shí),主線程還不空閑封断,正在執(zhí)行其它代碼斯辰,
所以自然有誤差。
事件循環(huán)機(jī)制進(jìn)一步補(bǔ)充
這里就直接引用一張圖片來協(xié)助理解:(參考自Philip Roberts的演講《Help, I'm stuck in an event-loop》)
上圖大致描述就是:
主線程運(yùn)行時(shí)會(huì)產(chǎn)生執(zhí)行棧坡疼,
棧中的代碼調(diào)用某些api時(shí)彬呻,它們會(huì)在事件隊(duì)列中添加各種事件(當(dāng)滿足觸發(fā)條件后,如ajax請(qǐng)求完畢)
而棧中的代碼執(zhí)行完畢柄瑰,就會(huì)讀取事件隊(duì)列中的事件闸氮,去執(zhí)行那些回調(diào)
如此循環(huán)
注意,總是要等待棧中的代碼執(zhí)行完畢后才會(huì)去讀取事件隊(duì)列中的事件
單獨(dú)說說定時(shí)器
上述事件循環(huán)機(jī)制的核心是:JS引擎線程和事件觸發(fā)線程
但事件上狱意,里面還有一些隱藏細(xì)節(jié),譬如調(diào)用setTimeout后拯欧,是如何等待特定時(shí)間后才添加到事件隊(duì)列中的详囤?
是JS引擎檢測(cè)的么?當(dāng)然不是了。它是由定時(shí)器線程控制(因?yàn)镴S引擎自己都忙不過來藏姐,根本無暇分身)
為什么要單獨(dú)的定時(shí)器線程隆箩?因?yàn)镴avaScript引擎是單線程的, 如果處于阻塞線程狀態(tài)就會(huì)影響記計(jì)時(shí)的準(zhǔn)確,因此很有必要單獨(dú)開一個(gè)線程用來計(jì)時(shí)羔杨。
什么時(shí)候會(huì)用到定時(shí)器線程捌臊?當(dāng)使用setTimeout或setInterval時(shí),它需要定時(shí)器線程計(jì)時(shí)兜材,計(jì)時(shí)完成后就會(huì)將特定的事件推入事件隊(duì)列中理澎。
譬如:
setTimeout(function(){console.log('hello!');},1000);復(fù)制代碼
這段代碼的作用是當(dāng)1000毫秒計(jì)時(shí)完畢后(由定時(shí)器線程計(jì)時(shí)),將回調(diào)函數(shù)推入事件隊(duì)列中曙寡,等待主線程執(zhí)行
setTimeout(function(){console.log('hello!');},0);console.log('begin');復(fù)制代碼
這段代碼的效果是最快的時(shí)間內(nèi)將回調(diào)函數(shù)推入事件隊(duì)列中糠爬,等待主線程執(zhí)行
注意:
執(zhí)行結(jié)果是:先begin后hello!
雖然代碼的本意是0毫秒后就推入事件隊(duì)列,但是W3C在HTML標(biāo)準(zhǔn)中規(guī)定举庶,規(guī)定要求setTimeout中低于4ms的時(shí)間間隔算為4ms执隧。
(不過也有一說是不同瀏覽器有不同的最小時(shí)間設(shè)定)
就算不等待4ms,就算假設(shè)0毫秒就推入事件隊(duì)列户侥,也會(huì)先執(zhí)行begin(因?yàn)橹挥锌蓤?zhí)行棧內(nèi)空了后才會(huì)主動(dòng)讀取事件隊(duì)列)
setTimeout而不是setInterval
用setTimeout模擬定期計(jì)時(shí)和直接用setInterval是有區(qū)別的镀琉。
因?yàn)槊看蝧etTimeout計(jì)時(shí)到后就會(huì)去執(zhí)行,然后執(zhí)行一段時(shí)間后才會(huì)繼續(xù)setTimeout蕊唐,中間就多了誤差
(誤差多少與代碼執(zhí)行時(shí)間有關(guān))
而setInterval則是每次都精確的隔一段時(shí)間推入一個(gè)事件
(但是屋摔,事件的實(shí)際執(zhí)行時(shí)間不一定就準(zhǔn)確,還有可能是這個(gè)事件還沒執(zhí)行完畢刃泌,下一個(gè)事件就來了)
而且setInterval有一些比較致命的問題就是:
累計(jì)效應(yīng)(上面提到的)凡壤,如果setInterval代碼在(setInterval)再次添加到隊(duì)列之前還沒有完成執(zhí)行,
就會(huì)導(dǎo)致定時(shí)器代碼連續(xù)運(yùn)行好幾次耙替,而之間沒有間隔亚侠。
就算正常間隔執(zhí)行,多個(gè)setInterval的代碼執(zhí)行時(shí)間可能會(huì)比預(yù)期兴咨取(因?yàn)榇a執(zhí)行需要一定時(shí)間)
譬如像iOS的webview,或者Safari等瀏覽器中都有一個(gè)特點(diǎn)硝烂,在滾動(dòng)的時(shí)候是不執(zhí)行JS的,如果使用了setInterval铜幽,會(huì)發(fā)現(xiàn)在滾動(dòng)結(jié)束后會(huì)執(zhí)行多次由于滾動(dòng)不執(zhí)行JS積攢回調(diào)滞谢,如果回調(diào)執(zhí)行時(shí)間過長(zhǎng),就會(huì)非常容器造成卡頓問題和一些不可知的錯(cuò)誤(這一塊后續(xù)有補(bǔ)充,setInterval自帶的優(yōu)化除抛,不會(huì)重復(fù)添加回調(diào))
而且把瀏覽器最小化顯示等操作時(shí)狮杨,setInterval并不是不執(zhí)行程序,
它會(huì)把setInterval的回調(diào)函數(shù)放在隊(duì)列中到忽,等瀏覽器窗口再次打開時(shí)橄教,一瞬間全部執(zhí)行時(shí)
所以清寇,鑒于這么多但問題,目前一般認(rèn)為的最佳方案是:用setTimeout模擬setInterval护蝶,或者特殊場(chǎng)合直接用requestAnimationFrame
補(bǔ)充:JS高程中有提到华烟,JS引擎會(huì)對(duì)setInterval進(jìn)行優(yōu)化,如果當(dāng)前事件隊(duì)列中有setInterval的回調(diào)持灰,不會(huì)重復(fù)添加盔夜。不過,仍然是有很多問題堤魁。喂链。。
事件循環(huán)進(jìn)階:macrotask與microtask
這段參考了參考來源中的第2篇文章(英文版的)姨涡,(加了下自己的理解重新描述了下)衩藤,
強(qiáng)烈推薦有英文基礎(chǔ)的同學(xué)直接觀看原文,作者描述的很清晰涛漂,示例也很不錯(cuò)赏表,如下:
jakearchibald.com/2015/tasks-…
上文中將JS事件循環(huán)機(jī)制梳理了一遍,在ES5的情況是夠用了匈仗,但是在ES6盛行的現(xiàn)在瓢剿,仍然會(huì)遇到一些問題,譬如下面這題:
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');復(fù)制代碼
嗯哼悠轩,它的正確執(zhí)行順序是這樣子的:
script startscript endpromise1promise2setTimeout復(fù)制代碼
為什么呢间狂?因?yàn)镻romise里有了一個(gè)一個(gè)新的概念:microtask
或者,進(jìn)一步火架,JS中分為兩種任務(wù)類型:macrotask和microtask鉴象,在ECMAScript中,microtask稱為jobs何鸡,macrotask可稱為task
它們的定義纺弊?區(qū)別?簡(jiǎn)單點(diǎn)可以按如下理解:
macrotask(又稱之為宏任務(wù))骡男,可以理解是每次執(zhí)行棧執(zhí)行的代碼就是一個(gè)宏任務(wù)(包括每次從事件隊(duì)列中獲取一個(gè)事件回調(diào)并放到執(zhí)行棧中執(zhí)行)
每一個(gè)task會(huì)從頭到尾將這個(gè)任務(wù)執(zhí)行完畢淆游,不會(huì)執(zhí)行其它
瀏覽器為了能夠使得JS內(nèi)部task與DOM任務(wù)能夠有序的執(zhí)行,會(huì)在一個(gè)task執(zhí)行結(jié)束后隔盛,在下一個(gè) task 執(zhí)行開始前犹菱,對(duì)頁面進(jìn)行重新渲染(task->渲染->task->...)
microtask(又稱為微任務(wù)),可以理解是在當(dāng)前 task 執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)
也就是說吮炕,在當(dāng)前task任務(wù)后腊脱,下一個(gè)task之前,在渲染之前
所以它的響應(yīng)速度相比setTimeout(setTimeout是task)會(huì)更快龙亲,因?yàn)闊o需等渲染
也就是說陕凹,在某一個(gè)macrotask執(zhí)行完后震鹉,就會(huì)將在它執(zhí)行期間產(chǎn)生的所有microtask都執(zhí)行完畢(在渲染前)
分別很么樣的場(chǎng)景會(huì)形成macrotask和microtask呢?
macrotask:主代碼塊捆姜,setTimeout,setInterval等(可以看到迎膜,事件隊(duì)列中的每一個(gè)事件都是一個(gè)macrotask)
microtask:Promise泥技,process.nextTick等
補(bǔ)充:在node環(huán)境下,process.nextTick的優(yōu)先級(jí)高于Promise磕仅,也就是可以簡(jiǎn)單理解為:在宏任務(wù)結(jié)束后會(huì)先執(zhí)行微任務(wù)隊(duì)列中的nextTickQueue部分珊豹,然后才會(huì)執(zhí)行微任務(wù)中的Promise部分。
參考:segmentfault.com/q/101000001…
再根據(jù)線程來理解下:
macrotask中的事件都是放在一個(gè)事件隊(duì)列中的榕订,而這個(gè)隊(duì)列由事件觸發(fā)線程維護(hù)
microtask中的所有微任務(wù)都是添加到微任務(wù)隊(duì)列(Job Queues)中店茶,等待當(dāng)前macrotask執(zhí)行完畢后執(zhí)行,而這個(gè)隊(duì)列由JS引擎線程維護(hù)(這點(diǎn)由自己理解+推測(cè)得出劫恒,因?yàn)樗窃谥骶€程下無縫執(zhí)行的)
所以贩幻,總結(jié)下運(yùn)行機(jī)制:
執(zhí)行一個(gè)宏任務(wù)(棧中沒有就從事件隊(duì)列中獲取)
執(zhí)行過程中如果遇到微任務(wù)两嘴,就將它添加到微任務(wù)的任務(wù)隊(duì)列中
宏任務(wù)執(zhí)行完畢后丛楚,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)
當(dāng)前宏任務(wù)執(zhí)行完畢,開始檢查渲染憔辫,然后GUI線程接管渲染
渲染完畢后趣些,JS線程繼續(xù)接管,開始下一個(gè)宏任務(wù)(從事件隊(duì)列中獲确∧)
如圖:
另外坏平,請(qǐng)注意下Promise的polyfill與官方版本的區(qū)別:
官方版本中,是標(biāo)準(zhǔn)的microtask形式
polyfill锦亦,一般都是通過setTimeout模擬的舶替,所以是macrotask形式
請(qǐng)?zhí)貏e注意這兩點(diǎn)區(qū)別
注意,有一些瀏覽器執(zhí)行結(jié)果不一樣(因?yàn)樗鼈兛赡馨裮icrotask當(dāng)成macrotask來執(zhí)行了)孽亲,
但是為了簡(jiǎn)單坎穿,這里不描述一些不標(biāo)準(zhǔn)的瀏覽器下的場(chǎng)景(但記住,有些瀏覽器可能并不標(biāo)準(zhǔn))
20180126補(bǔ)充:使用MutationObserver實(shí)現(xiàn)microtask
MutationObserver可以用來實(shí)現(xiàn)microtask
(它屬于microtask返劲,優(yōu)先級(jí)小于Promise玲昧,
一般是Promise不支持時(shí)才會(huì)這樣做)
它是HTML5中的新特性,作用是:監(jiān)聽一個(gè)DOM變動(dòng)篮绿,
當(dāng)DOM對(duì)象樹發(fā)生任何變動(dòng)時(shí)孵延,Mutation Observer會(huì)得到通知
像以前的Vue源碼中就是利用它來模擬nextTick的,
具體原理是亲配,創(chuàng)建一個(gè)TextNode并監(jiān)聽內(nèi)容變化尘应,
然后要nextTick的時(shí)候去改一下這個(gè)節(jié)點(diǎn)的文本內(nèi)容惶凝,
如下:(Vue的源碼,未修改)
varcounter =1varobserver =newMutationObserver(nextTickHandler)vartextNode =document.createTextNode(String(counter))observer.observe(textNode, {characterData:true})timerFunc =()=>{? ? counter = (counter +1) %2textNode.data =String(counter)}復(fù)制代碼
不過犬钢,現(xiàn)在的Vue(2.5+)的nextTick實(shí)現(xiàn)移除了MutationObserver的方式(據(jù)說是兼容性原因)苍鲜,
取而代之的是使用MessageChannel
(當(dāng)然,默認(rèn)情況仍然是Promise玷犹,不支持才兼容的)混滔。
MessageChannel屬于宏任務(wù),優(yōu)先級(jí)是:MessageChannel->setTimeout歹颓,所以Vue(2.5+)內(nèi)部的nextTick與2.4及之前的實(shí)現(xiàn)是不一樣的坯屿,需要注意下。
這里不展開巍扛,可以看下juejin.im/post/5a1af8…
作者:dailc
鏈接:https://juejin.im/post/5a6547d0f265da3e283a1df7
來源:掘金
著作權(quán)歸作者所有领跛。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。