JS 執行機制最全面的一次梳理

語言: CN / TW / HK

文末免費送三本最近的 新 "紅寶書"

最近發現有不少介紹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頁,就相當於建立了一個獨立的瀏覽器程序。

關於以上幾點的驗證, 請再第一張圖

圖中打開了 Chrome 瀏覽器的多個標籤頁,然後可以在 Chrome的工作管理員 中看到有多個程序(分別是每一個Tab頁面有一個獨立的程序,以及一個主程序)。感興趣的可以自行嘗試下,如果再多開啟一個Tab頁,程序正常會+1以上

注意: 在這裡瀏覽器應該也有自己的優化機制,有時候開啟多個tab頁後,可以在Chrome工作管理員中看到,有些程序被合併了 (所以每一個Tab標籤對應一個程序並不一定是絕對的)

瀏覽器都包含哪些程序?

知道了瀏覽器是多程序後,再來看看它到底包含哪些程序:(為了簡化理解,僅列舉主要程序)

  1. Browser程序:瀏覽器的主程序(負責協調、主控),只有一個。作用有

  • 負責瀏覽器介面顯示,與使用者互動。如前進,後退等

  • 負責各個頁面的管理,建立和銷燬其他程序

  • 將Renderer程序得到的記憶體中的Bitmap,繪製到使用者介面上

  • 網路資源的管理,下載等

  • 第三方外掛程序:每種型別的外掛對應一個程序,僅當使用該外掛時才建立

  • GPU程序:最多一個,用於3D繪製等

  • 瀏覽器渲染程序(瀏覽器核心)(Renderer程序,內部是多執行緒的):預設每個Tab頁面一個程序,互不影響。主要作用為

    • 頁面渲染,指令碼執行,事件處理等

    強化記憶: 在瀏覽器中開啟一個網頁相當於新起了一個程序(程序內有自己的多執行緒)

    當然,瀏覽器有時會將多個程序合併(譬如開啟多個空白標籤頁後,會發現多個空白標籤頁被合併成了一個程序),如圖

    另外,可以通過Chrome的 更多工具 -> 工作管理員 自行驗證

    瀏覽器多程序的優勢

    相比於單程序瀏覽器,多程序有如下優點:

    • 避免單個page crash影響整個瀏覽器

    • 避免第三方外掛crash影響整個瀏覽器

    • 多程序充分利用多核優勢

    • 方便使用沙盒模型隔離外掛等程序,提高瀏覽器穩定性

    簡單點理解: 如果瀏覽器是單程序,那麼某個Tab頁崩潰了,就影響了整個瀏覽器,體驗有多差;同理如果是單程序,外掛崩潰了也會影響整個瀏覽器;而且多程序還有其它的諸多優勢。。。

    當然,記憶體等資源消耗也會更大,有點空間換時間的意思。

    重點是瀏覽器核心(渲染程序)

    重點來了,我們可以看到,上面提到了這麼多的程序,那麼,對於普通的前端操作來說,最終要的是什麼呢?答案是 渲染程序

    可以這樣理解,頁面的渲染,JS的執行,事件的迴圈,都在這個程序內進行。接下來重點分析這個程序

    請牢記,瀏覽器的渲染程序是多執行緒的 (這點如果不理解, 請回頭看程序和執行緒的區分

    終於到了執行緒這個概念了?,好親切。那麼接下來看看它都包含了哪些執行緒(列舉一些主要常駐執行緒):

    1. GUI渲染執行緒

    • 負責渲染瀏覽器介面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。

    • 當介面需要重繪(Repaint)或由於某種操作引發迴流(reflow)時,該執行緒就會執行

    • 注意, GUI渲染執行緒與JS引擎執行緒是互斥的 ,當JS引擎執行時GUI執行緒會被掛起(相當於被凍結了),GUI更新會被儲存在一個佇列中 等到JS引擎空閒時 立即被執行。

  • JS引擎執行緒

    • 也稱為JS核心,負責處理Javascript指令碼程式。(例如V8引擎)

    • JS引擎執行緒負責解析Javascript指令碼,執行程式碼。

    • JS引擎一直等待著任務佇列中任務的到來,然後加以處理,一個Tab頁(renderer程序)中無論什麼時候都只有一個JS執行緒在執行JS程式

    • 同樣注意, GUI渲染執行緒與JS引擎執行緒是互斥的 ,所以如果JS執行的時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞。

  • 事件觸發執行緒

    • 歸屬於瀏覽器而不是JS引擎,用來控制事件迴圈(可以理解,JS引擎自己都忙不過來,需要瀏覽器另開執行緒協助)

    • 當JS引擎執行程式碼塊如setTimeOut時(也可來自瀏覽器核心的其他執行緒,如滑鼠點選、AJAX非同步請求等),會將對應任務新增到事件執行緒中

    • 當對應的事件符合觸發條件被觸發時,該執行緒會把事件新增到待處理佇列的隊尾,等待JS引擎的處理

    • 注意,由於JS的單執行緒關係,所以這些待處理佇列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時才會去執行)

  • 定時觸發器執行緒

    • 傳說中的 setIntervalsetTimeout 所線上程
    • 瀏覽器定時計數器並不是由JavaScript引擎計數的,(因為JavaScript引擎是單執行緒的, 如果處於阻塞執行緒狀態就會影響記計時的準確)

    • 因此通過單獨執行緒來計時並觸發定時(計時完畢後,新增到事件佇列中,等待JS引擎空閒後執行)

    • 注意,W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算為4ms。

  • 非同步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中支援了 Web Worker

    MDN的官方解釋是:

    Web Worker為Web內容在後臺執行緒中執行指令碼提供了一種簡單的方法。執行緒可以執行任務而不干擾使用者介面

    一個worker是使用一個建構函式建立的一個物件(e.g. Worker()) 執行一個命名的JavaScript檔案

    這個檔案包含將在工作執行緒中執行的程式碼; workers 執行在另一個全域性上下文中,不同於當前的window

    因此,使用 window快捷方式獲取當前全域性的範圍 (而不是self) 在一個 Worker 內將返回錯誤

    這樣理解下:

    • 建立Worker時,JS引擎向瀏覽器申請開一個子執行緒(子執行緒是瀏覽器開的,完全受主執行緒控制,而且不能操作DOM)

    • JS引擎執行緒與worker執行緒間通過特定的方式通訊(postMessage API,需要通過序列化物件來與執行緒互動特定的資料)

    所以,如果有非常耗時的工作,請單獨開一個Worker執行緒,這樣裡面不管如何翻天覆地都不會影響JS引擎主執行緒, 只待計算出結果後,將結果通訊給主執行緒即可,perfect!

    而且注意下, JS引擎是單執行緒的 ,這一點的本質仍然未改變,Worker可以理解是瀏覽器給JS引擎開的外掛,專門用來解決那些大量計算問題。

    其它,關於Worker的詳解就不是本文的範疇了,因此不再贅述。

    WebWorker與SharedWorker

    既然都到了這裡,就再提一下 SharedWorker (避免後續將這兩個概念搞混)

    • WebWorker只屬於某個頁面,不會和其他頁面的Render程序(瀏覽器核心程序)共享

      • 所以Chrome在Render程序中(每一個Tab頁就是一個render程序)建立一個新的執行緒來執行Worker中的JavaScript程式。

    • SharedWorker是瀏覽器所有頁面共享的,不能採用與Worker同樣的方式實現,因為它不隸屬於某個Render程序,可以為多個Render程序共享使用

      • 所以Chrome瀏覽器為SharedWorker單獨建立一個程序來執行JavaScript程式,在瀏覽器中每個相同的JavaScript只存在一個SharedWorker程序,不管它被建立多少次。

    看到這裡,應該就很容易明白了,本質上就是程序和執行緒的區別。SharedWorker由獨立的程序管理,WebWorker只是屬於render程序下的一個執行緒

    簡單梳理下瀏覽器渲染流程

    本來是直接計劃開始談JS執行機制的,但想了想,既然上述都一直在談瀏覽器,直接跳到JS可能再突兀,因此,中間再補充下瀏覽器的渲染流程(簡單版本)

    為了簡化理解,前期工作直接省略成:(要展開的或完全可以寫另一篇超長文)

    - 瀏覽器輸入url,瀏覽器主程序接管,開一個下載執行緒,
    然後進行 http請求(略去DNS查詢,IP定址等等操作),然後等待響應,獲取內容,
    隨後將內容通過RendererHost介面轉交給Renderer程序

    - 瀏覽器渲染流程開始

    瀏覽器器核心拿到內容後,渲染大概可以劃分成以下幾個步驟:

    1. 解析html建立dom樹

    2. 解析css構建render樹(將CSS程式碼解析成樹形的資料結構,然後結合DOM合併成render樹)

    3. 佈局render樹(Layout/reflow),負責各元素尺寸、位置的計算

    4. 繪製render樹(paint),繪製頁面畫素資訊

    5. 瀏覽器會將各層的資訊傳送給GPU,GPU會將各層合成(composite),顯示在螢幕上。

    所有詳細步驟都已經略去,渲染完畢後就是 load 事件了,之後就是自己的JS邏輯處理了

    既然略去了一些詳細的步驟,那麼就提一些可能需要注意的細節把。

    這裡重繪參考來源中的一張圖:(參考來源第一篇)

    load事件與DOMContentLoaded事件的先後

    上面提到,渲染完畢後會觸發 load 事件,那麼你能分清楚 load 事件與 DOMContentLoaded 事件的先後麼?

    很簡單,知道它們的定義就可以了:

    • 當 DOMContentLoaded 事件觸發時,僅當DOM載入完成,不包括樣式表,圖片。

    (譬如如果有async載入的指令碼就不一定完成)

    • 當 onload 事件觸發時,頁面上所有的DOM,樣式表,指令碼,圖片都已經載入完成了。

    (渲染完畢了)

    所以,順序是: DOMContentLoaded -> load

    css載入是否會阻塞dom樹渲染?

    這裡說的是頭部引入css的情況

    首先,我們都知道: css是由單獨的下載執行緒非同步下載的。

    然後再說下幾個現象:

    • css載入不會阻塞DOM樹解析(非同步載入時DOM照常構建)

    • 但會阻塞render樹渲染(渲染時需等css載入完畢,因為render樹需要css資訊)

    這可能也是瀏覽器的一種優化機制。

    因為你載入css的時候,可能會修改下面DOM節點的樣式, 如果css載入不阻塞render樹渲染的話,那麼當css載入完之後, render樹可能又得重新重繪或者回流了,這就造成了一些沒有必要的損耗。所以乾脆就先把DOM樹的結構先解析完,把可以做的工作做完,然後等你css載入完之後, 在根據最終的樣式來渲染render樹,這種做法效能方面確實會比較好一點。

    普通圖層和複合圖層

    渲染步驟中就提到了 composite 概念。

    可以簡單的這樣理解,瀏覽器渲染的圖層一般包含兩大類: 普通圖層 以及 複合圖層

    首先,普通文件流內可以理解為一個複合圖層(這裡稱為 預設複合層 ,裡面不管新增多少元素,其實都是在同一個複合圖層中)

    其次,absolute佈局(fixed也一樣),雖然可以脫離普通文件流,但它仍然屬於 預設複合層

    然後,可以通過 硬體加速 的方式,宣告一個 新的複合圖層 ,它會單獨分配資源 (當然也會脫離普通文件流,這樣一來,不管這個複合圖層中怎麼變化,也不會影響 預設複合層 裡的迴流重繪)

    可以簡單理解下: GPU中,各個複合圖層是單獨繪製的,所以互不影響 ,這也是為什麼某些場景硬體加速效果一級棒

    可以 Chrome原始碼除錯 -> More Tools -> Rendering -> Layer borders 中看到,黃色的就是複合圖層資訊

    如下圖。可以驗證上述的說法

    如何變成複合圖層(硬體加速)

    將該元素變成一個複合圖層,就是傳說中的硬體加速技術

    • 最常用的方式: translate3dtranslateZ
    • opacity 屬性/過渡動畫(需要動畫執行的過程中才會建立合成層,動畫沒有開始或結束後元素還會回到之前的狀態)
    • will-chang 屬性(這個比較偏僻),一般配合opacity與translate使用(而且經測試,除了上述可以引發硬體加速的屬性外,其它屬性並不會變成複合層),

    作用是提前告訴瀏覽器要變化,這樣瀏覽器會開始做一些優化工作(這個最好用完後就釋放)

    • <video><iframe><canvas><webgl> 等元素
    • 其它,譬如以前的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引擎的一些執行機制分析。

    注意,這裡不談 可執行上下文VOscop chain 等概念(這些完全可以整理成另一篇文章了),這裡主要是結合 Event Loop 來談JS程式碼是如何執行的。

    讀這部分的前提是已經知道了JS引擎是單執行緒,而且這裡會用到上文中的幾個概念:(如果不是很理解,可以回頭溫習)

    • JS引擎執行緒

    • 事件觸發執行緒

    • 定時觸發器執行緒

    然後再理解一個概念:

    • JS分為同步任務和非同步任務

    • 同步任務都在主執行緒上執行,形成一個 執行棧
    • 主執行緒之外, 事件觸發執行緒 管理著一個 任務佇列 ,只要非同步任務有了執行結果,就在 任務佇列 之中放置一個事件。
    • 一旦 執行棧 中的所有同步任務執行完畢(此時JS引擎空閒),系統就會讀取 任務佇列 ,將可執行的非同步任務新增到可執行棧中,開始執行。

    看圖:

    看到這裡,應該就可以理解了:為什麼有時候setTimeout推入的事件不能準時執行?因為可能在它推入到事件列表時,主執行緒還不空閒,正在執行其它程式碼, 所以自然有誤差。

    事件迴圈機制進一步補充

    這裡就直接引用一張圖片來協助理解:(參考自Philip Roberts的演講《 Help, I'm stuck in an event-loop [1] 》)

    上圖大致描述就是:

    • 主執行緒執行時會產生執行棧,

    棧中的程式碼呼叫某些api時,它們會在事件佇列中新增各種事件(當滿足觸發條件後,如ajax請求完畢)

    • 而棧中的程式碼執行完畢,就會讀取事件佇列中的事件,去執行那些回撥

    • 如此迴圈

    • 注意,總是要等待棧中的程式碼執行完畢後才會去讀取事件佇列中的事件

    單獨說說定時器

    上述事件迴圈機制的核心是:JS引擎執行緒和事件觸發執行緒

    但事件上,裡面還有一些隱藏細節,譬如呼叫 setTimeout 後,是如何等待特定時間後才新增到事件佇列中的?

    是JS引擎檢測的麼?當然不是了。它是由 定時器執行緒 控制(因為JS引擎自己都忙不過來,根本無暇分身)

    為什麼要單獨的定時器執行緒?因為JavaScript引擎是單執行緒的, 如果處於阻塞執行緒狀態就會影響記計時的準確,因此很有必要單獨開一個執行緒用來計時。

    什麼時候會用到定時器執行緒? 當使用 setTimeoutsetInterval ,它需要定時器執行緒計時,計時完成後就會將特定的事件推入事件佇列中。

    譬如:

    setTimeout(function(){
    console.log('hello!');
    }, 1000);

    這段程式碼的作用是當 1000 毫秒計時完畢後(由定時器執行緒計時),將回調函式推入事件佇列中,等待主執行緒執行

    setTimeout(function(){
    console.log('hello!');
    }, 0);

    console.log('begin');

    這段程式碼的效果是最快的時間內將回調函式推入事件佇列中,等待主執行緒執行

    注意:

    • 執行結果是:先 beginhello!
    • 雖然程式碼的本意是0毫秒後就推入事件佇列,但是W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算為4ms。

    (不過也有一說是不同瀏覽器有不同的最小時間設定)

    • 就算不等待4ms,就算假設0毫秒就推入事件佇列,也會先執行 begin (因為只有可執行棧內空了後才會主動讀取事件佇列)

    setTimeout而不是setInterval

    用setTimeout模擬定期計時和直接用setInterval是有區別的。

    因為每次setTimeout計時到後就會去執行,然後執行一段時間後才會繼續setTimeout,中間就多了誤差 (誤差多少與程式碼執行時間有關)

    而setInterval則是每次都精確的隔一段時間推入一個事件 (但是,事件的實際執行時間不一定就準確,還有可能是這個事件還沒執行完畢,下一個事件就來了)

    而且setInterval有一些比較致命的問題就是:

    • 累計效應(上面提到的),如果setInterval程式碼在(setInterval)再次新增到佇列之前還沒有完成執行,

    就會導致定時器程式碼連續執行好幾次,而之間沒有間隔。就算正常間隔執行,多個setInterval的程式碼執行時間可能會比預期小(因為程式碼執行需要一定時間)

    • 譬如像iOS的webview,或者Safari等瀏覽器中都有一個特點, 在滾動的時候是不執行JS的 ,如果使用了setInterval,會發現在滾動結束後會執行多次由於滾動不執行JS積攢回撥,如果回撥執行時間過長,就會非常容器造成卡頓問題和一些不可知的錯誤 (這一塊後續有補充,setInterval自帶的優化,不會重複添加回調)

    • 而且把瀏覽器最小化顯示等操作時,setInterval並不是不執行程式,

    它會把setInterval的回撥函式放在佇列中,等瀏覽器視窗再次開啟時,一瞬間全部執行時

    所以,鑑於這麼多但問題,目前一般認為的最佳方案是: 用setTimeout模擬setInterval,或者特殊場合直接用requestAnimationFrame

    補充:JS高程中有提到,JS引擎會對setInterval進行優化,如果當前事件佇列中有setInterval的回撥,不會重複新增。不過,仍然是有很多問題。。。

    事件迴圈進階:macrotask與microtask

    這段參考了參考來源中的第2篇文章(英文版的),(加了下自己的理解重新描述了下), 強烈推薦有英文基礎的同學直接觀看原文,作者描述的很清晰,示例也很不錯,如下:

    https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/ [2]

    上文中將JS事件迴圈機制梳理了一遍,在ES5的情況是夠用了,但是在ES6盛行的現在,仍然會遇到一些問題,譬如下面這題:

    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');

    嗯哼,它的正確執行順序是這樣子的:

    script start
    script end
    promise1
    promise2
    setTimeout

    為什麼呢?因為Promise裡有了一個一個新的概念: microtask

    或者,進一步,JS中分為兩種任務型別:** macrotaskmicrotask **,在ECMAScript中,microtask稱為 jobs ,macrotask可稱為 task

    它們的定義?區別?簡單點可以按如下理解:

    • macrotask(又稱之為巨集任務),可以理解是每次執行棧執行的程式碼就是一個巨集任務(包括每次從事件佇列中獲取一個事件回撥並放到執行棧中執行)

      • 每一個task會從頭到尾將這個任務執行完畢,不會執行其它

      • 瀏覽器為了能夠使得JS內部task與DOM任務能夠有序的執行,會在一個task執行結束後,在下一個 task 執行開始前,對頁面進行重新渲染

    (`task->渲染->task->...`)
    • microtask(又稱為微任務),可以理解是在當前 task 執行結束後立即執行的任務

      • 也就是說,在當前task任務後,下一個task之前,在渲染之前

      • 所以它的響應速度相比setTimeout(setTimeout是task)會更快,因為無需等渲染

      • 也就是說,在某一個macrotask執行完後,就會將在它執行期間產生的所有microtask都執行完畢(在渲染前)

    分別很麼樣的場景會形成macrotask和microtask呢?

    • macrotask:主程式碼塊,setTimeout,setInterval等(可以看到,事件佇列中的每一個事件都是一個macrotask)

    • microtask:Promise,process.nextTick等

    __補充:在node環境下,process.nextTick的優先順序高於Promise__,也就是可以簡單理解為:在巨集任務結束後會先執行微任務佇列中的nextTickQueue部分,然後才會執行微任務中的Promise部分。

    參考:https://segmentfault.com/q/1010000011914016

    再根據執行緒來理解下:

    • macrotask中的事件都是放在一個事件佇列中的,而這個佇列由 事件觸發執行緒 維護

    • microtask中的所有微任務都是新增到微任務佇列(Job Queues)中,等待當前macrotask執行完畢後執行,而這個佇列由 JS引擎執行緒維護

    (這點由自己理解+推測得出,因為它是在主執行緒下無縫執行的)

    所以,總結下執行機制:

    • 執行一個巨集任務(棧中沒有就從事件佇列中獲取)

    • 執行過程中如果遇到微任務,就將它新增到微任務的任務佇列中

    • 巨集任務執行完畢後,立即執行當前微任務佇列中的所有微任務(依次執行)

    • 當前巨集任務執行完畢,開始檢查渲染,然後GUI執行緒接管渲染

    • 渲染完畢後,JS執行緒繼續接管,開始下一個巨集任務(從事件佇列中獲取)

    如圖:

    另外,請注意下 Promisepolyfill 與官方版本的區別:

    • 官方版本中,是標準的microtask形式

    • polyfill,一般都是通過setTimeout模擬的,所以是macrotask形式

    • 請特別注意這兩點區別

    注意,有一些瀏覽器執行結果不一樣(因為它們可能把microtask當成macrotask來執行了), 但是為了簡單,這裡不描述一些不標準的瀏覽器下的場景(但記住,有些瀏覽器可能並不標準)

    20180126補充:使用MutationObserver實現microtask

    MutationObserver可以用來實現microtask (它屬於microtask,優先順序小於Promise, 一般是Promise不支援時才會這樣做)

    它是HTML5中的新特性,作用是:監聽一個DOM變動, 當DOM物件樹發生任何變動時,Mutation Observer會得到通知

    像以前的Vue原始碼中就是利用它來模擬nextTick的, 具體原理是,建立一個TextNode並監聽內容變化, 然後要nextTick的時候去改一下這個節點的文字內容, 如下:(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)
    }

    對應Vue原始碼連結 [3]

    不過,現在的Vue(2.5+)的nextTick實現移除了MutationObserver的方式(據說是相容性原因), 取而代之的是使用MessageChannel (當然,預設情況仍然是Promise,不支援才相容的)。

    MessageChannel屬於巨集任務,優先順序是: MessageChannel->setTimeout , 所以Vue(2.5+)內部的nextTick與2.4及之前的實現是不一樣的,需要注意下。

    這裡不展開,可以看下 https://juejin.im/post/5a1af88f5188254a701ec230

    寫在最後的話

    看到這裡,不知道對JS的執行機制是不是更加理解了,從頭到尾梳理,而不是就某一個碎片化知識應該是會更清晰的吧?

    同時,也應該注意到了JS根本就沒有想象的那麼簡單,前端的知識也是無窮無盡,層出不窮的概念、N多易忘的知識點、各式各樣的框架、 底層原理方面也是可以無限的往下深挖,然後你就會發現,你知道的太少了。。。

    另外,本文也打算先告一段落,其它的,如JS詞法解析,可執行上下文以及VO等概念就不繼續在本文中寫了,後續可以考慮另開新的文章。

    最後,喜歡的話,就請給個贊吧!

    作者:撒網要見魚

    原文:https://segmentfault.com/a/1190000012925872

    參考資料

    [1]

    Help, I'm stuck in an event-loop: https://link.segmentfault.com/?enc=M7Ql%2BZOX8Vwc%2FkvIjcRthw%3D%3D.KbCEoH2v08qXotf2MgaLS9h9nKGrF0h9CdfFEOMZ7cE%3D

    [2]

    https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/: https://link.segmentfault.com/?enc=2V2JIk%2Bf5dUr4mX5EoZNMw%3D%3D.qXMBMW1IgFrJpoN6I%2FJnRlpW76hxcZveWUW72MZuNqOCRihWO6ApuVn4M7bKsNulIYUvVFXIXSXffuEHTmcmqsbxzOPtz%2Fob%2B5PEQ%2FI7ZIQ%3D

    [3]

    對應Vue原始碼連結: https://link.segmentfault.com/?enc=Xo%2BamvSRLYEWeBIG8K2qiQ%3D%3D.o75U0kLp4833wIuMuDUxX8N92Bww%2Fd5btjDBN2dJrtE109HxQuBqgX4GXPUiTccv%2Byoo7eX1%2B9ulmGaKX4QIUP4UFoMH16IDL9%2Fr%2FppPuEgA%2BJuQ0aA8QOSJIRQF2cURcjwOUwfbRT7AwVo7qf4R2w%3D%3D

    [4]

    https://www.cnblogs.com/lhb25/p/how-browsers-work.html: https://link.segmentfault.com/?enc=8%2FJG2tOnNnBUCpv7%2BcKh4Q%3D%3D.LnRKJZmN6RYS8vQTuNe7bOxTDLddlAvLNOvBb5Q1XuCiBxAoo1uvD7YQpEKklIWOqPpKA%2FBfQMnkWmaqDgqECQ%3D%3D

    [5]

    https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/: https://link.segmentfault.com/?enc=EQEnDnSqj5f9c5FKDMscVw%3D%3D.nlIQSwMqleZuHd9nWFzv7oR3g4IGViPICknFGa%2FMgLIlBMG%2B2S0qDOkxTyGjiZdQ4Z0QHuzDdBfgLpYJVGprDoCdEvCngJiICZ2ZwHDsegw%3D

    [6]

    https://segmentfault.com/p/1210000012780980: https://segmentfault.com/p/1210000012780980

    [7]

    http://blog.csdn.net/u013510838/article/details/55211033: https://link.segmentfault.com/?enc=XJRkFmWU%2BhXhzFeXJthWQg%3D%3D.UAwSH3kvCovJtDSF2bu1HtizA6UisbuGAwmh6yWJ4obVtGJtWPkN3o2mYcP8%2Fl6dw9lxJOR6ASbIg18d4vFLjg%3D%3D

    [8]

    http://blog.csdn.net/Steward2011/article/details/51319298: https://link.segmentfault.com/?enc=3yYqodUyJai368lw1ZZpUw%3D%3D.BguyKJeFzY3Vro83TVqG9Y8IQVjpaXlTHVy0oCdgvNLJastAQIiE0fPuBHW3MqiApSRGRmnbWKdXE4Ct19glwA%3D%3D

    [9]

    http://www.imweb.io/topic/58e3bfa845e5c13468f567d5: https://link.segmentfault.com/?enc=D0JF8WWE0KxGkLhmkVulZA%3D%3D.omShqyxpmecjF94gAxITn4Qj7pzDIJeFXuBcOc9k9bEWb7815M2jqxxK%2FCTKWbasAitS%2BqRIpAbNsdguC3diCg%3D%3D

    [10]

    https://segmentfault.com/a/1190000008015671: https://segmentfault.com/a/1190000008015671

    [11]

    https://juejin.im/post/5a4ed917f265da3e317df515: https://link.segmentfault.com/?enc=nfYwvwzcs2x%2BiYSyQKKOTA%3D%3D.jOGfm5FiljgaxW2tyyI%2FSTz684kEnsBgTSBgcPCfWarcRghlZVWZ5D%2B39HwUHCK0

    [12]

    http://www.cnblogs.com/iovec/p/7904416.html: https://link.segmentfault.com/?enc=Xb17ddlUVqFCkP96EUxZrg%3D%3D.3Ibg1IzSZrL9KAc6SJiHcgjVsVaIFRyZyLcXMaPkBhi3KFN0kr2T%2Bany0en3ES8K

    [13]

    https://www.cnblogs.com/wyaocn/p/5761163.html: https://link.segmentfault.com/?enc=0ENx%2BTnHDgToiuIoFXiTHw%3D%3D.A6Pou91TocsIR%2BNCoEVJ%2FG3wFY%2BAY22VvSxQAppUQJmoGJjc1XdRn8q3ELKX5JrT

    [14]

    http://www.ruanyifeng.com/blog/2014/10/event-loop.html#comment-text: https://link.segmentfault.com/?enc=aXmt5yu5y5bYpGuZQgw5Qg%3D%3D.fSK%2F8Nx9xzeXD7VTi0SKsk9cVDP4SPPqfQF%2FioG%2B4LlIQgPNP63Om2h57f1J%2BV4EZeiypkHyPi5SqNFlHwKUO3UlaP2%2FNDF8HduuopxkyCE%3D

    免費送書

    前端開發是需要終生學習的,JavaScript是極具活力的語言,也是一種常用常新的語言。講解JavaScript基礎的書雖然很多,但是,能夠與時俱進地介紹JavaScript新特性的書籍卻不多。這本書幾乎涵蓋了ES2015~ES2020的所有新特性,以及目前處在階段3的新特性,給出了豐富的例項,並經常將新特性與之前的語法特性進行對比。即使是對JavaScript不瞭解的同學,也可通過本書迅速上手,並對前端開發產生興趣和成就感。這是你成為JavaScript高手的必備書籍之一。

    參與規則 :留言區點贊前 名讀者即可中獎,另外再從其餘留言中隨機抽取 名幸運讀者。一共送 本!!

    開獎時間2022 年 5 月 15 日 22:00

    注意事項 :禁止作弊;提前加我微信好友,避免開獎後聯絡不到導致機會作廢

    :point_down::point_down::point_down: