摘要:瀏覽器的渲染進程是多線程的。異步請求線程在在連接后是通過瀏覽器新開一個線程請求將檢測到狀態(tài)變更時,如果設(shè)置有回調(diào)函數(shù),異步線程就產(chǎn)生狀態(tài)變更事件,將這個回調(diào)再放入事件隊列中。
[TOC]
瀏覽器進程線程 區(qū)分線程和進程**- 什么是進程** 狹義定義:進程是正在運行的程序的實例(an instance of a computer program that is being executed)。 廣義定義:進程是一個具有一定獨立功能的程序關(guān)于某個數(shù)據(jù)集合的一次運行活動。它是操作系統(tǒng)動態(tài)執(zhí)行的基本單元,在傳統(tǒng)的操作系統(tǒng)中,進程既是基本的分配單元,也是基本的執(zhí)行單元。 **- 什么是線程** 線程(英語:thread)是操作系統(tǒng)能夠進行運算調(diào)度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發(fā)多個線程,每條線程并行執(zhí)行不同的任務(wù)。
在當(dāng)代面向線程設(shè)計的計算機結(jié)構(gòu)中,進程是線程的容器。
一個進程有一個或多個線程,線程之間共同完成進程分配下來的任務(wù)。先看個形象的比喻:
- 進程是一個工廠,工廠有它的獨立資源 - 工廠之間相互獨立 - 線程是工廠中的工人,多個工人協(xié)作完成任務(wù) - 工廠內(nèi)有一個或多個工人 - 工人之間共享空間
再完善完善概念:
- 工廠的資源 -> 系統(tǒng)分配的內(nèi)存(獨立的一塊內(nèi)存) - 工廠之間的相互獨立 -> 進程之間相互獨立 - 多個工人協(xié)作完成任務(wù) -> 多個線程在進程中協(xié)作完成任務(wù) - 工廠內(nèi)有一個或多個工人 -> 一個進程由一個或多個線程組成 - 工人之間共享空間 -> 同一進程下的各個線程之間共享程序的內(nèi)存空間(包括代碼段、數(shù)據(jù)集、堆等)
然后再鞏固下:
可以打開任務(wù)管理器,可以看到有一個后臺進程列表。這里就是查看進程的地方,而且可以看到每個進程的內(nèi)存資源信息以及cpu占有率。
進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位),線程是cpu調(diào)度的最小單位(線程是建立在進程的基礎(chǔ)上的一次程序運行單位)。
一般通用的說法:單線程與多線程,都是指在一個進程內(nèi)的單和多。(所以核心還是得屬于一個進程才行)
瀏覽器是多線程的理解了進程與線程了區(qū)別后,接下來對瀏覽器進行一定程度上的認識:(先看下簡化理解)
瀏覽器是多進程的
瀏覽器之所以能夠運行,是因為系統(tǒng)給它的進程分配了資源(cpu、內(nèi)存)
簡單點理解,每打開一個Tab頁,就相當(dāng)于創(chuàng)建了一個獨立的瀏覽器進程。
圖中打開了Chrome瀏覽器的多個標簽頁,然后可以在Chrome的任務(wù)管理器中看到有多個進程(分別是每一個Tab頁面有一個獨立的進程,以及一個主進程)。
注意:在這里瀏覽器應(yīng)該也有自己的優(yōu)化機制,有時候打開多個tab頁后,可以在Chrome任務(wù)管理器中看到,有些進程被合并了(譬如打開多個空白標簽頁后,會發(fā)現(xiàn)多個空白標簽頁被合并成了一個進程),所以每一個Tab標簽對應(yīng)一個進程并不一定是絕對的。
瀏覽器都包含哪些進程?除了瀏覽器的標簽頁進程之外,瀏覽器還有一些其他進程來輔助支撐標簽頁的進程,主要包含哪些進程:(為了簡化理解,僅列舉主要進程)
Browser進程:瀏覽器的主進程(負責(zé)協(xié)調(diào)、主控),只有一個。作用有
負責(zé)瀏覽器界面顯示,與用戶交互。如前進,后退等
負責(zé)各個頁面的管理,創(chuàng)建和銷毀其他進程
將Renderer進程得到的內(nèi)存中的Bitmap,繪制到用戶界面上
網(wǎng)絡(luò)資源的管理,下載等
第三方插件進程:每種類型的插件對應(yīng)一個進程,僅當(dāng)使用該插件時才創(chuàng)建
GPU進程:最多一個,用于3D繪制等
瀏覽器渲染進程(瀏覽器內(nèi)核)(Renderer進程,內(nèi)部是多線程的):默認每個Tab頁面一個進程,互不影響。主要作用為
頁面渲染,腳本執(zhí)行,事件處理等
強化記憶:在瀏覽器中打開一個網(wǎng)頁相當(dāng)于新起了一個進程(進程內(nèi)有自己的多線程)
瀏覽器多進程的優(yōu)勢相比于單進程瀏覽器,多進程有如下優(yōu)點:
避免單個page crash影響整個瀏覽器
避免第三方插件crash影響整個瀏覽器
多進程充分利用多核優(yōu)勢
方便使用沙盒模型隔離插件等進程,提高瀏覽器穩(wěn)定性
簡單點理解:如果瀏覽器是單進程,那么某個Tab頁崩潰了,就影響了整個瀏覽器,體驗有多差;同理如果是單進程,插件崩潰了也會影響整個瀏覽器;
重點是瀏覽器內(nèi)核(渲染進程)瀏覽器內(nèi)核,即我們的渲染進程,有名Renderer進程,我們頁面的渲染,js的執(zhí)行,事件的循環(huán)都在這一進程內(nèi)進行,也就是說,該進程下面擁有著多個線程,靠著這些現(xiàn)成共同完成渲染任務(wù)。
對于普通的前端操作來說,頁面的渲染,JS的執(zhí)行,事件的循環(huán),都在這個進程內(nèi)進行。瀏覽器的渲染進程是多線程的。
渲染進程包含哪些主要的線程?1.GUI渲染線程【圖形用戶界面(Graphical User Interface,簡稱 GUI,又稱圖形用戶接口)】
負責(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í)行。
2.JS引擎線程
也稱為JS內(nèi)核,負責(zé)處理Javascript腳本程序。(例如V8引擎)
JS引擎線程負責(zé)解析Javascript腳本,運行代碼。
JS引擎一直等待著任務(wù)隊列中任務(wù)的到來,然后加以處理,一個Tab頁(renderer進程)中無論什么時候都只有一個JS線程在運行JS程序
同樣注意,GUI渲染線程與JS引擎線程是互斥的,所以如果JS執(zhí)行的時間過長,這樣就會造成頁面的渲染不連貫,導(dǎo)致頁面渲染加載阻塞。
3.事件觸發(fā)線程
歸屬于瀏覽器而不是JS引擎,用來控制事件循環(huán)(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開線程協(xié)助)
當(dāng)JS引擎執(zhí)行代碼塊如setTimeOut時(也可來自瀏覽器內(nèi)核的其他線程,如鼠標點擊、AJAX異步請求等),會將對應(yīng)任務(wù)添加到事件線程中
當(dāng)對應(yīng)的事件符合觸發(fā)條件被觸發(fā)時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理(事件循環(huán) Event Loop)
注意,由于JS的單線程關(guān)系,所以這些待處理隊列中的事件都得排隊等待JS引擎處理(當(dāng)JS引擎空閑時才會去執(zhí)行)
4.定時觸發(fā)器線程
傳說中的setInterval與setTimeout所在線程
瀏覽器定時計數(shù)器并不是由JavaScript引擎計數(shù)的,(因為JavaScript引擎是單線程的, 如果處于阻塞線程狀態(tài)就會影響記計時的準確)
因此通過多帶帶線程來計時并觸發(fā)定時【計時完畢后,添加到事件隊列中,等待JS引擎空閑后執(zhí)行,這也是“JavaScript定時器不準確”的原因(可用requestAnimationFrame)】
注意,W3C在HTML標準中規(guī)定,規(guī)定要求setTimeout中低于4ms的時間間隔算為4ms。
5.異步http請求線程
在XMLHttpRequest在連接后是通過瀏覽器新開一個線程請求
將檢測到狀態(tài)變更時,如果設(shè)置有回調(diào)函數(shù),異步線程就產(chǎn)生狀態(tài)變更事件,將這個回調(diào)再放入事件隊列中。再由JavaScript引擎執(zhí)行。
為什么JS引擎是單線程的?為什么需要異步? 單線程又是如何實現(xiàn)異步的呢? 查看【鏈接描述】
Browser進程和瀏覽器內(nèi)核(Renderer進程)的通信過程如果自己打開任務(wù)管理器,然后打開一個瀏覽器,就可以看到:任務(wù)管理器中出現(xiàn)了兩個進程(一個是主控進程,一個則是打開Tab頁的渲染進程),
然后在這前提下,看下整個的過程:(簡化了很多)
Browser進程收到用戶請求,首先需要獲取頁面內(nèi)容(譬如通過網(wǎng)絡(luò)下載資源),隨后將該任務(wù)通過RendererHost接口傳遞給Render進程
Renderer進程的Renderer接口收到消息,簡單解釋后,交給渲染線程,然后開始渲染
渲染線程接收請求,加載網(wǎng)頁并渲染網(wǎng)頁,這其中可能需要Browser進程獲取資源和需要GPU進程來幫助渲染
當(dāng)然可能會有JS線程操作DOM(這樣可能會造成回流并重繪)
最后Render進程將結(jié)果傳遞給Browser進程
Browser進程接收到結(jié)果并將結(jié)果繪制出來
這里繪一張簡單的圖:(很簡化)
為什么JS引擎是單線程的JavaScript作為一門客戶端的腳本語言,主要的任務(wù)是處理用戶的交互,而用戶的交互無非就是響應(yīng)DOM的增刪改,使用事件隊列的形式,一次事件循環(huán)只處理一個事件響應(yīng),使得腳本執(zhí)行相對連續(xù)。如果JS引擎被設(shè)計為多線程的,那么DOM之間必然會存在資源競爭,那么語言的實現(xiàn)會變得非常臃腫,在客戶端跑起來,資源的消耗和性能將會是不太樂觀的,故設(shè)計為單線程的形式,并附加一些其他的線程來實現(xiàn)異步的形式,這樣運行成本相對于使用JS多線程來說降低了很多。
梳理瀏覽器內(nèi)核中線程之間的關(guān)系 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í)行。
從上述的互斥關(guān)系,可以推導(dǎo)出,JS如果執(zhí)行時間過長就會阻塞頁面。
譬如,假設(shè)JS引擎正在進行巨量的計算,此時就算GUI有更新,也會被保存到隊列中,等待JS引擎空閑后執(zhí)行。
然后,由于巨量計算,所以JS引擎很可能很久很久后才能空閑,自然會感覺到巨卡無比。
所以,要盡量避免JS執(zhí)行時間過長,這樣就會造成頁面的渲染不連貫,導(dǎo)致頁面渲染加載阻塞的感覺。
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ā)線程、定時觸發(fā)器線程、異步HTTP請求線程三個線程有一個共同點,那就是使用回調(diào)函數(shù)的形式,當(dāng)滿足了特定的條件,這些回調(diào)函數(shù)會被執(zhí)行。這些回調(diào)函數(shù)被瀏覽器內(nèi)核理解成事件,在瀏覽器內(nèi)核中擁有一個事件隊列,這三個線程當(dāng)滿足了內(nèi)部特定的條件,會將這些回調(diào)函數(shù)添加到事件隊列中,等待JS引擎空閑執(zhí)行。例如異步HTTP請求線程,線程如果檢測到請求的狀態(tài)變更,如果設(shè)置有回調(diào)函數(shù),回調(diào)函數(shù)會被添加事件隊列中,等待JS引擎空閑了執(zhí)行。
但是,JS引擎對事件隊列(宏任務(wù))與JS引擎內(nèi)的任務(wù)(微任務(wù))執(zhí)行存在著先后循序,當(dāng)每執(zhí)行完一個事件隊列的時間,JS引擎會檢測內(nèi)部是否有未執(zhí)行的任務(wù),如果有,將會優(yōu)先執(zhí)行(微任務(wù))。
因為JS引擎是單線程的,當(dāng)JS執(zhí)行時間過長會頁面阻塞,那么JS就真的對CPU密集型計算無能為力么?
所以,后來HTML5中支持了 Web Worker。
來自MDN的官方解釋
Web Workers 使得一個Web應(yīng)用程序可以在與主執(zhí)行線程分離的后臺線程中運行一個腳本操作。這樣做的好處是可以在一個多帶帶的線程中執(zhí)行費時的處理任務(wù),從而允許主(通常是UI)線程運行而不被阻塞/放慢。
這樣理解下:
創(chuàng)建Worker時,JS引擎向瀏覽器申請開一個子線程(子線程是瀏覽器開的,完全受主線程控制,而且不能操作DOM)
JS引擎線程與worker線程間通過特定的方式通信(postMessage API,需要通過序列化對象來與線程交互特定的數(shù)據(jù))
所以,如果有非常耗時的工作,請多帶帶開一個Worker線程,這樣里面不管如何翻天覆地都不會影響JS引擎主線程,只待計算出結(jié)果后,將結(jié)果通信給主線程即可,perfect!
而且注意下,JS引擎是單線程的,這一點的本質(zhì)仍然未改變,Worker可以理解是瀏覽器給JS引擎開的外掛,專門用來解決那些大量計算問題。
注意點:
WebWorker可以想瀏覽器申請一個子線程,該子線程服務(wù)于主線程,完全受主線程控制。
JS引擎線程與worker線程間通過特定的方式通信(postMessage API,需要通過序列化對象來與線程交互特定的數(shù)據(jù))
所以,如果需要進行一些高耗時的計算時,可以多帶帶開啟一個WebWorker線程,這樣不管這個WebWorker子線程怎么密集計算、怎么阻塞,都不會影響JS引擎主線程,只需要等計算結(jié)束,將結(jié)果通過postMessage傳輸給主線程就可以了。
另外,還有個東西叫 SharedWorker,與WebWorker在概念上所不同。
WebWorker 只屬于某一個頁面,不會和其他標簽頁的Renderer進程共享,WebWorker是屬于Renderer進程創(chuàng)建的進程。所以Chrome在Render進程中(每一個Tab頁就是一個render進程)創(chuàng)建一個新的線程來運行Worker中的JavaScript程序。
SharedWorker 是由瀏覽器多帶帶創(chuàng)建的進程來運行的JS程序,它被所有的Renderer進程所共享,在瀏覽器中,最多只能存在一個SharedWorker進程。
SharedWorker由進程管理,WebWorker是某一個Renderer進程下的線程。
看到這里,應(yīng)該就很容易明白了,本質(zhì)上就是進程和線程的區(qū)別。SharedWorker由獨立的進程管理,WebWorker只是屬于render進程下的一個線程
瀏覽器的渲染流程每個瀏覽器內(nèi)核的渲染流程不一樣,下面我們主要以webkit為主。
首先是渲染的前奏:
瀏覽器輸入url,瀏覽器主進程接管,開了一個下載線程
然后進行HTTP請求(DNS查詢、IP尋址等等),等待響應(yīng),開始下載響應(yīng)報文。
將下載完的內(nèi)容轉(zhuǎn)交給Renderer進程管理
開始渲染...
在說渲染之前,需要理解一些概念:
DOM Tree: 瀏覽器將HTML解析成樹形的數(shù)據(jù)結(jié)構(gòu)。
CSS Rule Tree:瀏覽器將CSS解析成樹形的數(shù)據(jù)結(jié)構(gòu)。
Render Tree:DOM樹和CSS規(guī)則樹合并后生產(chǎn)Render樹。
layout:有了Render Tree,瀏覽器已經(jīng)能知道網(wǎng)頁中有哪些節(jié)點、各個節(jié)點的CSS定義以及他們的從屬關(guān)系,從而去計算出每個節(jié)點在屏幕中的位置。
painting: 按照算出來的規(guī)則,通過顯卡,把內(nèi)容畫到屏幕上。
reflow(回流):當(dāng)瀏覽器發(fā)現(xiàn)某個部分發(fā)生了點變化影響了布局,需要倒回去重新渲染,內(nèi)行稱這個回退的過程叫 reflow。reflow 會從 這個 root frame 開始遞歸往下,依次計算所有的結(jié)點幾何尺寸和位置。reflow 幾乎是無法避免的?,F(xiàn)在界面上流行的一些效果,比如樹狀目錄的折疊、展開(實質(zhì)上是元素的顯 示與隱藏)等,都將引起瀏覽器的 reflow。鼠標滑過、點擊……只要這些行為引起了頁面上某些元素的占位面積、定位方式、邊距等屬性的變化,都會引起它內(nèi)部、周圍甚至整個頁面的重新渲 染。通常我們都無法預(yù)估瀏覽器到底會 reflow 哪一部分的代碼,它們都彼此相互影響著。
repaint(重繪):改變某個元素的背景色、文字顏色、邊框顏色等等不影響它周圍或內(nèi)部布局的屬性時,屏幕的一部分要重畫,但是元素的幾何尺寸沒有變。
注意:display:none的節(jié)點不會被加入Render Tree,而visibility: hidden則會,所以display:none會觸發(fā)reflow,visibility: hidden會觸發(fā)repaint。
瀏覽器內(nèi)核拿到響應(yīng)報文之后,渲染大概分為以下步驟
解析html生產(chǎn)DOM樹。
解析CSS規(guī)則。
根據(jù)DOM Tree和CSS Tree生成Render Tree。
根據(jù)Render樹進行l(wèi)ayout,負責(zé)各個元素節(jié)點的尺寸、位置計算。
繪制Render樹(painting),繪制頁面像素信息。
瀏覽器會將各層的信息發(fā)送給GPU,GPU會將各層合成(composite),顯示在屏幕上。
詳細步驟略去,大概步驟如下,渲染完畢后JS引擎開始執(zhí)行load事件,繪制流程見下圖。
由圖中可以看出,css在加載過程中不會影響到DOM樹的生成,但是會影響到Render樹的生成,進而影響到layout,所以一般來說,style的link標簽需要盡量放在head里面,因為在解析DOM樹的時候是自上而下的,而css樣式又是通過異步加載的,這樣的話,解析DOM樹下的body節(jié)點和加載css樣式能盡可能的并行,加快Render樹的生成的速度,當(dāng)然,如果css是通過js動態(tài)添加進來的,會引起頁面的重繪或重新布局。
從有html標準以來到目前為止(2017年5月),標準一直是規(guī)定style元素不應(yīng)出現(xiàn)在body元素中。
前面提到了load事件,那么與DOMContentLoaded事件有什么分別。
當(dāng) DOMContentLoaded 事件觸發(fā)時,僅當(dāng)DOM加載完成,不包括樣式表,圖片。 (譬如如果有async加載的腳本就不一定完成)
當(dāng) onLoad 事件觸發(fā)時,頁面上所有的DOM,樣式表,腳本,圖片都已經(jīng)加載完成了。 (渲染完畢了)
順序是:DOMContentLoaded -> load
普通圖層和復(fù)合圖層渲染步驟就提到了composite概念;瀏覽器渲染的圖層一般包含兩大類:普通圖層以及復(fù)合圖層。
普通文檔流內(nèi)可以理解為一個復(fù)合圖層(這里默認復(fù)合層,里面不管添加多少元素,其實都是在同個復(fù)合圖層中)
absolute布局(fixed也一樣),雖然可以脫離文檔流,但它仍然屬于默認復(fù)合層
可以通過硬件加速的方式,聲明一個新的復(fù)合圖層,它會多帶帶分配資源(當(dāng)然也會脫離普通文檔流,這樣一來,不管這個復(fù)合圖層中怎么變化,也不會影響默認復(fù)合層里的回流重繪)
可以簡單理解下:GPU中,各個復(fù)合圖層是多帶帶繪制的,所以互不影響,這也是為什么某些場景硬件加速效果一級棒
如何變成復(fù)合圖層(硬件加速)
將元素變成一個復(fù)合圖層,就是傳說中的硬件加速技術(shù)
最常用的方式:translate3d,translatez
opacity屬性/過渡動畫(需要動畫執(zhí)行的過程中才會創(chuàng)建合成層,動畫沒有開始或結(jié)束后元素還會回到之前的狀態(tài))
will-chang屬性(這個比較偏僻),一般配合opacity與translate使用(而且經(jīng)測試,除了上述可以引發(fā)硬件加速的屬性外,其它屬性并不會變成復(fù)合層),作用是提前告訴瀏覽器要變化,這樣瀏覽器會開始做一些優(yōu)化工作(這個最好用完后就釋放)
等元素
其它,譬如以前的flash插件
absolute和硬件加速的區(qū)別
可以看到,absolute雖然可以脫離普通文檔流,但是無法脫離默認復(fù)合層。
所以,就算absolute中信息改變時不會改變普通文檔流中render樹,但是,瀏覽器最終繪制時,是整個復(fù)合層繪制的,所以absolute中信息的改變,仍然會影響整個復(fù)合層的繪制。(瀏覽器會重繪它,如果復(fù)合層中內(nèi)容多,absolute帶來的繪制信息變化過大,資源消耗是非常嚴重的)
而硬件加速直接就是在另一個復(fù)合層了(另起爐灶),所以它的信息改變不會影響默認復(fù)合層(當(dāng)然了,內(nèi)部肯定會影響屬于自己的復(fù)合層),僅僅是引發(fā)最后的合成(輸出視圖)
復(fù)合圖層的作用
一般一個元素開啟硬件加速后會變成復(fù)合圖層,可以獨立于普通文檔流中,改動后可以避免整個頁面重繪,提升性能。
但是盡量不要大量使用復(fù)合圖層,否則由于資源消耗過度,頁面反而會變的更卡。
硬件加速時請使用index
使用硬件加速時,盡可能的使用index,防止瀏覽器默認給后續(xù)的元素創(chuàng)建復(fù)合層渲染
具體的原理是:
webkit CSS3中,如果這個元素添加了硬件加速,并且index層級比較低,那么在這個元素的后面其它元素(層級比這個元素高的,或者相同的,并且relective或absolute屬性相同的),會默認變?yōu)閺?fù)合層渲染,如果處理不當(dāng)會極大的影響性能
簡單點理解,可以認為是一個隱式合成的概念:如果a是一個復(fù)合層,而且b在a上面,那么b也會被隱式轉(zhuǎn)為一個復(fù)合圖層,這點需要特別注意
從Event Loop談JS的運行機制到此時,已經(jīng)是屬于瀏覽器頁面初次渲染完畢后的事情,JS引擎的一些運行機制分析。主要是結(jié)合Event Loop來談JS代碼是如何執(zhí)行的。
我們已經(jīng)知道了JS引擎是單線程的,知道了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í)行?因為可能在它推入到事件列表時,主線程還不空閑,正在執(zhí)行其它代碼,所以就必須等待,自然有誤差。
主線程在運行時會產(chǎn)生執(zhí)行棧,棧中的代碼調(diào)用某些api時,它們會在事件隊列中添加各種事件(當(dāng)滿足觸發(fā)條件后,如ajax請求完畢)。而當(dāng)棧中的代碼執(zhí)行完畢,就會去讀取事件隊列中的事件,去執(zhí)行那些回調(diào),如此循環(huán)。
定時器上面事件循環(huán)機制的核心是:JS引擎線程和事件觸發(fā)線程
調(diào)用setTimeout后,是由定時器線程控制等到特定時間后添加到事件隊列的,因為JS引擎是單線程的,如果處于阻塞線程狀態(tài)就會影響計時準確,因此很有必要另開一個線程用來計時。
當(dāng)使用setTimout或setInterval時,需要定時器線程計時,計時完成后就會將特定的事件推入事件隊列中。
如:
setTimeout(()=>console.log("hello!),1000) //等1000毫秒計時完畢后(由定時器線程計時),將回調(diào)函數(shù)推入事件隊列中,等待主線程執(zhí)行 setTimeout(()=>{ console.log("hello") },0) console.log("begin")
這段代碼的效果是最快的時間內(nèi)將回調(diào)函數(shù)推入事件隊列中,等待主線程執(zhí)行。
注意:
執(zhí)行結(jié)果是:先begin,后hello
雖然代碼的本意是0毫秒就推入事件隊列,但是W3C在HTML標準中規(guī)定,規(guī)定要求setTimeout中低于4ms的時間間隔算為4ms
就算不等待4ms,就算假設(shè)0毫秒就推入事件隊列,也會先執(zhí)行begin(因為只能可執(zhí)行棧內(nèi)空了后才會主動讀取事件隊列)
setInterval
用setTimeout模擬定期計時和直接用setInterval是有區(qū)別的:
每次setTimeout計時到后就會去執(zhí)行,然后執(zhí)行一段時間后才會繼續(xù)setTimeout,中間就多了誤差
而setInterval則是每次都精確的隔一段時間推入一個事件(但是,事件的實際執(zhí)行時間不一定就準確,還有可能是這個事件還沒執(zhí)行完畢,下一個事件就來了)
而且setInterval有一些比較致命的問題:
累積效應(yīng),如果setInterval代碼在setInterval再次添加到隊列之前還沒有完成執(zhí)行,就會導(dǎo)致定時器代碼連續(xù)運行好幾次,而之間沒有間隔,就算正常間隔執(zhí)行,多個setInterval的代碼執(zhí)行時間可能會比預(yù)期?。ㄒ驗榇a執(zhí)行需要一定時間)
比如你ios的webview,或者safari等瀏覽器中都有一人特點,在滾動的時候是不執(zhí)行JS的,如果使用了setInterval,會發(fā)現(xiàn)在滾動結(jié)束后會執(zhí)行多次由于滾動不執(zhí)行JS積攢回調(diào),如果回調(diào)執(zhí)行時間過長,就會非常容易造成卡頓問題和一些不可知的錯誤(setInterval自帶的優(yōu)化,如果當(dāng)前事件隊列中有setInterval的回調(diào),不會重復(fù)添加回調(diào))
而且把瀏覽器最小化顯示等操作時,setInterval并不是不執(zhí)行程序,它會把setInterval的回調(diào)函數(shù)放在隊列中,等瀏覽器窗口再次打開時,一瞬間全部執(zhí)行
所以,至于這么問題,一般認為的最佳方案是:用setTimeout模擬setInterval或者特殊場合直接用requestAnimationFrame
Promise時代的microtask與macrotask在es6盛行的現(xiàn)在,可以看下這題:
console.log("script start"); setTimeout(()=>{ console.log("setTimeout") },0); Promise.resolve() .then(()=>console.log("promise1")) .then(()=>console.log("promise2")) console.log("script end") //執(zhí)行結(jié)果: script start script end promise1 promise2 setTimeout
因為promise有一個新的概念microtask.或者可以說JS中分為兩種任務(wù):macrotask和microtask;
理解如下:
macrotask(又叫宏任務(wù)),主代碼塊,setTimeout,setInterval等(可以看到,事件隊列中的每一個事件都是一個macrotask)
可以理解是每次執(zhí)行的代碼就是一個宏任務(wù)(包括每次從事件隊列中獲取一個事件回調(diào)并放到執(zhí)行棧中執(zhí)行)
第一個macrotask會從頭到尾將這個任務(wù)執(zhí)行完畢,不會執(zhí)行其它
瀏覽器為了能夠使得JS內(nèi)部macrotask與DOM任務(wù)能夠有序的執(zhí)行,會在一個macrotask執(zhí)行結(jié)束后,在下一個macrotask執(zhí)行開始前,對頁面進行重新渲染(task->渲染->task->...)
microtask(又叫微任務(wù)),Promise,process.nextTick等。
可以理解是在當(dāng)前macrotask執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)
也就是說在當(dāng)前macrotask任務(wù)后,下一個macrotask之前,在渲染之前
所以它的響應(yīng)速度相比setTimeout(setTimeout是macrotask)會更快因為無需等待渲染
也就是說,在某一個macrotask執(zhí)行完成后,就會將在它執(zhí)行期間產(chǎn)生的所有microtask都執(zhí)行完畢(在渲染前)
注意:在Node環(huán)境下,process.nextTick的優(yōu)先級高于promise.也就是:在宏任務(wù)結(jié)束后會先執(zhí)行微任務(wù)隊列中的nextTick部分,然后才會執(zhí)行微任務(wù)中的promise部分。
另外,setImmediate則是規(guī)定:在下一次Event Loop(宏任務(wù))時觸發(fā)(所以它是屬于優(yōu)先級較高的宏任務(wù)),(Node.js文檔中稱,setImmediate指定的回調(diào)函數(shù),總是排在setTimeout前面),所以setImmediate如果嵌套的話,是需要經(jīng)過多個Loop才能完成的,而不會像process.nextTick一樣沒完沒了。
可以理解:
macrotask中的事件都是放在一個事件隊列中的,而這個隊列由事件觸發(fā)線程維護.
microtask中的所有微任務(wù)都是添加到微任務(wù)隊列中,等待當(dāng)前macrotask執(zhí)行完后執(zhí)行,而這個隊列由JS引擎線程維護。
所以:
執(zhí)行一個宏任務(wù)(棧中沒有就從事件隊列中獲?。?/p>
執(zhí)行過程中如果遇到微任務(wù),就將它添加到微任務(wù)的任務(wù)隊列中
宏任務(wù)執(zhí)行完畢后,立即執(zhí)行當(dāng)前微任務(wù)隊列中的所有微任務(wù)(依次執(zhí)行)
當(dāng)前宏任務(wù)執(zhí)行完畢,開始檢查渲染,然后GUI線程接管渲染
渲染完畢后,JS線程繼續(xù)接管,開始下一個宏任務(wù)(從事件隊列中獲取)
new Promise里的函數(shù)是直接執(zhí)行的算做主程序里,而且.then后面的才會放到微任務(wù)中。
另外,請注意下Promise的polyfill與官方版本的區(qū)別:
官方版本中,是標準的microtask形式
polyfill,一般都是通過setTimeout模擬的,所以是macrotask形式
請?zhí)貏e注意這兩點區(qū)別
注意,有一些瀏覽器執(zhí)行結(jié)果不一樣(因為它們可能把microtask當(dāng)成macrotask來執(zhí)行了),但是為了簡單,這里不描述一些不標準的瀏覽器下的場景(但記住,有些瀏覽器可能并不標準)
Mutation Observer可以用來實現(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=newMutationObserver(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)移除了Mutation Observer的方式(據(jù)說是兼容性原因),取而代之的是使用MessageChannel(當(dāng)然,默認情況仍然是Promise,不支持才兼容的)。
MessageChannel屬于宏任務(wù),優(yōu)先級是:setImmediate->MessageChannel->setTimeout,所以Vue(2.5+)內(nèi)部的nextTick與2.4及之前的實現(xiàn)是不一樣的,需要注意下。
參考文檔瀏覽器渲染機制
Js基礎(chǔ)知識(四) - js運行原理與機制
前端中的進程、線程、事件系統(tǒng)
JS是單線程,你了解其運行機制嗎?
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/102345.html
摘要:首先一上來就分享了兩個學(xué)習(xí)方法建立知識架構(gòu)追本溯源。列一份前端知識架構(gòu)圖在這章節(jié)中,分享了本專欄要學(xué)習(xí)的知識架構(gòu)瀏覽器的實現(xiàn)原理和前端工程實踐四個模塊。最后一個是前端工程實踐,從性能工具鏈持續(xù)集成搭建系統(tǒng)架構(gòu)與基礎(chǔ)庫五個方面講起。 明確你的前端學(xué)習(xí)路線 自己特別喜歡屯課,看著自己買的課,有種滿足感,仿佛知識都是我的了,翻看極客時間買的課,決定這段時間把重學(xué)前端專欄學(xué)習(xí)一遍。 從周六到今...
摘要:隨著時間的流逝,這些與自己相關(guān)的信息就散落在了各個角落,有的偶爾回頭檢索,大多數(shù)用后即丟棄,最終被遺忘遺失。私鏈信息目錄私鏈的目標是分類整理存放用戶積累的知識信息,幫助用戶構(gòu)建管理自己的知識信息體系。 這是一個信息社會,這是一個數(shù)字化時代,移動設(shè)備、互聯(lián)網(wǎng)、信息數(shù)字化已經(jīng)成為人所共知的常識。在這種環(huán)境中,你有沒有問過自己:屬于我的數(shù)字化信息都有哪些,都在什么地方呢? 每個人在每天都會生...
摘要:碎片化學(xué)習(xí)我們必須學(xué)會碎片化學(xué)習(xí)。碎片化學(xué)習(xí)也要講究方法,比如我以前寫的談學(xué)習(xí)讀源碼和面試經(jīng)都有提到碎片化學(xué)習(xí)的誤區(qū),并較之以正確的方法。首先,應(yīng)該建構(gòu)起基礎(chǔ)的知識體系碎片化學(xué)習(xí)仍然需要完整系統(tǒng)的知識體系。 4-27在小密圈接到第一次付費提問,喜獲8塊。慶祝一下。 這個話題也是我在小密圈里和那位同學(xué)的交流時產(chǎn)生的。他說他學(xué)習(xí)的知識也不系統(tǒng)化,學(xué)習(xí)的知識也比較混亂。不系統(tǒng)暫時沒有好辦法...
摘要:知識點前端面試有很多知識點,因為前端本就涉及到多個方面。因為對于這樣的前端框架我還不是很熟練,在這方面不能提供很好的學(xué)習(xí)思路。 關(guān)于這幾次的面試 前幾次的面試,讓我對于一個前端工程師需要掌握的知識體系有了一個全新的認識。之前自己在學(xué)習(xí)方面一直屬于野路子,沒有一個很規(guī)范的學(xué)習(xí)路徑,往往都是想到什么就去學(xué)什么。而且基本都是處于會用的那種水平。并沒有真正的做到知其然且知其所以然。面試基本都沒...
摘要:所有我們不熟悉或者沒有掌握的知識皆可稱之為知識盲區(qū),有知識盲區(qū)并不可怕,針對知識盲區(qū)去學(xué)習(xí)即可。只有這樣,我們才會準確知道自己的知識盲區(qū)所處何處,以及發(fā)現(xiàn)更多的知識盲區(qū)。節(jié)點對應(yīng)的對象是,其他的具體節(jié)點對象全都是繼承自對象。 前端時間在部門內(nèi)進行分享,準備素材時偶然發(fā)現(xiàn)下面的一個現(xiàn)象,因為和當(dāng)時分享的主題無關(guān),就先記下來了,事后重新審視,并把一些思考記錄如下: 一、問題 HTML: ...
摘要:在他的重學(xué)前端課程中提到到現(xiàn)在為止,前端工程師已經(jīng)成為研發(fā)體系中的重要崗位之一。大部分前端工程師的知識,其實都是來自于實踐和工作中零散的學(xué)習(xí)。一基礎(chǔ)前端工程師吃飯的家伙,深度廣度一樣都不能差。 開篇 前端開發(fā)是一個非常特殊的行業(yè),它的歷史實際上不是很長,但是知識之繁雜,技術(shù)迭代速度之快是其他技術(shù)所不能比擬的。 winter在他的《重學(xué)前端》課程中提到: 到現(xiàn)在為止,前端工程師已經(jīng)成為研...
閱讀 1881·2021-11-15 11:39
閱讀 1088·2020-12-03 17:06
閱讀 742·2019-12-27 11:42
閱讀 3277·2019-08-30 13:59
閱讀 1469·2019-08-26 13:22
閱讀 3291·2019-08-26 12:15
閱讀 2479·2019-08-26 10:22
閱讀 1566·2019-08-23 18:40