從瀏覽器多進程到JS單線程,JS運行機制最全面的一次梳理
最近發現有不少介紹JS單線程運行機制的文章,但是發現很多都僅僅是介紹某一部分的知識,而且各個地方的說法還不統一,容易造成困惑。 因此準備梳理這塊知識點,結合已有的認知,基於網上的大量參考資料, 從瀏覽器多進程到JS單線程,將JS引擎的運行機制系統的梳理一遍。
展現形式:由於是屬於系統梳理型,就沒有由淺入深了,而是從頭到尾的梳理知識體系, 重點是將關鍵節點的知識點串聯起來,而不是僅僅剖析某一部分知識。
內容是:從瀏覽器進程,再到瀏覽器內核運行,再到JS引擎單線程,再到JS事件循環機制,從頭到尾系統的梳理一遍,擺脫碎片化,形成一個知識體系
目標是:看完這篇文章後,對瀏覽器多進程,JS單線程,JS事件循環機制這些都能有一定理解, 有一個知識體系骨架,而不是似懂非懂的感覺。
另外,本文適合有一定經驗的前端人員,新手請規避,避免受到過多的概念衝擊。可以先存起來,有了一定理解後再看,也可以分成多批次觀看,避免過度疲勞。
大綱
區分進程和線程
瀏覽器是多進程的
瀏覽器都包含哪些進程?
瀏覽器多進程的優勢
重點是瀏覽器內核(渲染進程)
Browser進程和瀏覽器內核(Renderer進程)的通信過程
梳理瀏覽器內核中線程之間的關係
GUI渲染線程與JS引擎線程互斥
JS阻塞頁面載入
WebWorker,JS的多線程?
WebWorker與SharedWorker
簡單梳理下瀏覽器渲染流程
load事件與DOMContentLoaded事件的先後
css載入是否會阻塞dom樹渲染?
普通圖層和複合圖層
從Event Loop談JS的運行機制
事件循環機制進一步補充
單獨說說定時器
setTimeout而不是setInterval
事件循環進階:macrotask與microtask
寫在最後的話
區分進程和線程
線程和進程區分不清,是很多新手都會犯的錯誤,沒有關係。這很正常。先看看下面這個形象的比喻:
再完善完善概念:
然後再鞏固下:
如果是windows電腦中,可以打開任務管理器,可以看到有一個後台進程列表。對,那裡就是查看進程的地方,而且可以看到每個進程的內存資源信息以及cpu佔有率。
所以,應該更容易理解了:進程是cpu資源分配的最小單位(系統會給它分配內存)
最後,再用較為官方的術語描述一遍:
進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)
線程是cpu調度的最小單位(線程是建立在進程的基礎上的一次程序運行單位,一個進程中可以有多個線程)
tips
不同進程之間也可以通信,不過代價較大
現在,一般通用的叫法:單線程與多線程,都是指在一個進程內的單和多。(所以核心還是得屬於一個進程才行)
瀏覽器是多進程的
理解了進程與線程了區別後,接下來對瀏覽器進行一定程度上的認識:(先看下簡化理解)
瀏覽器是多進程的
瀏覽器之所以能夠運行,是因為系統給它的進程分配了資源(cpu、內存)
簡單點理解,每打開一個Tab頁,就相當於創建了一個獨立的瀏覽器進程。
關於以上幾點的驗證,請再第一張圖:
圖中打開了 瀏覽器的多個標籤頁,然後可以在 中看到有多個進程(分別是每一個Tab頁面有一個獨立的進程,以及一個主進程)。 感興趣的可以自行嘗試下,如果再多打開一個Tab頁,進程正常會+1以上
注意:在這裡瀏覽器應該也有自己的優化機制,有時候打開多個tab頁後,可以在Chrome任務管理器中看到,有些進程被合併了 (所以每一個Tab標籤對應一個進程並不一定是絕對的)
瀏覽器都包含哪些進程?
知道了瀏覽器是多進程後,再來看看它到底包含哪些進程:(為了簡化理解,僅列舉主要進程)
1.Browser進程:瀏覽器的主進程(負責協調、主控),只有一個。作用有
負責瀏覽器界面顯示,與用戶交互。如前進,後退等
負責各個頁面的管理,創建和銷毀其他進程
將Renderer進程得到的內存中的Bitmap,繪製到用戶界面上
網路資源的管理,下載等
2.第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建 3.GPU進程:最多一個,用於3D繪製等 4.瀏覽器渲染進程(瀏覽器內核)(Renderer進程,內部是多線程的):默認每個Tab頁面一個進程,互不影響。主要作用為
強化記憶:在瀏覽器中打開一個網頁相當於新起了一個進程(進程內有自己的多線程)
當然,瀏覽器有時會將多個進程合併(譬如打開多個空白標籤頁後,會發現多個空白標籤頁被合併成了一個進程),如圖
另外,可以通過Chrome的 自行驗證
瀏覽器多進程的優勢
相比於單進程瀏覽器,多進程有如下優點:
避免單個page crash影響整個瀏覽器
避免第三方插件crash影響整個瀏覽器
多進程充分利用多核優勢
方便使用沙盒模型隔離插件等進程,提高瀏覽器穩定性
簡單點理解:如果瀏覽器是單進程,那麼某個Tab頁崩潰了,就影響了整個瀏覽器,體驗有多差;同理如果是單進程,插件崩潰了也會影響整個瀏覽器;而且多進程還有其它的諸多優勢。。。
當然,內存等資源消耗也會更大,有點空間換時間的意思。
重點是瀏覽器內核(渲染進程)
重點來了,我們可以看到,上面提到了這麼多的進程,那麼,對於普通的前端操作來說,最終要的是什麼呢?答案是渲染進程
可以這樣理解,頁面的渲染,JS的執行,事件的循環,都在這個進程內進行。接下來重點分析這個進程
請牢記,瀏覽器的渲染進程是多線程的(這點如果不理解,請回頭看進程和線程的區分)
終於到了線程這個概念了,好親切。那麼接下來看看它都包含了哪些線程(列舉一些主要常駐線程):
1.GUI渲染線程
負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,布局和繪製等。
當界面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該線程就會執行
注意,GUI渲染線程與JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(相當於被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閑時立即被執行。
2.JS引擎線程
也稱為JS內核,負責處理Javascript腳本程序。(例如V8引擎)
JS引擎線程負責解析Javascript腳本,運行代碼。
JS引擎一直等待著任務隊列中任務的到來,然後加以處理,瀏覽器無論什麼時候都只有一個JS線程在運行JS程序
同樣注意,GUI渲染線程與JS引擎線程是互斥的,所以如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞。
3.事件觸發線程
歸屬於瀏覽器而不是JS引擎,用來控制事件循環(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開線程協助)
當JS引擎執行代碼塊如setTimeOut時(也可來自瀏覽器內核的其他線程,如滑鼠點擊、AJAX非同步請求等),會將對應任務添加到事件線程中
當對應的事件符合觸發條件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待JS引擎的處理
注意,由於JS的單線程關係,所以這些待處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閑時才會去執行)
4.定時觸發器線程
傳說中的 與 所在線程
瀏覽器定時計數器並不是由JavaScript引擎計數的,(因為JavaScript引擎是單線程的, 如果處於阻塞線程狀態就會影響記計時的準確)
因此通過單獨線程來計時並觸發定時(計時完畢後,添加到事件隊列中,等待JS引擎空閑後執行)
注意,W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算為4ms。
5.非同步http請求線程
在XMLHttpRequest在連接後是通過瀏覽器新開一個線程請求
將檢測到狀態變更時,如果設置有回調函數,非同步線程就產生狀態變更事件,將這個回調再放入事件隊列中。再由JavaScript引擎執行。
看到這裡,如果覺得累了,可以先休息下,這些概念需要被消化,畢竟後續將提到的事件循環機制就是基於 的,所以如果僅僅是看某個碎片化知識, 可能會有一種似懂非懂的感覺。要完成的梳理一遍才能快速沉澱,不易遺忘。放張圖鞏固下吧:
再說一點,為什麼JS引擎是單線程的?額,這個問題其實應該沒有標準答案,譬如,可能僅僅是因為由於多線程的複雜性,譬如多線程操作一般要加鎖,因此最初設計時選擇了單線程。。。
Browser進程和瀏覽器內核(Renderer進程)的通信過程
看到這裡,首先,應該對瀏覽器內的進程和線程都有一定理解了,那麼接下來,再談談瀏覽器的Browser進程(控制進程)是如何和內核通信的, 這點也理解後,就可以將這部分的知識串聯起來,從頭到尾有一個完整的概念。
如果自己打開任務管理器,然後打開一個瀏覽器,就可以看到:任務管理器中出現了兩個進程(一個是主控進程,一個則是打開Tab頁的渲染進程), 然後在這前提下,看下整個的過程:(簡化了很多)
Browser進程收到用戶請求,首先需要獲取頁面內容(譬如通過網路下載資源),隨後將該任務通過RendererHost介面傳遞給Render進程
Renderer進程的Renderer介面收到消息,簡單解釋後,交給渲染線程,然後開始渲染
渲染線程接收請求,載入網頁並渲染網頁,這其中可能需要Browser進程獲取資源和需要GPU進程來幫助渲染
當然可能會有JS線程操作DOM(這樣可能會造成迴流並重繪)
最後Render進程將結果傳遞給Browser進程
Browser進程接收到結果並將結果繪製出來
這裡繪一張簡單的圖:(很簡化)
看完這一整套流程,應該對瀏覽器的運作有了一定理解了,這樣有了知識架構的基礎後,後續就方便往上填充內容。
這塊再往深處講的話就涉及到瀏覽器內核源碼解析了,不屬於本文範圍。
如果這一塊要深挖,建議去讀一些瀏覽器內核源碼解析文章,或者可以先看看參考下來源中的第一篇文章,寫的不錯
梳理瀏覽器內核中線程之間的關係
到了這裡,已經對瀏覽器的運行有了一個整體的概念,接下來,先簡單梳理一些概念
GUI渲染線程與JS引擎線程互斥
由於JavaScript是可操縱DOM的,如果在修改這些元素屬性同時渲染界面(即JS線程和UI線程同時運行),那麼渲染線程前後獲得的元素數據就可能不一致了。
因此為了防止渲染出現不可預期的結果,瀏覽器設置GUI渲染線程與JS引擎為互斥的關係,當JS引擎執行時GUI線程會被掛起, GUI更新則會被保存在一個隊列中等到JS引擎線程空閑時立即被執行。
JS阻塞頁面載入
從上述的互斥關係,可以推導出,JS如果執行時間過長就會阻塞頁面。
譬如,假設JS引擎正在進行巨量的計算,此時就算GUI有更新,也會被保存到隊列中,等待JS引擎空閑後執行。 然後,由於巨量計算,所以JS引擎很可能很久很久後才能空閑,自然會感覺到巨卡無比。
所以,要盡量避免JS執行時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞的感覺。
WebWorker,JS的多線程?
前文中有提到JS引擎是單線程的,而且JS執行時間過長會阻塞頁面,那麼JS就真的對cpu密集型計算無能為力么?
所以,後來HTML5中支持了 。
MDN的官方解釋是:
這樣理解下:
創建Worker時,JS引擎向瀏覽器申請開一個子線程(子線程是瀏覽器開的,完全受主線程式控制制,而且不能操作DOM)
JS引擎線程與worker線程間通過特定的方式通信(postMessage API,需要通過序列化對象來與線程交互特定的數據)
所以,如果有非常耗時的工作,請單獨開一個Worker線程,這樣裡面不管如何翻天覆地都不會影響JS引擎主線程, 只待計算出結果後,將結果通信給主線程即可,perfect!
而且注意下,JS引擎是單線程的,這一點的本質仍然未改變,Worker可以理解是瀏覽器給JS引擎開的外掛,專門用來解決那些大量計算問題。
其它,關於Worker的詳解就不是本文的範疇了,因此不再贅述。
WebWorker與SharedWorker
既然都到了這裡,就再提一下 (避免後續將這兩個概念搞混)
WebWorker只屬於某個頁面,不會和其他頁面的Render進程(瀏覽器內核進程)共享
所以Chrome在Render進程中(每一個Tab頁就是一個render進程)創建一個新的線程來運行Worker中的JavaScript程序。
SharedWorker是瀏覽器所有頁面共享的,不能採用與Worker同樣的方式實現,因為它不隸屬於某個Render進程,可以為多個Render進程共享使用
所以Chrome瀏覽器為SharedWorker單獨創建一個進程來運行JavaScript程序,在瀏覽器中每個相同的JavaScript只存在一個SharedWorker進程,不管它被創建多少次。
看到這裡,應該就很容易明白了,本質上就是進程和線程的區別。SharedWorker由獨立的進程管理,WebWorker只是屬於render進程下的一個線程
簡單梳理下瀏覽器渲染流程
本來是直接計劃開始談JS運行機制的,但想了想,既然上述都一直在談瀏覽器,直接跳到JS可能再突兀,因此,中間再補充下瀏覽器的渲染流程(簡單版本)
為了簡化理解,前期工作直接省略成:(要展開的或完全可以寫另一篇超長文)
瀏覽器器內核拿到內容後,渲染大概可以劃分成以下幾個步驟:
解析html建立dom樹
解析css構建render樹(將CSS代碼解析成樹形的數據結構,然後結合DOM合併成render樹)
布局render樹(Layout/reflow),負責各元素尺寸、位置的計算
繪製render樹(paint),繪製頁面像素信息
瀏覽器會將各層的信息發送給GPU,GPU會將各層合成(composite),顯示在屏幕上。
所有詳細步驟都已經略去,渲染完畢後就是 事件了,之後就是自己的JS邏輯處理了
既然略去了一些詳細的步驟,那麼就提一些可能需要注意的細節把。
load事件與DOMContentLoaded事件的先後
上面提到,渲染完畢後會觸發 事件,那麼你能分清楚 事件與 事件的先後么?
很簡單,知道它們的定義就可以了:
當 DOMContentLoaded 事件觸發時,僅當DOM載入完成,不包括樣式表,圖片。
(譬如如果有async載入的腳本就不一定完成)
當 onload 事件觸發時,頁面上所有的DOM,樣式表,腳本,圖片都已經載入完成了。
(渲染完畢了)
所以,順序是:
css載入是否會阻塞dom樹渲染?
這裡說的是頭部引入css的情況
首先,我們都知道:css是由單獨的下載線程非同步下載的。
然後再說下幾個現象:
css載入不會阻塞DOM樹解析(非同步載入時DOM照常構建)
但會阻塞render樹渲染(渲染時需等css載入完畢,因為render樹需要css信息)
這可能也是瀏覽器的一種優化機制。
因為你載入css的時候,可能會修改下面DOM節點的樣式, 如果css載入不阻塞render樹渲染的話,那麼當css載入完之後, render樹可能又得重新重繪或者迴流了,這就造成了一些沒有必要的損耗。 所以乾脆就先把DOM樹的結構先解析完,把可以做的工作做完,然後等你css載入完之後, 在根據最終的樣式來渲染render樹,這種做法性能方面確實會比較好一點。
普通圖層和複合圖層
渲染步驟中就提到了 概念。
可以簡單的這樣理解,瀏覽器渲染的圖層一般包含兩大類: 以及
首先,普通文檔流內可以理解為一個複合圖層(這裡稱為 ,裡面不管添加多少元素,其實都是在同一個複合圖層中)
其次,absolute布局(fixed也一樣),雖然可以脫離普通文檔流,但它仍然屬於 。
然後,可以通過 的方式,聲明一個 ,它會單獨分配資源 (當然也會脫離普通文檔流,這樣一來,不管這個複合圖層中怎麼變化,也不會影響 里的迴流重繪)
可以簡單理解下:GPU中,各個複合圖層是單獨繪製的,所以互不影響,這也是為什麼某些場景硬體加速效果一級棒
可以 中看到,黃色的就是複合圖層信息
如下圖。可以驗證上述的說法
如何變成複合圖層(硬體加速)
將該元素變成一個複合圖層,就是傳說中的硬體加速技術
最常用的方式: 、
屬性/過渡動畫(需要動畫執行的過程中才會創建合成層,動畫沒有開始或結束後元素還會回到之前的狀態)
屬性(這個比較偏僻),一般配合opacity與translate使用(而且經測試,除了上述可以引發硬體加速的屬性外,其它屬性並不會變成複合層),
作用是提前告訴瀏覽器要變化,這樣瀏覽器會開始做一些優化工作(這個最好用完後就釋放)
等元素
其它,譬如以前的flash插件
absolute和硬體加速的區別
可以看到,absolute雖然可以脫離普通文檔流,但是無法脫離默認複合層。 所以,就算absolute中信息改變時不會改變普通文檔流中render樹, 但是,瀏覽器最終繪製時,是整個複合層繪製的,所以absolute中信息的改變,仍然會影響整個複合層的繪製。 (瀏覽器會重繪它,如果複合層中內容多,absolute帶來的繪製信息變化過大,資源消耗是非常嚴重的)
而硬體加速直接就是在另一個複合層了(另起爐灶),所以它的信息改變不會影響默認複合層 (當然了,內部肯定會影響屬於自己的複合層),僅僅是引發最後的合成(輸出視圖)
複合圖層的作用?
一般一個元素開啟硬體加速後會變成複合圖層,可以獨立於普通文檔流中,改動後可以避免整個頁面重繪,提升性能
但是盡量不要大量使用複合圖層,否則由於資源消耗過度,頁面反而會變的更卡
硬體加速時請使用index
使用硬體加速時,儘可能的使用index,防止瀏覽器默認給後續的元素創建複合層渲染
具體的原理時這樣的:webkit CSS3中,如果這個元素添加了硬體加速,並且index層級比較低, 那麼在這個元素的後面其它元素(層級比這個元素高的,或者相同的,並且releative或absolute屬性相同的), 會默認變為複合層渲染,如果處理不當會極大的影響性能
簡單點理解,其實可以認為是一個隱式合成的概念:如果a是一個複合圖層,而且b在a上面,那麼b也會被隱式轉為一個複合圖層,這點需要特別注意
另外,這個問題可以在這個地址看到重現(原作者分析的挺到位的,直接上鏈接):
http://web.jobbole.com/83575/
從Event Loop談JS的運行機制
到此時,已經是屬於瀏覽器頁面初次渲染完畢後的事情,JS引擎的一些運行機制分析。
注意,這裡不談 , , 等概念(這些完全可以整理成另一篇文章了),這裡主要是結合 來談JS代碼是如何執行的。
讀這部分的前提是已經知道了JS引擎是單線程,而且這裡會用到上文中的幾個概念:(如果不是很理解,可以回頭溫習)
然後再理解一個概念:
JS分為同步任務和非同步任務
同步任務都在主線程上執行,形成一個
主線程之外,事件觸發線程管理著一個 ,只要非同步任務有了運行結果,就在 之中放置一個事件。
一旦 中的所有同步任務執行完畢(此時JS引擎空閑),系統就會讀取 ,將可運行的非同步任務添加到可執行棧中,開始執行。
看圖:
看到這裡,應該就可以理解了:為什麼有時候setTimeout推入的事件不能準時執行?因為可能在它推入到事件列表時,主線程還不空閑,正在執行其它代碼, 所以自然有誤差。
事件循環機制進一步補充
這裡就直接引用一張圖片來協助理解:(參考自Philip Roberts的演講《Help, I"m stuck in an event-loop》)
上圖大致描述就是:
棧中的代碼調用某些api時,它們會在事件隊列中添加各種事件(當滿足觸發條件後,如ajax請求完畢)
而棧中的代碼執行完畢,就會讀取事件隊列中的事件,去執行那些回調
如此循環
注意,總是要等待棧中的代碼執行完畢後才會去讀取事件隊列中的事件
單獨說說定時器
上述事件循環機制的核心是:JS引擎線程和事件觸發線程
但事件上,裡面還有一些隱藏細節,譬如調用 後,是如何等待特定時間後才添加到事件隊列中的?
是JS引擎檢測的么?當然不是了。它是由定時器線程控制(因為JS引擎自己都忙不過來,根本無暇分身)
為什麼要單獨的定時器線程?因為JavaScript引擎是單線程的, 如果處於阻塞線程狀態就會影響記計時的準確,因此很有必要單獨開一個線程用來計時。
什麼時候會用到定時器線程?當使用 或 時,它需要定時器線程計時,計時完成後就會將特定的事件推入事件隊列中。
譬如:
這段代碼的作用是當 毫秒計時完畢後(由定時器線程計時),將回調函數推入事件隊列中,等待主線程執行
這段代碼的效果是最快的時間內將回調函數推入事件隊列中,等待主線程執行
注意:
執行結果是:先 後
雖然代碼的本意是0毫秒後就推入事件隊列,但是W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算為4ms。
(不過也有一說是不同瀏覽器有不同的最小時間設定)
就算不等待4秒,就算假設0毫秒就推入事件隊列,也會先執行 (因為只有可執行棧內空了後才會主動讀取事件隊列)
setTimeout而不是setInterval
用setTimeout模擬定期計時和直接用setInterval是有區別的。
因為每次setTimeout計時到後就會去執行,然後執行一段時間後才會繼續setTimeout,中間就多了誤差 (誤差多少與代碼執行時間有關)
而setInterval則是每次都精確的隔一段時間推入一個事件 (但是,事件的實際執行時間不一定就準確,還有可能是這個事件還沒執行完畢,下一個事件就來了)
而且setInterval有一些比較致命的問題就是:
累計效應(上面提到的),如果setInterval代碼在(setInterval)再次添加到隊列之前還沒有完成執行,
就會導致定時器代碼連續運行好幾次,而之間沒有間隔。 就算正常間隔執行,多個setInterval的代碼執行時間可能會比預期小(因為代碼執行需要一定時間)
譬如像iOS的webview,或者Safari等瀏覽器中都有一個特點,在滾動的時候是不執行JS的,
如果使用了setInterval,會發現在滾動結束後會執行多次由於滾動不執行JS積攢回調, 如果回調執行時間過長,就會非常容器造成卡頓問題和一些不可知的錯誤
而且把瀏覽器最小化顯示等操作時,setInterval並不是不執行程序,
它會把setInterval的回調函數放在隊列中,等瀏覽器窗口再次打開時,一瞬間全部執行時
所以,鑒於這麼多但問題,目前一般認為的最佳方案是:用setTimeout模擬setInterval,或者特殊場合直接用requestAnimationFrame
事件循環進階:macrotask與microtask
這段參考了參考來源中的第2篇文章(英文版的),(加了下自己的理解重新描述了下), 強烈推薦有英文基礎的同學直接觀看原文,作者描述的很清晰,示例也很不錯,如下:
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/
上文中將JS事件循環機制梳理了一遍,在ES5的情況是夠用了,但是在ES6盛行的現在,仍然會遇到一些問題,譬如下面這題:
嗯哼,它的正確執行順序是這樣子的:
為什麼呢?因為Promise里有了一個一個新的概念:
或者,進一步,JS中分為兩種任務類型:和,在ECMAScript中,microtask稱為 ,macrotask可稱為
它們的定義?區別?簡單點可以按如下理解:
macrotask(又稱之為宏任務),可以理解是每次執行棧執行的代碼就是一個宏任務(包括每次從事件隊列中獲取一個事件回調並放到執行棧中執行)
每一個task會從頭到尾將這個任務執行完畢,不會執行其它
瀏覽器為了能夠使得JS內部task與DOM任務能夠有序的執行,會在一個task執行結束後,在下一個 task 執行開始前,對頁面進行重新渲染
microtask(又稱為微任務),可以理解是在當前 task 執行結束後立即執行的任務
也就是說,在當前task任務後,下一個task之前,在渲染之前
所以它的響應速度相比setTimeout(setTimeout是task)會更快,因為無需等渲染
也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的所有microtask都執行完畢(在渲染前)
分別很么樣的場景會形成macrotask和microtask呢?
macrotask:主代碼塊,setTimeout,setInterval等(可以看到,事件隊列中的每一個事件都是一個macrotask)
microtask:Promise,process.nextTick等
再根據線程來理解下:
macrotask中的事件都是放在一個事件隊列中的,而這個隊列由事件觸發線程維護
microtask中的所有微任務都是添加到微任務隊列(Job Queues)中,等待當前macrotask執行完畢後執行,而這個隊列由JS引擎線程維護
(這點由自己理解+推測得出,因為它是在主線程下無縫執行的)
所以,總結下運行機制:
執行一個宏任務(棧中沒有就從事件隊列中獲取)
執行過程中如果遇到微任務,就將它添加到微任務的任務隊列中
宏任務執行完畢後,立即執行當前微任務隊列中的所有微任務(依次執行)
當前宏任務執行完畢,開始檢查渲染,然後GUI線程接管渲染
渲染完畢後,JS線程繼續接管,開始下一個宏任務(從事件隊列中獲取)
如圖:
另外,請注意下 的 與官方版本的區別:
官方版本中,是標準的microtask形式
polyfill,一般都是通過setTimeout模擬的,所以是macrotask形式(目前沒見過有能直接模擬microtask,如果確實有,後續會修改這部分)
請特別注意這兩點區別
注意,有一些瀏覽器執行結果不一樣(因為它們可能把microtask當成macrotask來執行了), 但是為了簡單,這裡不描述一些不標準的瀏覽器下的場景(但記住,有些瀏覽器可能並不標準)
寫在最後的話
看到這裡,不知道對JS的運行機制是不是更加理解了,從頭到尾梳理,而不是就某一個碎片化知識應該是會更清晰的吧?
同時,也應該注意到了JS根本就沒有想像的那麼簡單,前端的知識也是無窮無盡,層出不窮的概念、N多易忘的知識點、各式各樣的框架、 底層原理方面也是可以無限的往下深挖,然後你就會發現,你知道的太少了。。。
另外,本文也打算先告一段落,其它的,如JS詞法解析,可執行上下文以及VO等概念就不繼續在本文中寫了,後續可以考慮另開新的文章。
最後,喜歡的話,就請給個贊吧!
原文:https://segmentfault.com/a/1190000012925872
TAG:JavaScript |