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