1. 程式人生 > >JS 引擎執行機制講解

JS 引擎執行機制講解

SpiderMonkey Brendan Eich 網景

主流瀏覽器 核心 js引擎
IE -> Edge trident->EdgeHTML JScript(IE3.0-IE8.0) / Chakra(IE9+之後,查克拉,微軟也看火影麼…)
Chrome webkit->blink V8(大名鼎鼎)
Firefox Gecko SpiderMonkey(1.0-3.0)/ TraceMonkey(3.5-3.6)/ JaegerMonkey(4.0-
Safari webkit Nitro(4-)
Opera Presto->blink Linear A(4.0-6.1)/ Linear B(7.0-9.2)/ Futhark(9.5-10.2)/ Carakan(10.5-)

讀到這片文章,相信大家已經清楚 JS 引擎是單執行緒,開題先給大家來個靈魂四問:

Q0:瀏覽器核心和 js 引擎的關係?
Q1:JS 引擎為什麼是單執行緒的?
Q2:為什麼需要非同步?
Q3:單執行緒又是如何實現非同步的呢?

A0: 瀏覽器核心名字有很多,渲染引擎排版引擎解釋引擎,英文(Rendering Engine) ,在早期核心也是包含 js 引擎的,而現在 js 引擎越來越獨立了,可以把它單獨提出來,所以,我們所說的核心更偏向於指渲染引擎
**A1:**假設 JS 引擎是多執行緒的。那麼我們現在有 2 個程序, process1 和 process2,如果它們對同一個 dom 同時進行操作。其中 process1 刪除了該 dom ,而 process2 編輯了該 dom ,同時下達 2 個矛盾的命令,瀏覽器究竟該如何執行呢? 這樣想的話,JS 引擎被被設計成單執行緒應該就容易理解了吧。

**A2:**如果 JS 中不存在非同步,只能自上而下執行,如果上一行解析時間很長,那麼下面的程式碼就會被阻塞。對於使用者而言,阻塞就意味著"卡死",這樣就導致了很差的使用者體驗

**A3:**通過事件迴圈(event loop)

下邊我們就通過 Event Loop 來看看 JS 引擎的執行機制。

這裡我們還需要回顧其他幾個名詞 :JS 引擎執行緒事件觸發執行緒定時觸發執行緒

JS 分為同步任務和非同步任務,同步任務在主執行緒上執行,形成一個執行棧;事件觸發執行緒管理著一個任務佇列,只要非同步任務有了執行結果,就在任務佇列中放置一個事件,一旦執行棧裡中的同步任務執行完畢,系統就會讀取任務佇列,將可執行的非同步任務新增到可執行棧中,開始執行。
在這裡插入圖片描述


看到這裡,我們會明白一件事,那就是為什麼有時候 setTimeOut 推入佇列裡的事件執行時間不準確。原因便是推入的事件被推入佇列的時候,js 引擎執行緒比較繁忙,沒有立即執行,所以有誤差。

下邊通過圖片輔助理解,對事件迴圈的進一步補充:
在這裡插入圖片描述
上圖的大致描述:

  • 主執行緒在執行時產生執行棧,棧中的程式碼呼叫 API 時,會往任務佇列裡新增各種事件(當滿足觸發條件後推入任務佇列,如 ajax 請求完成)
  • 棧中的程式碼執行完畢,就會讀取任務佇列中的事件,去執行那些回撥,如此迴圈

定時器

定時器執行緒

  • 為什麼要單獨的定時器執行緒?

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

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

來個例子幫助大家理解一下:

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

console.log('Hi');

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

Hi
setTimeOut

驚訝吧!此處,雖然程式碼的本意是 0 毫秒後就推入事件佇列,但是 W3C 在 HTML 標準中規定,規定要求setTimeout中低於 4ms 的時間間隔算為4ms。再退一步講,即使不用等待 4ms 結果依然如此。因為在 JS 引擎執行緒執行空閒時才會執去行被定時器推入到事件佇列中的回撥函式。

setTimeOut 和 setInterval 用來計時的區別

  • setTimeOut 計時完成,執行回撥函式候,才會進行下一次計時,這中間就會多出執行回撥函式所用的時間。所以計時會有誤差。
  • setInterval 雖然會每次都精確的隔一段時間把回撥函式推入執行佇列一次,但是執行回撥函式的時間如果大於間隔時間,那麼事件隊裡就會累積同一定時器的推入的事件,這樣就會導致程式碼執行好幾次,而沒有時間間隔;即使正常時間間隔執行,多個setInterval的程式碼執行時間可能會比預期小(因為程式碼執行需要一定時間)。甚至有更致命的問題:當把瀏覽器最小化顯示等操作時,setInterval並不是不執行程式,它會把setInterval的回撥函式放在佇列中,等瀏覽器視窗再次開啟時,一瞬間全部執行。

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

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

es6 中對事件迴圈的重新考量

話不多說,直接看圖上例子:
在這裡插入圖片描述

setTimeout(function(){
  console.log("1:setTimeOut")
})
new Promise(function(resolve){
  console.log("2:馬上執行 for 迴圈")
  for (var i = 0; i < 1000; i++){
    i == 99 && resolve();
  }
}).then(function(){
  console.log('3:Execute then function')
})
console.log('4:end of the code')

首先執行 script 下的巨集任務,遇到 setTimeout ,將其放到巨集任務的【佇列】裡
遇到 new Promise 直接執行,列印**“2:馬上執行for迴圈啦”**
遇到 then 方法,是微任務,將其放到微任務的【佇列裡】
列印 "4:end of the code"
本輪巨集任務執行完畢,檢視本輪的微任務,發現有一個then方法裡的函式, 列印**“3:Execute then function”**
到此,本輪的 event loop 全部完成。
下一輪的迴圈裡,先執行一個巨集任務,發現巨集任務的【佇列】裡有一個 setTimeout裡的函式,執行列印**“1:setTimeOut”**

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(巨集任務)script(整體程式碼) setTimeout setInterval setImmediate I/O UI rendering
    可以理解是每次執行棧執行的程式碼就是一個巨集任務(包括每次從事件佇列中獲取一個事件回撥並放到執行棧中執行)

    • 每一個 macrotask 會從頭到尾將這個任務執行完畢,不會執行其它.
    • 瀏覽器為了能夠使得 JS 內部 macrotask 與 DOM 任務能夠有序的執行,會在一個 macrotask 執行結束後,在下一個 macrotask 執行開始前,對頁面進行重新渲染,即macrotask -> 渲染 -> macrotask -> ····
    • 使用場景:主程式碼塊,setTimeout,setInterval等(可以看到,事件佇列中的每一個事件都是一個 macrotask)
  • microtask(微任務)process.nextTick Promises Object.observe MutationObserver
    可以理解是在當前 macrotask 執行結束後立即執行的任務

    • 也就是說,在當前 macrotask 任務後,下一個 macrotask 之前,在渲染之前
    • 所以它的響應速度相比 setTimeout( setTimeout 是巨集任務)會更快,因為無需等渲染。也就是說,某一個macrotask執行完後,就會將在它執行期間產生的所有microtask都執行完畢
    • 使用場景:Promise,process.nextTick等

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

從執行緒的角度理解一下,巨集任務和微任務:

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

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

總結

  • 執行一個巨集任務(棧中沒有就從事件佇列中獲取)
  • 執行過程中如果遇到微任務,就將它新增到微任務的任務佇列中
  • 巨集任務執行完畢後,立即執行當前微任務佇列中的所有微任務(依次執行)
  • 當前巨集任務執行完畢,開始檢查渲染,然後GUI執行緒接管渲染
  • 渲染完畢後,JS執行緒繼續接管,開始下一個巨集任務(從事件佇列中獲取)