前言
見解有限,如有描述不當(dāng)之處,請幫忙及時指出费变,如有錯誤摧扇,會及時修正。
----------超長文+多圖預(yù)警挚歧,需要花費不少時間扛稽。----------
如果看完本文后,還對進(jìn)程線程傻傻分不清滑负,不清楚瀏覽器多進(jìn)程庇绽、瀏覽器內(nèi)核多線程、JS單線程橙困、JS運行機制的區(qū)別瞧掺。那么請回復(fù)我,一定是我寫的還不夠清晰凡傅,我來改辟狈。。夏跷。
----------正文開始----------
最近發(fā)現(xiàn)有不少介紹JS單線程運行機制的文章哼转,但是發(fā)現(xiàn)很多都僅僅是介紹某一部分的知識,而且各個地方的說法還不統(tǒng)一槽华,容易造成困惑壹蔓。
因此準(zhǔn)備梳理這塊知識點,結(jié)合已有的認(rèn)知猫态,基于網(wǎng)上的大量參考資料佣蓉,
從瀏覽器多進(jìn)程到JS單線程,將JS引擎的運行機制系統(tǒng)的梳理一遍亲雪。
展現(xiàn)形式:由于是屬于系統(tǒng)梳理型勇凭,就沒有由淺入深了,而是從頭到尾的梳理知識體系义辕,
重點是將關(guān)鍵節(jié)點的知識點串聯(lián)起來虾标,而不是僅僅剖析某一部分知識。
內(nèi)容是:從瀏覽器進(jìn)程灌砖,再到瀏覽器內(nèi)核運行璧函,再到JS引擎單線程,再到JS事件循環(huán)機制基显,從頭到尾系統(tǒng)的梳理一遍蘸吓,擺脫碎片化,形成一個知識體系
目標(biāo)是:看完這篇文章后续镇,對瀏覽器多進(jìn)程美澳,JS單線程销部,JS事件循環(huán)機制這些都能有一定理解摸航,
有一個知識體系骨架制跟,而不是似懂非懂的感覺。
另外酱虎,本文適合有一定經(jīng)驗的前端人員雨膨,新手請規(guī)避,避免受到過多的概念沖擊读串×募牵可以先存起來,有了一定理解后再看恢暖,也可以分成多批次觀看排监,避免過度疲勞。
大綱
區(qū)分進(jìn)程和線程
-
瀏覽器是多進(jìn)程的
- 瀏覽器都包含哪些進(jìn)程杰捂?
- 瀏覽器多進(jìn)程的優(yōu)勢
- 重點是瀏覽器內(nèi)核(渲染進(jìn)程)
- Browser進(jìn)程和瀏覽器內(nèi)核(Renderer進(jìn)程)的通信過程
-
梳理瀏覽器內(nèi)核中線程之間的關(guān)系
- GUI渲染線程與JS引擎線程互斥
- JS阻塞頁面加載
- WebWorker舆床,JS的多線程?
- WebWorker與SharedWorker
-
簡單梳理下瀏覽器渲染流程
- load事件與DOMContentLoaded事件的先后
- css加載是否會阻塞dom樹渲染嫁佳?
- 普通圖層和復(fù)合圖層
-
從Event Loop談JS的運行機制
- 事件循環(huán)機制進(jìn)一步補充
- 單獨說說定時器
- setTimeout而不是setInterval
事件循環(huán)進(jìn)階:macrotask與microtask
寫在最后的話
區(qū)分進(jìn)程和線程
線程和進(jìn)程區(qū)分不清挨队,是很多新手都會犯的錯誤,沒有關(guān)系蒿往。這很正常盛垦。先看看下面這個形象的比喻:
- 進(jìn)程是一個工廠,工廠有它的獨立資源
- 工廠之間相互獨立
- 線程是工廠中的工人瓤漏,多個工人協(xié)作完成任務(wù)
- 工廠內(nèi)有一個或多個工人
- 工人之間共享空間
再完善完善概念:
- 工廠的資源 -> 系統(tǒng)分配的內(nèi)存(獨立的一塊內(nèi)存)
- 工廠之間的相互獨立 -> 進(jìn)程之間相互獨立
- 多個工人協(xié)作完成任務(wù) -> 多個線程在進(jìn)程中協(xié)作完成任務(wù)
- 工廠內(nèi)有一個或多個工人 -> 一個進(jìn)程由一個或多個線程組成
- 工人之間共享空間 -> 同一進(jìn)程下的各個線程之間共享程序的內(nèi)存空間(包括代碼段腾夯、數(shù)據(jù)集、堆等)
然后再鞏固下:
如果是windows電腦中蔬充,可以打開任務(wù)管理器俯在,可以看到有一個后臺進(jìn)程列表。對娃惯,那里就是查看進(jìn)程的地方跷乐,而且可以看到每個進(jìn)程的內(nèi)存資源信息以及cpu占有率。
所以趾浅,應(yīng)該更容易理解了:進(jìn)程是cpu資源分配的最小單位(系統(tǒng)會給它分配內(nèi)存)
最后愕提,再用較為官方的術(shù)語描述一遍:
- 進(jìn)程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)
- 線程是cpu調(diào)度的最小單位(線程是建立在進(jìn)程的基礎(chǔ)上的一次程序運行單位,一個進(jìn)程中可以有多個線程)
tips
- 不同進(jìn)程之間也可以通信皿哨,不過代價較大
- 現(xiàn)在浅侨,一般通用的叫法:單線程與多線程,都是指在一個進(jìn)程內(nèi)的單和多证膨。(所以核心還是得屬于一個進(jìn)程才行)
瀏覽器是多進(jìn)程的
理解了進(jìn)程與線程了區(qū)別后如输,接下來對瀏覽器進(jìn)行一定程度上的認(rèn)識:(先看下簡化理解)
- 瀏覽器是多進(jìn)程的
- 瀏覽器之所以能夠運行,是因為系統(tǒng)給它的進(jìn)程分配了資源(cpu、內(nèi)存)
- 簡單點理解不见,每打開一個Tab頁澳化,就相當(dāng)于創(chuàng)建了一個獨立的瀏覽器進(jìn)程。
關(guān)于以上幾點的驗證稳吮,請再第一張圖:
圖中打開了Chrome
瀏覽器的多個標(biāo)簽頁缎谷,然后可以在Chrome的任務(wù)管理器
中看到有多個進(jìn)程(分別是每一個Tab頁面有一個獨立的進(jìn)程,以及一個主進(jìn)程)灶似。
感興趣的可以自行嘗試下列林,如果再多打開一個Tab頁,進(jìn)程正常會+1以上
注意:在這里瀏覽器應(yīng)該也有自己的優(yōu)化機制酪惭,有時候打開多個tab頁后希痴,可以在Chrome任務(wù)管理器中看到,有些進(jìn)程被合并了
(所以每一個Tab標(biāo)簽對應(yīng)一個進(jìn)程并不一定是絕對的)
瀏覽器都包含哪些進(jìn)程春感?
知道了瀏覽器是多進(jìn)程后润梯,再來看看它到底包含哪些進(jìn)程:(為了簡化理解,僅列舉主要進(jìn)程)
-
Browser進(jìn)程:瀏覽器的主進(jìn)程(負(fù)責(zé)協(xié)調(diào)甥厦、主控)纺铭,只有一個。作用有
- 負(fù)責(zé)瀏覽器界面顯示刀疙,與用戶交互舶赔。如前進(jìn),后退等
- 負(fù)責(zé)各個頁面的管理谦秧,創(chuàng)建和銷毀其他進(jìn)程
- 將Renderer進(jìn)程得到的內(nèi)存中的Bitmap竟纳,繪制到用戶界面上
- 網(wǎng)絡(luò)資源的管理,下載等
第三方插件進(jìn)程:每種類型的插件對應(yīng)一個進(jìn)程疚鲤,僅當(dāng)使用該插件時才創(chuàng)建
GPU進(jìn)程:最多一個锥累,用于3D繪制等
-
瀏覽器渲染進(jìn)程(瀏覽器內(nèi)核)(Renderer進(jìn)程,內(nèi)部是多線程的):默認(rèn)每個Tab頁面一個進(jìn)程集歇,互不影響桶略。主要作用為
- 頁面渲染,腳本執(zhí)行诲宇,事件處理等
強化記憶:在瀏覽器中打開一個網(wǎng)頁相當(dāng)于新起了一個進(jìn)程(進(jìn)程內(nèi)有自己的多線程)
當(dāng)然际歼,瀏覽器有時會將多個進(jìn)程合并(譬如打開多個空白標(biāo)簽頁后,會發(fā)現(xiàn)多個空白標(biāo)簽頁被合并成了一個進(jìn)程)姑蓝,如圖
另外鹅心,可以通過Chrome的更多工具 -> 任務(wù)管理器
自行驗證
瀏覽器多進(jìn)程的優(yōu)勢
相比于單進(jìn)程瀏覽器,多進(jìn)程有如下優(yōu)點:
- 避免單個page crash影響整個瀏覽器
- 避免第三方插件crash影響整個瀏覽器
- 多進(jìn)程充分利用多核優(yōu)勢
- 方便使用沙盒模型隔離插件等進(jìn)程纺荧,提高瀏覽器穩(wěn)定性
簡單點理解:如果瀏覽器是單進(jìn)程旭愧,那么某個Tab頁崩潰了颅筋,就影響了整個瀏覽器,體驗有多差输枯;同理如果是單進(jìn)程议泵,插件崩潰了也會影響整個瀏覽器;而且多進(jìn)程還有其它的諸多優(yōu)勢用押。肢簿。靶剑。
當(dāng)然蜻拨,內(nèi)存等資源消耗也會更大,有點空間換時間的意思桩引。
重點是瀏覽器內(nèi)核(渲染進(jìn)程)
重點來了缎讼,我們可以看到,上面提到了這么多的進(jìn)程坑匠,那么血崭,對于普通的前端操作來說,最終要的是什么呢厘灼?答案是渲染進(jìn)程
可以這樣理解夹纫,頁面的渲染,JS的執(zhí)行设凹,事件的循環(huán)舰讹,都在這個進(jìn)程內(nèi)進(jìn)行。接下來重點分析這個進(jìn)程
請牢記闪朱,瀏覽器的渲染進(jìn)程是多線程的(這點如果不理解月匣,請回頭看進(jìn)程和線程的區(qū)分)
終于到了線程這個概念了??,好親切奋姿。那么接下來看看它都包含了哪些線程(列舉一些主要常駐線程):
-
GUI渲染線程
- 負(fù)責(zé)渲染瀏覽器界面锄开,解析HTML,CSS称诗,構(gòu)建DOM樹和RenderObject樹萍悴,布局和繪制等。
- 當(dāng)界面需要重繪(Repaint)或由于某種操作引發(fā)回流(reflow)時寓免,該線程就會執(zhí)行
- 注意退腥,GUI渲染線程與JS引擎線程是互斥的,當(dāng)JS引擎執(zhí)行時GUI線程會被掛起(相當(dāng)于被凍結(jié)了)再榄,GUI更新會被保存在一個隊列中等到JS引擎空閑時立即被執(zhí)行狡刘。
-
JS引擎線程
- 也稱為JS內(nèi)核,負(fù)責(zé)處理Javascript腳本程序困鸥。(例如V8引擎)
- JS引擎線程負(fù)責(zé)解析Javascript腳本嗅蔬,運行代碼剑按。
- JS引擎一直等待著任務(wù)隊列中任務(wù)的到來,然后加以處理澜术,一個Tab頁(renderer進(jìn)程)中無論什么時候都只有一個JS線程在運行JS程序
- 同樣注意艺蝴,GUI渲染線程與JS引擎線程是互斥的,所以如果JS執(zhí)行的時間過長鸟废,這樣就會造成頁面的渲染不連貫猜敢,導(dǎo)致頁面渲染加載阻塞。
-
事件觸發(fā)線程
- 歸屬于瀏覽器而不是JS引擎盒延,用來控制事件循環(huán)(可以理解缩擂,JS引擎自己都忙不過來,需要瀏覽器另開線程協(xié)助)
- 當(dāng)JS引擎執(zhí)行代碼塊如setTimeOut時(也可來自瀏覽器內(nèi)核的其他線程,如鼠標(biāo)點擊添寺、AJAX異步請求等)胯盯,會將對應(yīng)任務(wù)添加到事件線程中
- 當(dāng)對應(yīng)的事件符合觸發(fā)條件被觸發(fā)時,該線程會把事件添加到待處理隊列的隊尾计露,等待JS引擎的處理
- 注意博脑,由于JS的單線程關(guān)系,所以這些待處理隊列中的事件都得排隊等待JS引擎處理(當(dāng)JS引擎空閑時才會去執(zhí)行)
-
定時觸發(fā)器線程
- 傳說中的
setInterval
與setTimeout
所在線程 - 瀏覽器定時計數(shù)器并不是由JavaScript引擎計數(shù)的,(因為JavaScript引擎是單線程的, 如果處于阻塞線程狀態(tài)就會影響記計時的準(zhǔn)確)
- 因此通過單獨線程來計時并觸發(fā)定時(計時完畢后票罐,添加到事件隊列中叉趣,等待JS引擎空閑后執(zhí)行)
- 注意,W3C在HTML標(biāo)準(zhǔn)中規(guī)定该押,規(guī)定要求setTimeout中低于4ms的時間間隔算為4ms疗杉。
- 傳說中的
-
異步http請求線程
- 在XMLHttpRequest在連接后是通過瀏覽器新開一個線程請求
- 將檢測到狀態(tài)變更時,如果設(shè)置有回調(diào)函數(shù)沈善,異步線程就產(chǎn)生狀態(tài)變更事件乡数,將這個回調(diào)再放入事件隊列中。再由JavaScript引擎執(zhí)行闻牡。
看到這里净赴,如果覺得累了,可以先休息下罩润,這些概念需要被消化玖翅,畢竟后續(xù)將提到的事件循環(huán)機制就是基于事件觸發(fā)線程
的,所以如果僅僅是看某個碎片化知識割以,
可能會有一種似懂非懂的感覺金度。要完成的梳理一遍才能快速沉淀,不易遺忘严沥。放張圖鞏固下吧:
再說一點猜极,為什么JS引擎是單線程的?額消玄,這個問題其實應(yīng)該沒有標(biāo)準(zhǔn)答案跟伏,譬如丢胚,可能僅僅是因為由于多線程的復(fù)雜性,譬如多線程操作一般要加鎖受扳,因此最初設(shè)計時選擇了單線程携龟。。勘高。
Browser進(jìn)程和瀏覽器內(nèi)核(Renderer進(jìn)程)的通信過程
看到這里峡蟋,首先,應(yīng)該對瀏覽器內(nèi)的進(jìn)程和線程都有一定理解了华望,那么接下來蕊蝗,再談?wù)劄g覽器的Browser進(jìn)程(控制進(jìn)程)是如何和內(nèi)核通信的,
這點也理解后立美,就可以將這部分的知識串聯(lián)起來匿又,從頭到尾有一個完整的概念方灾。
如果自己打開任務(wù)管理器建蹄,然后打開一個瀏覽器,就可以看到:任務(wù)管理器中出現(xiàn)了兩個進(jìn)程(一個是主控進(jìn)程裕偿,一個則是打開Tab頁的渲染進(jìn)程)洞慎,
然后在這前提下,看下整個的過程:(簡化了很多)
Browser進(jìn)程收到用戶請求嘿棘,首先需要獲取頁面內(nèi)容(譬如通過網(wǎng)絡(luò)下載資源)劲腿,隨后將該任務(wù)通過RendererHost接口傳遞給Render進(jìn)程
-
Renderer進(jìn)程的Renderer接口收到消息,簡單解釋后鸟妙,交給渲染線程焦人,然后開始渲染
- 渲染線程接收請求,加載網(wǎng)頁并渲染網(wǎng)頁重父,這其中可能需要Browser進(jìn)程獲取資源和需要GPU進(jìn)程來幫助渲染
- 當(dāng)然可能會有JS線程操作DOM(這樣可能會造成回流并重繪)
- 最后Render進(jìn)程將結(jié)果傳遞給Browser進(jìn)程
Browser進(jìn)程接收到結(jié)果并將結(jié)果繪制出來
這里繪一張簡單的圖:(很簡化)
看完這一整套流程花椭,應(yīng)該對瀏覽器的運作有了一定理解了,這樣有了知識架構(gòu)的基礎(chǔ)后房午,后續(xù)就方便往上填充內(nèi)容矿辽。
這塊再往深處講的話就涉及到瀏覽器內(nèi)核源碼解析了,不屬于本文范圍郭厌。
如果這一塊要深挖袋倔,建議去讀一些瀏覽器內(nèi)核源碼解析文章,或者可以先看看參考下來源中的第一篇文章折柠,寫的不錯
梳理瀏覽器內(nèi)核中線程之間的關(guān)系
到了這里宾娜,已經(jīng)對瀏覽器的運行有了一個整體的概念,接下來扇售,先簡單梳理一些概念
GUI渲染線程與JS引擎線程互斥
由于JavaScript是可操縱DOM的前塔,如果在修改這些元素屬性同時渲染界面(即JS線程和UI線程同時運行)贾陷,那么渲染線程前后獲得的元素數(shù)據(jù)就可能不一致了。
因此為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果嘱根,瀏覽器設(shè)置GUI渲染線程與JS引擎為互斥的關(guān)系髓废,當(dāng)JS引擎執(zhí)行時GUI線程會被掛起,
GUI更新則會被保存在一個隊列中等到JS引擎線程空閑時立即被執(zhí)行该抒。
JS阻塞頁面加載
從上述的互斥關(guān)系慌洪,可以推導(dǎo)出,JS如果執(zhí)行時間過長就會阻塞頁面凑保。
譬如冈爹,假設(shè)JS引擎正在進(jìn)行巨量的計算,此時就算GUI有更新欧引,也會被保存到隊列中频伤,等待JS引擎空閑后執(zhí)行。
然后芝此,由于巨量計算憋肖,所以JS引擎很可能很久很久后才能空閑,自然會感覺到巨卡無比婚苹。
所以岸更,要盡量避免JS執(zhí)行時間過長,這樣就會造成頁面的渲染不連貫膊升,導(dǎo)致頁面渲染加載阻塞的感覺怎炊。
WebWorker,JS的多線程廓译?
前文中有提到JS引擎是單線程的评肆,而且JS執(zhí)行時間過長會阻塞頁面,那么JS就真的對cpu密集型計算無能為力么非区?
所以瓜挽,后來HTML5中支持了Web Worker
。
MDN的官方解釋是:
Web Worker為Web內(nèi)容在后臺線程中運行腳本提供了一種簡單的方法院仿。線程可以執(zhí)行任務(wù)而不干擾用戶界面
一個worker是使用一個構(gòu)造函數(shù)創(chuàng)建的一個對象(e.g. Worker()) 運行一個命名的JavaScript文件
這個文件包含將在工作線程中運行的代碼; workers 運行在另一個全局上下文中,不同于當(dāng)前的window
因此秸抚,使用 window快捷方式獲取當(dāng)前全局的范圍 (而不是self) 在一個 Worker 內(nèi)將返回錯誤
這樣理解下:
- 創(chuàng)建Worker時,JS引擎向瀏覽器申請開一個子線程(子線程是瀏覽器開的歹垫,完全受主線程控制剥汤,而且不能操作DOM)
- JS引擎線程與worker線程間通過特定的方式通信(postMessage API,需要通過序列化對象來與線程交互特定的數(shù)據(jù))
所以排惨,如果有非常耗時的工作吭敢,請單獨開一個Worker線程,這樣里面不管如何翻天覆地都不會影響JS引擎主線程暮芭,
只待計算出結(jié)果后鹿驼,將結(jié)果通信給主線程即可欲低,perfect!
而且注意下,JS引擎是單線程的畜晰,這一點的本質(zhì)仍然未改變砾莱,Worker可以理解是瀏覽器給JS引擎開的外掛,專門用來解決那些大量計算問題凄鼻。
其它腊瑟,關(guān)于Worker的詳解就不是本文的范疇了,因此不再贅述块蚌。
WebWorker與SharedWorker
既然都到了這里闰非,就再提一下SharedWorker
(避免后續(xù)將這兩個概念搞混)
-
WebWorker只屬于某個頁面,不會和其他頁面的Render進(jìn)程(瀏覽器內(nèi)核進(jìn)程)共享
- 所以Chrome在Render進(jìn)程中(每一個Tab頁就是一個render進(jìn)程)創(chuàng)建一個新的線程來運行Worker中的JavaScript程序峭范。
-
SharedWorker是瀏覽器所有頁面共享的财松,不能采用與Worker同樣的方式實現(xiàn),因為它不隸屬于某個Render進(jìn)程纱控,可以為多個Render進(jìn)程共享使用
- 所以Chrome瀏覽器為SharedWorker單獨創(chuàng)建一個進(jìn)程來運行JavaScript程序辆毡,在瀏覽器中每個相同的JavaScript只存在一個SharedWorker進(jìn)程,不管它被創(chuàng)建多少次其徙。
看到這里胚迫,應(yīng)該就很容易明白了喷户,本質(zhì)上就是進(jìn)程和線程的區(qū)別唾那。SharedWorker由獨立的進(jìn)程管理,WebWorker只是屬于render進(jìn)程下的一個線程
簡單梳理下瀏覽器渲染流程
本來是直接計劃開始談JS運行機制的褪尝,但想了想闹获,既然上述都一直在談瀏覽器,直接跳到JS可能再突兀河哑,因此避诽,中間再補充下瀏覽器的渲染流程(簡單版本)
為了簡化理解,前期工作直接省略成:(要展開的或完全可以寫另一篇超長文)
- 瀏覽器輸入url璃谨,瀏覽器主進(jìn)程接管沙庐,開一個下載線程,
然后進(jìn)行 http請求(略去DNS查詢佳吞,IP尋址等等操作)拱雏,然后等待響應(yīng),獲取內(nèi)容底扳,
隨后將內(nèi)容通過RendererHost接口轉(zhuǎn)交給Renderer進(jìn)程
- 瀏覽器渲染流程開始
瀏覽器器內(nèi)核拿到內(nèi)容后铸抑,渲染大概可以劃分成以下幾個步驟:
- 解析html建立dom樹
- 解析css構(gòu)建render樹(將CSS代碼解析成樹形的數(shù)據(jù)結(jié)構(gòu),然后結(jié)合DOM合并成render樹)
- 布局render樹(Layout/reflow)衷模,負(fù)責(zé)各元素尺寸、位置的計算
- 繪制render樹(paint),繪制頁面像素信息
- 瀏覽器會將各層的信息發(fā)送給GPU怖亭,GPU會將各層合成(composite)挠羔,顯示在屏幕上。
所有詳細(xì)步驟都已經(jīng)略去售碳,渲染完畢后就是load
事件了,之后就是自己的JS邏輯處理了
既然略去了一些詳細(xì)的步驟,那么就提一些可能需要注意的細(xì)節(jié)把氏涩。
這里重繪參考來源中的一張圖:(參考來源第一篇)
load事件與DOMContentLoaded事件的先后
上面提到,渲染完畢后會觸發(fā)load
事件有梆,那么你能分清楚load
事件與DOMContentLoaded
事件的先后么是尖?
很簡單,知道它們的定義就可以了:
- 當(dāng) DOMContentLoaded 事件觸發(fā)時泥耀,僅當(dāng)DOM加載完成饺汹,不包括樣式表,圖片痰催。
(譬如如果有async加載的腳本就不一定完成)
- 當(dāng) onload 事件觸發(fā)時兜辞,頁面上所有的DOM,樣式表夸溶,腳本逸吵,圖片都已經(jīng)加載完成了。
(渲染完畢了)
所以缝裁,順序是:DOMContentLoaded -> load
css加載是否會阻塞dom樹渲染扫皱?
這里說的是頭部引入css的情況
首先,我們都知道:css是由單獨的下載線程異步下載的捷绑。
然后再說下幾個現(xiàn)象:
- css加載不會阻塞DOM樹解析(異步加載時DOM照常構(gòu)建)
- 但會阻塞render樹渲染(渲染時需等css加載完畢韩脑,因為render樹需要css信息)
這可能也是瀏覽器的一種優(yōu)化機制。
因為你加載css的時候粹污,可能會修改下面DOM節(jié)點的樣式段多,
如果css加載不阻塞render樹渲染的話,那么當(dāng)css加載完之后壮吩,
render樹可能又得重新重繪或者回流了进苍,這就造成了一些沒有必要的損耗。
所以干脆就先把DOM樹的結(jié)構(gòu)先解析完鸭叙,把可以做的工作做完觉啊,然后等你css加載完之后,
在根據(jù)最終的樣式來渲染render樹递雀,這種做法性能方面確實會比較好一點柄延。
普通圖層和復(fù)合圖層
渲染步驟中就提到了composite
概念。
可以簡單的這樣理解,瀏覽器渲染的圖層一般包含兩大類:普通圖層
以及復(fù)合圖層
首先搜吧,普通文檔流內(nèi)可以理解為一個復(fù)合圖層(這里稱為默認(rèn)復(fù)合層
市俊,里面不管添加多少元素,其實都是在同一個復(fù)合圖層中)
其次滤奈,absolute布局(fixed也一樣)摆昧,雖然可以脫離普通文檔流,但它仍然屬于默認(rèn)復(fù)合層
蜒程。
然后绅你,可以通過硬件加速
的方式,聲明一個新的復(fù)合圖層
昭躺,它會單獨分配資源
(當(dāng)然也會脫離普通文檔流忌锯,這樣一來,不管這個復(fù)合圖層中怎么變化领炫,也不會影響默認(rèn)復(fù)合層
里的回流重繪)
可以簡單理解下:GPU中偶垮,各個復(fù)合圖層是單獨繪制的,所以互不影響帝洪,這也是為什么某些場景硬件加速效果一級棒
可以Chrome源碼調(diào)試 -> More Tools -> Rendering -> Layer borders
中看到似舵,黃色的就是復(fù)合圖層信息
如下圖〈邢浚可以驗證上述的說法
如何變成復(fù)合圖層(硬件加速)
將該元素變成一個復(fù)合圖層砚哗,就是傳說中的硬件加速技術(shù)
- 最常用的方式:
translate3d
、translateZ
-
opacity
屬性/過渡動畫(需要動畫執(zhí)行的過程中才會創(chuàng)建合成層砰奕,動畫沒有開始或結(jié)束后元素還會回到之前的狀態(tài)) -
will-chang
屬性(這個比較偏僻)蛛芥,一般配合opacity與translate使用(而且經(jīng)測試,除了上述可以引發(fā)硬件加速的屬性外脆淹,其它屬性并不會變成復(fù)合層)常空,
作用是提前告訴瀏覽器要變化,這樣瀏覽器會開始做一些優(yōu)化工作(這個最好用完后就釋放)
-
<video><iframe><canvas><webgl>
等元素 - 其它盖溺,譬如以前的flash插件
absolute和硬件加速的區(qū)別
可以看到,absolute雖然可以脫離普通文檔流铣缠,但是無法脫離默認(rèn)復(fù)合層烘嘱。
所以,就算absolute中信息改變時不會改變普通文檔流中render樹蝗蛙,
但是蝇庭,瀏覽器最終繪制時,是整個復(fù)合層繪制的捡硅,所以absolute中信息的改變哮内,仍然會影響整個復(fù)合層的繪制。
(瀏覽器會重繪它,如果復(fù)合層中內(nèi)容多北发,absolute帶來的繪制信息變化過大纹因,資源消耗是非常嚴(yán)重的)
而硬件加速直接就是在另一個復(fù)合層了(另起爐灶),所以它的信息改變不會影響默認(rèn)復(fù)合層
(當(dāng)然了琳拨,內(nèi)部肯定會影響屬于自己的復(fù)合層)瞭恰,僅僅是引發(fā)最后的合成(輸出視圖)
復(fù)合圖層的作用?
一般一個元素開啟硬件加速后會變成復(fù)合圖層狱庇,可以獨立于普通文檔流中惊畏,改動后可以避免整個頁面重繪,提升性能
但是盡量不要大量使用復(fù)合圖層密任,否則由于資源消耗過度颜启,頁面反而會變的更卡
硬件加速時請使用index
使用硬件加速時,盡可能的使用index浪讳,防止瀏覽器默認(rèn)給后續(xù)的元素創(chuàng)建復(fù)合層渲染
具體的原理時這樣的:
webkit CSS3中农曲,如果這個元素添加了硬件加速,并且index層級比較低驻债,
那么在這個元素的后面其它元素(層級比這個元素高的乳规,或者相同的,并且releative或absolute屬性相同的)合呐,
會默認(rèn)變?yōu)閺?fù)合層渲染暮的,如果處理不當(dāng)會極大的影響性能
簡單點理解,其實可以認(rèn)為是一個隱式合成的概念:如果a是一個復(fù)合圖層淌实,而且b在a上面冻辩,那么b也會被隱式轉(zhuǎn)為一個復(fù)合圖層,這點需要特別注意
另外拆祈,這個問題可以在這個地址看到重現(xiàn)(原作者分析的挺到位的恨闪,直接上鏈接):
從Event Loop談JS的運行機制
到此時,已經(jīng)是屬于瀏覽器頁面初次渲染完畢后的事情放坏,JS引擎的一些運行機制分析咙咽。
注意,這里不談可執(zhí)行上下文
淤年,VO
钧敞,scop chain
等概念(這些完全可以整理成另一篇文章了),這里主要是結(jié)合Event Loop
來談JS代碼是如何執(zhí)行的麸粮。
讀這部分的前提是已經(jīng)知道了JS引擎是單線程溉苛,而且這里會用到上文中的幾個概念:(如果不是很理解,可以回頭溫習(xí))
- JS引擎線程
- 事件觸發(fā)線程
- 定時觸發(fā)器線程
然后再理解一個概念:
- JS分為同步任務(wù)和異步任務(wù)
- 同步任務(wù)都在主線程上執(zhí)行弄诲,形成一個
執(zhí)行棧
- 主線程之外愚战,事件觸發(fā)線程管理著一個
任務(wù)隊列
,只要異步任務(wù)有了運行結(jié)果,就在任務(wù)隊列
之中放置一個事件寂玲。 - 一旦
執(zhí)行棧
中的所有同步任務(wù)執(zhí)行完畢(此時JS引擎空閑)塔插,系統(tǒng)就會讀取任務(wù)隊列
,將可運行的異步任務(wù)添加到可執(zhí)行棧中敢茁,開始執(zhí)行佑淀。
看圖:
看到這里,應(yīng)該就可以理解了:為什么有時候setTimeout推入的事件不能準(zhǔn)時執(zhí)行彰檬?因為可能在它推入到事件列表時伸刃,主線程還不空閑,正在執(zhí)行其它代碼逢倍,
所以自然有誤差捧颅。
事件循環(huán)機制進(jìn)一步補充
這里就直接引用一張圖片來協(xié)助理解:(參考自Philip Roberts的演講《Help, I'm stuck in an event-loop》)
上圖大致描述就是:
- 主線程運行時會產(chǎn)生執(zhí)行棧,
棧中的代碼調(diào)用某些api時较雕,它們會在事件隊列中添加各種事件(當(dāng)滿足觸發(fā)條件后碉哑,如ajax請求完畢)
- 而棧中的代碼執(zhí)行完畢,就會讀取事件隊列中的事件亮蒋,去執(zhí)行那些回調(diào)
- 如此循環(huán)
- 注意扣典,總是要等待棧中的代碼執(zhí)行完畢后才會去讀取事件隊列中的事件
單獨說說定時器
上述事件循環(huán)機制的核心是:JS引擎線程和事件觸發(fā)線程
但事件上,里面還有一些隱藏細(xì)節(jié)慎玖,譬如調(diào)用setTimeout
后贮尖,是如何等待特定時間后才添加到事件隊列中的?
是JS引擎檢測的么趁怔?當(dāng)然不是了湿硝。它是由定時器線程控制(因為JS引擎自己都忙不過來,根本無暇分身)
為什么要單獨的定時器線程润努?因為JavaScript引擎是單線程的, 如果處于阻塞線程狀態(tài)就會影響記計時的準(zhǔn)確关斜,因此很有必要單獨開一個線程用來計時。
什么時候會用到定時器線程铺浇?當(dāng)使用setTimeout
或setInterval
時痢畜,它需要定時器線程計時,計時完成后就會將特定的事件推入事件隊列中随抠。
譬如:
setTimeout(function(){
console.log('hello!');
}, 1000);
這段代碼的作用是當(dāng)1000
毫秒計時完畢后(由定時器線程計時)裁着,將回調(diào)函數(shù)推入事件隊列中,等待主線程執(zhí)行
setTimeout(function(){
console.log('hello!');
}, 0);
console.log('begin');
這段代碼的效果是最快的時間內(nèi)將回調(diào)函數(shù)推入事件隊列中拱她,等待主線程執(zhí)行
注意:
- 執(zhí)行結(jié)果是:先
begin
后hello!
- 雖然代碼的本意是0毫秒后就推入事件隊列,但是W3C在HTML標(biāo)準(zhǔn)中規(guī)定扔罪,規(guī)定要求setTimeout中低于4ms的時間間隔算為4ms秉沼。
(不過也有一說是不同瀏覽器有不同的最小時間設(shè)定)
- 就算不等待4ms,就算假設(shè)0毫秒就推入事件隊列,也會先執(zhí)行
begin
(因為只有可執(zhí)行棧內(nèi)空了后才會主動讀取事件隊列)
setTimeout而不是setInterval
用setTimeout模擬定期計時和直接用setInterval是有區(qū)別的唬复。
因為每次setTimeout計時到后就會去執(zhí)行矗积,然后執(zhí)行一段時間后才會繼續(xù)setTimeout,中間就多了誤差
(誤差多少與代碼執(zhí)行時間有關(guān))
而setInterval則是每次都精確的隔一段時間推入一個事件
(但是敞咧,事件的實際執(zhí)行時間不一定就準(zhǔn)確棘捣,還有可能是這個事件還沒執(zhí)行完畢,下一個事件就來了)
而且setInterval有一些比較致命的問題就是:
- 累計效應(yīng)(上面提到的)休建,如果setInterval代碼在(setInterval)再次添加到隊列之前還沒有完成執(zhí)行乍恐,
就會導(dǎo)致定時器代碼連續(xù)運行好幾次,而之間沒有間隔测砂。
就算正常間隔執(zhí)行茵烈,多個setInterval的代碼執(zhí)行時間可能會比預(yù)期小(因為代碼執(zhí)行需要一定時間)
-
譬如像iOS的webview,或者Safari等瀏覽器中都有一個特點砌些,在滾動的時候是不執(zhí)行JS的呜投,如果使用了setInterval,會發(fā)現(xiàn)在滾動結(jié)束后會執(zhí)行多次由于滾動不執(zhí)行JS積攢回調(diào)存璃,如果回調(diào)執(zhí)行時間過長,就會非常容器造成卡頓問題和一些不可知的錯誤(這一塊后續(xù)有補充仑荐,setInterval自帶的優(yōu)化,不會重復(fù)添加回調(diào)) - 而且把瀏覽器最小化顯示等操作時纵东,setInterval并不是不執(zhí)行程序粘招,
它會把setInterval的回調(diào)函數(shù)放在隊列中,等瀏覽器窗口再次打開時篮迎,一瞬間全部執(zhí)行時
所以男图,鑒于這么多但問題,目前一般認(rèn)為的最佳方案是:用setTimeout模擬setInterval甜橱,或者特殊場合直接用requestAnimationFrame
補充:JS高程中有提到逊笆,JS引擎會對setInterval進(jìn)行優(yōu)化,如果當(dāng)前事件隊列中有setInterval的回調(diào)岂傲,不會重復(fù)添加难裆。不過,仍然是有很多問題镊掖。乃戈。。
事件循環(huán)進(jìn)階:macrotask與microtask
這段參考了參考來源中的第2篇文章(英文版的)亩进,(加了下自己的理解重新描述了下)症虑,
強烈推薦有英文基礎(chǔ)的同學(xué)直接觀看原文,作者描述的很清晰归薛,示例也很不錯谍憔,如下:
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
上文中將JS事件循環(huán)機制梳理了一遍匪蝙,在ES5的情況是夠用了,但是在ES6盛行的現(xiàn)在习贫,仍然會遇到一些問題逛球,譬如下面這題:
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');
嗯哼,它的正確執(zhí)行順序是這樣子的:
script start
script end
promise1
promise2
setTimeout
為什么呢苫昌?因為Promise里有了一個一個新的概念:microtask
或者颤绕,進(jìn)一步,JS中分為兩種任務(wù)類型:macrotask
和microtask
祟身,在ECMAScript中奥务,microtask稱為jobs
,macrotask可稱為task
它們的定義月而?區(qū)別汗洒?簡單點可以按如下理解:
-
macrotask(又稱之為宏任務(wù)),可以理解是每次執(zhí)行棧執(zhí)行的代碼就是一個宏任務(wù)(包括每次從事件隊列中獲取一個事件回調(diào)并放到執(zhí)行棧中執(zhí)行)
- 每一個task會從頭到尾將這個任務(wù)執(zhí)行完畢父款,不會執(zhí)行其它
- 瀏覽器為了能夠使得JS內(nèi)部task與DOM任務(wù)能夠有序的執(zhí)行溢谤,會在一個task執(zhí)行結(jié)束后,在下一個 task 執(zhí)行開始前憨攒,對頁面進(jìn)行重新渲染
(`task->渲染->task->...`)
-
microtask(又稱為微任務(wù))世杀,可以理解是在當(dāng)前 task 執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)
- 也就是說,在當(dāng)前task任務(wù)后肝集,下一個task之前瞻坝,在渲染之前
- 所以它的響應(yīng)速度相比setTimeout(setTimeout是task)會更快,因為無需等渲染
- 也就是說杏瞻,在某一個macrotask執(zhí)行完后所刀,就會將在它執(zhí)行期間產(chǎn)生的所有microtask都執(zhí)行完畢(在渲染前)
分別很么樣的場景會形成macrotask和microtask呢?
- macrotask:主代碼塊捞挥,setTimeout浮创,setInterval等(可以看到,事件隊列中的每一個事件都是一個macrotask)
- microtask:Promise砌函,process.nextTick等
補充:在node環(huán)境下斩披,process.nextTick的優(yōu)先級高于Promise,也就是可以簡單理解為:在宏任務(wù)結(jié)束后會先執(zhí)行微任務(wù)隊列中的nextTickQueue部分讹俊,然后才會執(zhí)行微任務(wù)中的Promise部分垦沉。
參考:https://segmentfault.com/q/1010000011914016
再根據(jù)線程來理解下:
- macrotask中的事件都是放在一個事件隊列中的,而這個隊列由事件觸發(fā)線程維護(hù)
- microtask中的所有微任務(wù)都是添加到微任務(wù)隊列(Job Queues)中仍劈,等待當(dāng)前macrotask執(zhí)行完畢后執(zhí)行厕倍,而這個隊列由JS引擎線程維護(hù)
(這點由自己理解+推測得出,因為它是在主線程下無縫執(zhí)行的)
所以贩疙,總結(jié)下運行機制:
- 執(zhí)行一個宏任務(wù)(棧中沒有就從事件隊列中獲劝笄唷)
- 執(zhí)行過程中如果遇到微任務(wù)诬像,就將它添加到微任務(wù)的任務(wù)隊列中
- 宏任務(wù)執(zhí)行完畢后屋群,立即執(zhí)行當(dāng)前微任務(wù)隊列中的所有微任務(wù)(依次執(zhí)行)
- 當(dāng)前宏任務(wù)執(zhí)行完畢闸婴,開始檢查渲染,然后GUI線程接管渲染
- 渲染完畢后芍躏,JS線程繼續(xù)接管邪乍,開始下一個宏任務(wù)(從事件隊列中獲取)
如圖:
另外对竣,請注意下Promise
的polyfill
與官方版本的區(qū)別:
- 官方版本中庇楞,是標(biāo)準(zhǔn)的microtask形式
- polyfill,一般都是通過setTimeout模擬的否纬,所以是macrotask形式
- 請?zhí)貏e注意這兩點區(qū)別
注意吕晌,有一些瀏覽器執(zhí)行結(jié)果不一樣(因為它們可能把microtask當(dāng)成macrotask來執(zhí)行了),
但是為了簡單临燃,這里不描述一些不標(biāo)準(zhǔn)的瀏覽器下的場景(但記住睛驳,有些瀏覽器可能并不標(biāo)準(zhǔn))
20180126補充:使用MutationObserver實現(xiàn)microtask
MutationObserver可以用來實現(xiàn)microtask
(它屬于microtask,優(yōu)先級小于Promise膜廊,
一般是Promise不支持時才會這樣做)
它是HTML5中的新特性乏沸,作用是:監(jiān)聽一個DOM變動,
當(dāng)DOM對象樹發(fā)生任何變動時爪瓜,Mutation Observer會得到通知
像以前的Vue源碼中就是利用它來模擬nextTick的蹬跃,
具體原理是,創(chuàng)建一個TextNode并監(jiān)聽內(nèi)容變化铆铆,
然后要nextTick的時候去改一下這個節(jié)點的文本內(nèi)容蝶缀,
如下:(Vue的源碼,未修改)
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
不過薄货,現(xiàn)在的Vue(2.5+)的nextTick實現(xiàn)移除了MutationObserver的方式(據(jù)說是兼容性原因)翁都,
取而代之的是使用MessageChannel
(當(dāng)然,默認(rèn)情況仍然是Promise菲驴,不支持才兼容的)荐吵。
MessageChannel屬于宏任務(wù),優(yōu)先級是:setImmediate->MessageChannel->setTimeout
赊瞬,
所以Vue(2.5+)內(nèi)部的nextTick與2.4及之前的實現(xiàn)是不一樣的先煎,需要注意下。
這里不展開巧涧,可以看下https://juejin.im/post/5a1af88f5188254a701ec230
寫在最后的話
看到這里薯蝎,不知道對JS的運行機制是不是更加理解了,從頭到尾梳理谤绳,而不是就某一個碎片化知識應(yīng)該是會更清晰的吧占锯?
同時袒哥,也應(yīng)該注意到了JS根本就沒有想象的那么簡單,前端的知識也是無窮無盡消略,層出不窮的概念堡称、N多易忘的知識點、各式各樣的框架艺演、
底層原理方面也是可以無限的往下深挖却紧,然后你就會發(fā)現(xiàn),你知道的太少了胎撤。晓殊。。
另外伤提,本文也打算先告一段落巫俺,其它的,如JS詞法解析肿男,可執(zhí)行上下文以及VO等概念就不繼續(xù)在本文中寫了介汹,后續(xù)可以考慮另開新的文章。
最后次伶,喜歡的話痴昧,就請給個贊吧!
附錄
原文地址:
https://segmentfault.com/a/1190000012925872
參考資料
- https://www.cnblogs.com/lhb25/p/how-browsers-work.html
- https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
- https://segmentfault.com/p/1210000012780980
- http://blog.csdn.net/u013510838/article/details/55211033
- http://blog.csdn.net/Steward2011/article/details/51319298
- http://www.imweb.io/topic/58e3bfa845e5c13468f567d5
- https://segmentfault.com/a/1190000008015671
- https://juejin.im/post/5a4ed917f265da3e317df515
- http://www.cnblogs.com/iovec/p/7904416.html
- https://www.cnblogs.com/wyaocn/p/5761163.html
- http://www.ruanyifeng.com/blog/2014/10/event-loop.html#comment-text