首頁  >  文章  >  web前端  >  完全掌握JavaScript運作機制及原理

完全掌握JavaScript運作機制及原理

WBOY
WBOY轉載
2022-04-25 17:05:203047瀏覽

這篇文章為大家帶來了關於javascript的相關知識,其中主要介紹了關於JavaScript運行機制和原理的相關問題,包括了解析引擎等內容,下面一起來看一下,希望對大家有幫助。

完全掌握JavaScript運作機制及原理

【相關推薦:javascript影片教學web前端

寫js也有兩年多了,一直對它的運作機制和原理不是很了解,今天特意把大神們的理論和自己的總結都記錄到下面:

什麼是JavaScript解析引擎

簡單來說,JavaScript解析引擎就是能夠「讀懂」JavaScript程式碼,並且精確地給出程式碼執行結果的一段程式。

比方說,當你寫了 var a = 1 1; 這樣一段程式碼,JavaScript引擎做的事情就是看懂(解析)你這段程式碼,並且將a的值變成2。

學過編譯原理的人知道,對於靜態語言來說(如Java、C 、C),處理上述這些事情的叫編譯器(Compiler),相應地對於JavaScript這樣的動態語言則叫解譯器(Interpreter)。這兩者的差異用一句話來概括就是:編譯器是將原始碼編譯為另外一種程式碼(例如機器碼,或字節碼),而解釋器則是直接解析並將程式碼執行結果輸出。比方說,firebug的console就是一個JavaScript的解釋器。

但是,現在很難去界定說,JavaScript引擎它到底算是個解釋器還是個編譯器,因為,比如像V8(Chrome的JS引擎),它其實為了提高JS的運行性能,在運作之前會先將JS編譯成本地的機器碼(native machine code),然後再去執行機器碼(這樣速度就快很多)。

JavaScript解析引擎與ECMAScript是什麼關係

JavaScript引擎是一段程序,我們寫的JavaScript程式碼也是程序,如何讓程式去讀懂程式呢?這就需要定義規則。例如,之前提到的var a = 1 1;,它表示:

左邊var代表了這是申明(declaration),它宣告了a這個變數
右邊的表示要將1和1做加法
中間的等號表示了這是個賦值語句
最後的分號表示這句語句結束了
上述這些就是規則,有了它就等於有了衡量的標準,JavaScript引擎就可以根據這個標準去解析JavaScript程式碼了。那麼這裡的ECMAScript就是定義了這些規則。其中ECMAScript 262這份文檔,就是對JavaScript這門語言定義了一整套完整的標準。其中包括:

var,if,else,break,continue等是JavaScript的關鍵字
abstract,int,long等是JavaScript保留字
怎麼樣算是數字、怎麼樣算是字串等等
定義了運算子( ,-,>, 定義了JavaScript的語法
定義了對表達式,語句等標準的處理演算法,例如遇到==該如何處理
...
標準的JavaScript引擎就會根據這套文件去實現,注意這裡強調了標準,因為也有不按照標準來實現的,例如IE的JS引擎。這也是為什麼JavaScript會有相容性的問題。至於為什麼IE的JS引擎不按照標準來實現,就要說到瀏覽器大戰了,這裡就不贅述了,自行Google之。

所以,簡單的說,ECMAScript定義了語言的標準,JavaScript引擎根據它來實現,這就是兩者的關係。

JavaScript解析引擎與瀏覽器是什麼關係

簡單地說,JavaScript引擎是瀏覽器的組成部分之一。因為瀏覽器還要做很多別的事情,像是解析頁面、渲染頁面、Cookie管理、記錄等等。那麼,既然是組成部分,因此一般情況下JavaScript引擎都是瀏覽器開發人員自行開發的。例如:IE9的Chakra、Firefox的TraceMonkey、Chrome的V8等等。

因此也看出,不同瀏覽器都採用了不同的JavaScript引擎。因此,我們只能說要深入了解哪個JavaScript引擎。

為什麼JavaScript是單線程

JavaScript語言的一大特點就是單線程,也就是說,同一時間只能做一件事。那麼,為什麼JavaScript不能有多個執行緒呢?這樣能提高效率啊。

JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與使用者互動,以及操作DOM。這決定了它只能是單線程,否則會帶來複雜的同步問題。例如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,而這時瀏覽器應該以哪個線程為準?

所以,為了避免複雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,將來也不會改變。

為了利用多核心CPU的運算能力,HTML5提出Web Worker標準,允許JavaScript腳本建立多個線程,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質。

區分行程和執行緒

行程是cpu資源分配的最小單位,行程可以包含多個執行緒。瀏覽器就是多進程的,每開啟的一個瀏覽器視窗就是一個進程。

執行緒是cpu調度的最小單位,同一行程下的各個執行緒之間共享程式的記憶體空間。

可以把進程看做一個倉庫,線程是可以運輸的貨車,每個倉庫有屬於自己的多輛貨車為倉庫服務(運貨),每個倉庫可以同時由多輛車同時拉貨,但是每輛車同一時間只能做一件事,就是運輸本次的貨物。

核心點:

程式是cpu 資源分配的最小單位(是能擁有資源和獨立運作的最小單位)

#執行緒是cpu調度的最小單位(線程是建立在進程的基礎上的一次程序運行單位,一個進程中可以有多個線程)

不同進程之間也可以通信,不過代價較大。
瀏覽器是多進程的
理解了進程與線程了區別後,接下來對瀏覽器進行一定程度上的認識:(先看下簡化理解)

瀏覽器是多進程的

瀏覽器之所以能夠運行,是因為系統給它的進程分配了資源(cpu、內存)

簡單點理解,每打開一個Tab頁,就相當於創建了一個獨立的瀏覽器進程。

以Chrome 為例,它的多個標籤頁,然後可以在Chrome的任務管理器中看到有多個進程(分別是每一個Tab 頁面有一個獨立的進程,以及一個主進程),在Windows 的任務管理器中也可以看出。

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

瀏覽器都包含哪些進程

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

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

  • 負責瀏覽器介面顯示,與使用者互動。如前進,後退等
  • 負責各個頁面的管理,創建和銷毀其他進程
  • 將Renderer 進程得到的內存中的Bitmap,繪製到用戶界面上
  • 網絡資源的管理,下載等

(2)第三方插件進程:每種類型的插件對應一個進程,僅當使用該插件時才創建
(3)GPU 進程:最多一個,用於3D 繪製等
(4)瀏覽器渲染進程(瀏覽器內核,Renderer進程,內部是多線程的):預設每個Tab 頁面一個進程,互不影響。主要作用為:頁面渲染,腳本執行,事件處理等

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

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

瀏覽器多進程的優勢

相較於單一進程瀏覽器,多進程有以下優點:

  • 避免單一page crash 影響整個瀏覽器
  • 避免第三方外掛程式crash 影響整個瀏覽器
  • 多進程充分利用多核心優勢
  • 方便使用沙盒模型隔離外掛程式等進程,提高瀏覽器穩定性

簡單理解:

如果瀏覽器是單進程,那麼某個Tab 頁崩潰了,就影響了整個瀏覽器,體驗有多差;同理如果是單進程,插件崩潰了也會影響整個瀏覽器。

當然,記憶體等資源消耗也會更大,有點空間換時間的意思。再大的記憶體也不夠 Chrome 吃的,記憶體洩漏問題現在已經改善了一些了,也僅僅是改善,還有就是會導致耗電增加。

瀏覽器核心(渲染進程)

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

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

請牢記,瀏覽器的渲染進程是多線程的(JS 引擎是單線程的)

那麼接下來看看它都包含了哪些線程(列舉一些主要常駐線程):

1、GUI 渲染執行緒

  • 負責渲染瀏覽器介面,解析HTML,CSS,建構DOM 樹和RenderObject 樹(簡單理解為CSS 形成的樣式樹,Flutter 核心之一),佈局和繪製等。
  • 當介面需要重繪(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 引擎的處理
  • 注意,由於JS的單線程關係,所以這些待處理佇列中的事件都得排隊等待JS 引擎處理(當JS 引擎空閒時才會去執行)
  • 這裡可以簡單理解為,它負責管理一堆事件和一個“事件佇列”,只有在事件佇列的任務JS 引擎才會在空閒的時候去執行,而它要做的,就是負責當某個事件被觸發時,把它加入到事件佇列。例如滑鼠單擊。

4、定時觸發器執行緒

  • 傳說中的setInterval 與setTimeout 所在的執行緒
  • 瀏覽器定時計數器並不是由JavaScript 引擎計數的,(因為JavaScript 引擎是單線程的,如果處於阻塞線程狀態就會影響記計時的準確)
  • #因此通過單獨線程來計時並觸發定時,計時完畢後,添加到事件隊列中(對應事件觸發線程的「事件符合觸發條件被觸發時」),等待JS 引擎空閒後執行。
  • 注意,W3C 在 HTML 標準中規定,規定要求 setTimeout 中低於 4ms 的時間間隔算為 4ms。

5、非同步http請求執行緒

  • 在XMLHttpRequest 在連線後是透過瀏覽器新開一個執行緒請求
  • 將偵測到狀態變更時,如果設定有回呼函數,非同步執行緒就會產生狀態變更事件,將這個回呼再放入事件佇列中。再由 JavaScript 引擎執行。

Browser進程和瀏覽器核心(Renderer進程)的通訊過程

#看到這裡,首先,應該對瀏覽器內的進程和執行緒都有一定理解了,那麼接下來,再談談瀏覽器的Browser 進程(控制進程)是如何和核心通訊的, 這點也理解後,就可以將這部分的知識串聯起來,從頭到尾有一個完整的概念。

如果自己開啟任務管理器,然後開啟一個瀏覽器,就可以看到:工作管理員中出現了兩個進程(一個是主控進程,一個則是開啟Tab 頁的渲染進程) , 然後在這前提下,看下整個的過程:(簡化了很多)

  • Browser 進程收到用戶請求,首先需要獲取頁面內容(譬如透過網路下載資源),隨後將該任務透過RendererHost 介面傳遞給Render (核心)進程
  • Renderer 進程的Renderer 介面收到訊息,簡單解釋後,交給渲染線程,然後開始渲染


#渲染線程接收請求,加載網頁並渲染網頁,這其中可能需要Browser 進程獲取資源和需要GPU 進程來幫助渲染完全掌握JavaScript運作機制及原理

當然可能會有JS 線程操作DOM(這樣可能會造成回流並重繪)

最後Render 進程將結果傳遞給Browser 進程

Browser 進程接收到結果並將結果繪製出來#########這裡繪一張簡單的圖:(很簡化)###############瀏覽器內核中執行緒之間的關係########到了這裡,已經對瀏覽器的運作有了一個整體的概念,接下來,先簡單整理一些概念###

GUI 渲染執行緒與JS 引擎執行緒互斥

由於JavaScript 是可操縱DOM 的,如果在修改這些元素屬性同時渲染介面(即JS 執行緒和UI 執行緒同時執行),那麼渲染執行緒前後獲得的元素數據就可能不一致了。

因此為了防止渲染出現不可預期的結果,瀏覽器設定GUI 渲染執行緒與JS 引擎為互斥的關係,當JS 引擎執行時GUI 執行緒會被掛起, GUI 更新則會被儲存在一個佇列中等到JS 引擎執行緒空閒時立即被執行。

JS阻塞頁面載入

從上述的互斥關係,可以推導出,JS 如果執行時間過長就會阻塞頁面。

譬如,假設 JS 引擎正在進行巨量的計算,此時就算 GUI 有更新,也會被儲存到佇列中,等待 JS 引擎空閒後執行。然後,由於巨量計算,所以 JS 引擎很可能很久很久後才能空閒,自然會感覺到巨卡無比。

所以,要盡量避免 JS 執行時間過長,這樣就會造成頁面的渲染不連貫,導致頁面渲染載入阻塞的感覺。

解決這種問題,除了將運算放在後端,如果避不開,並且巨量計算還和UI 有關係,那麼我的思路就是使用setTimeout 將任務分割,中間給出一點空閒時間讓JS 引擎去處理下UI,不至於頁面直接卡死。

如果直接決定最低要求 HTML5 的版本,那麼可以看看下面的 WebWorker。

WebWorker, JS的多執行緒

前文有提到JS 引擎是單執行緒的,而且JS 執行時間過長會阻塞頁面,那麼JS 就真的對cpu 密集型計算無能為力麼?

所以,後來 HTML5 中支援了Web Worker。

MDN 的官方解釋是:

Web Worker 為 Web 內容在後台執行緒中執行腳本提供了一種簡單的方法。

執行緒可以執行任務而不干擾使用者介面,一個worker 是使用一個建構函式建立的一個物件(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 進程下的一個執行緒。

瀏覽器渲染流程

補充下瀏覽器的渲染流程(簡單版本)

為了簡化理解,前期工作直接省略成:

瀏覽器輸入url,瀏覽器主進程接管,開一個下載線程,然後進行http 請求(略去DNS 查詢,IP 尋址等等操作),然後等待響應,獲取內容,隨後將內容通過RendererHost 接口轉交給Renderer 進程
瀏覽器渲染流程開始
瀏覽器器核心拿到內容後,渲染大概可以分割成以下幾個步驟:

解析html 建立dom 樹
解析css 建構render 樹(將CSS 程式碼解析成樹形的資料結構,然後結合DOM 合併成render 樹)
佈局render 樹(Layout/reflow),負責各元素尺寸、位置的計算
繪製render 樹(paint),繪製頁面像素資訊
瀏覽器會將各層的資訊傳送給GPU,GPU 會將各層合成(composite),顯示在螢幕上。
所有詳細步驟都已經略去,渲染完畢後就是 load 事件了,之後就是自己的 JS 邏輯處理了。

既然略去了一些詳細的步驟,那麼就提一些可能需要注意的細節把。
完全掌握JavaScript運作機制及原理

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 DevTools --> More Tools --> Rendering --> Layer borders中看到,黃色的就是複合圖層資訊

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

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

最常用的方式:translate3d、translateZ
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 也會被隱式轉為一個複合圖層,這點需要特別注意。

從EventLoop談JS的運作機制

到此時,已經是屬於瀏覽器頁面初次渲染完畢後的事情,JS 引擎的一些運作機制分析。

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

讀這部分的前提是已經知道了JS 引擎是單線程,而且這裡會用到上文中的幾個概念:

  • JS 引擎線程
  • 事件觸發執行緒
  • 定時觸發器執行緒
    然後再理解一個概念:

JS 分成同步任務和非同步任務

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

看圖:
完全掌握JavaScript運作機制及原理

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

事件循環機制進一步補充

完全掌握JavaScript運作機制及原理

上圖大致描述就是:

  • 主執行緒運行時會產生執行棧,當堆疊中的程式碼呼叫某些api 時,它們會在事件佇列中新增各種事件(當滿足觸發條件後,如ajax 請求完畢)
  • #而堆疊中的程式碼執行完畢,就會讀取事件佇列中的事件,去執行那些回調,如此循環

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

單獨說說定時器

上述事件循環機制的核心是:JS 引擎線程和事件觸發線程

但事件上,裡面還有一些隱藏細節,譬如調用setTimeout 後,是如何等待特定時間後才加入事件佇列中的?

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

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

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

setTimeout而不是setInterval

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

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

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

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

累積效應,如果 setInterval 程式碼在再次加入佇列之前還沒有完成執行, 就會導致計時器程式碼連續運行好幾次,而之間沒有間隔。就算正常間隔執行,多個setInterval 的程式碼執行時間可能會比預期小(因為程式碼執行需要一定時間)
譬如像iOS 的webview,或者Safari 等瀏覽器中都有一個特點,在滾動的時候是不執行JS的,如果使用了setInterval,會發現在滾動結束後會執行多次由於滾動不執行JS 積攢回調,如果回調執行時間過長,就會非常容器造成卡頓問題和一些不可知的錯誤(這一塊後續有補充,setInterval 自帶的優化,不會重複加回調)
而且把瀏覽器最小化顯示等操作時,setInterval 並不是不執行程序, 它會把setInterval 的回調函數放在佇列中,當瀏覽器視窗再次開啟時,一瞬間全部執行
所以,鑑於這麼多問題,目前一般認為的最佳方案是:用setTimeout 模擬setInterval,或者特殊場合直接用requestAnimationFrame

#補充:JS 高程有提到,JS 引擎會對setInterval 進行最佳化,如果目前事件佇列中有setInterval 的回調,不會重複新增。

事件循環進階:macrotask與microtask

#上文中將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 中分為兩種任務類型:macrotask 和 microtask,在 ECMAScript 中,microtask 稱為 jobs,macrotask 可稱為task。

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

1、macrotask(又稱之為巨集任務)

#可以理解是每次執行堆疊執行的程式碼就是一個巨集任務(包括每次從事件佇列中取得一個事件回呼並放到執行棧中執行)

  • #每一個task 會從頭到尾將這個任務執行完畢,不會執行其它
  • 瀏覽器為了能夠使得JS 內部task 與DOM 任務能夠有序的執行,會在一個task 執行結束後,在下一個task 執行開始前,對頁面進行重新渲染(task -> 渲染-> task -> …)
2、microtask(又稱微任務)

可以理解是在目前task 執行結束後立即執行的任務

  • 也就是說,在當前task 任務後,下一個task 之前,在渲染之前
  • 所以它的響應速度相比setTimeout(setTimeout 是task)會更快,因為無需等待渲染
  • 也就是說,在某一個macrotask 執行完後,就會將在​​它執行期間產生的所有microtask 都執行完畢(在渲染前)
3、分別什麼樣的場景會形成macrotask 和microtask 呢
  • macrotask:主程式碼區塊,setTimeout,setInterval 等(事件佇列中的每一個事件都是一個macrotask)
  • microtask:Promise,process.nextTick等

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

再根據執行緒來理解下:

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

(2)microtask 中的所有微任務都是添加到微任務隊列(Job Queues)中,等待當前macrotask 執行完畢後執行,而這個隊列由JS 引擎線程維護(這點由自己理解推測得出,因為它是在主執行緒下無縫執行的)
所以,總結下運行機制:

  • 執行一個巨集任務(堆疊中沒有就從事件佇列中取得)
  • #執行過程中如果遇到微任務,就將它加入微任務的任務佇列中
  • 巨集任務執行完畢後,立即執行目前微任務佇列中的所有微任務(依序執行)
  • 目前巨集任務執行完畢,開始檢查渲染,然後GUI執行緒接管渲染
  • 渲染完畢後,JS執行緒繼續接管,開始下一個巨集任務(從事件佇列中取得)

如圖:
完全掌握JavaScript運作機制及原理

另外,請注意下Promise 的polyfill 與官方版本的差異:在

官方版本中,是標準的microtask 形式
polyfill,一般都是透過setTimeout 模擬的,所以是macrotask 形式
注意,有一些瀏覽器執行結果不一樣(因為它們可能把microtask 當成macrotask 來執行了), 但是為了簡單,這裡不描述一些不標準的瀏覽器下的場景(但記住,有些瀏覽器可能並不標準)

补充:使用MutationObserver实现microtask

MutationObserver可以用来实现microtask (它属于microtask,优先级小于Promise, 一般是Promise不支持时才会这样做)

它是HTML5中的新特性,作用是:监听一个DOM变动, 当DOM对象树发生任何变动时,Mutation Observer会得到通知

像以前的Vue源码中就是利用它来模拟nextTick的, 具体原理是,创建一个TextNode并监听内容变化, 然后要nextTick的时候去改一下这个节点的文本内容, 如下:

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(2.5+)的nextTick实现移除了MutationObserver的方式(据说是兼容性原因), 取而代之的是使用MessageChannel (当然,默认情况仍然是Promise,不支持才兼容的)。

MessageChannel属于宏任务,优先级是:MessageChannel->setTimeout, 所以Vue(2.5+)内部的nextTick与2.4及之前的实现是不一样的,需要注意下。

【相关推荐:javascript视频教程web前端

以上是完全掌握JavaScript運作機制及原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:csdn.net。如有侵權,請聯絡admin@php.cn刪除