【JS】js引擎執行過程

概述

js引擎執行過程主要分爲三個階段,分別是語法分析,預編譯和執行階段,上篇文章咱們介紹了語法分析和預編譯階段,那麼咱們先作個簡單歸納,以下:html

  • 語法分析: 分別對加載完成的代碼塊進行語法檢驗,語法正確則進入預編譯階段;不正確則中止該代碼塊的執行,查找下一個代碼塊並進行加載,加載完成再次進入該代碼塊的語法分析階段node

  • 預編譯:經過語法分析階段後,進入預編譯階段,則建立變量對象(建立arguments對象(函數運行環境下),函數聲明提早解析,變量聲明提高),肯定做用域鏈以及this指向。git

如還有疑問回頭看看上篇文章js引擎的執行過程(一)


本文主要分析js引擎執行的第三個階段–執行階段,在分析以前咱們先思考如下兩個問題:es6

js是單線程的,爲了不代碼解析阻塞使用了異步執行,那麼它的異步執行機制是怎麼樣的?github

經過事件循環(Event Loop),理解了事件循環的原理就理解了js的異步執行機制,本文主要介紹。promise

js是單線程的,那麼是否表明參與js執行過程的線程就只有一個?瀏覽器

不是的,會有四個線程參與該過程,可是永遠只有JS引擎線程在執行JS腳本程序,其餘的三個線程只協助,不參與代碼解析與執行。參與js執行過程的線程分別是:數據結構

  • JS引擎線程: 也稱爲JS內核,負責解析執行Javascript腳本程序的主線程(例如V8引擎)併發

  • 事件觸發線程: 歸屬於瀏覽器內核進程,不受JS引擎線程控制。主要用於控制事件(例如鼠標,鍵盤等事件),當該事件被觸發時候,事件觸發線程就會把該事件的處理函數推動事件隊列,等待JS引擎線程執行異步

  • 定時器觸發線程:主要控制計時器setInterval和延時器setTimeout,用於定時器的計時,計時完畢,知足定時器的觸發條件,則將定時器的處理函數推動事件隊列中,等待JS引擎線程執行。
    注:W3C在HTML標準中規定setTimeout低於4ms的時間間隔算爲4ms。

  • HTTP異步請求線程:經過XMLHttpRequest鏈接後,經過瀏覽器新開的一個線程,監控readyState狀態變動時,若是設置了該狀態的回調函數,則將該狀態的處理函數推動事件隊列中,等待JS引擎線程執行。
    注:瀏覽器對通一域名請求的併發鏈接數是有限制的,Chrome和Firefox限制數爲6個,ie8則爲10個。

總結:永遠只有JS引擎線程在執行JS腳本程序,其餘三個線程只負責將知足觸發條件的處理函數推動事件隊列,等待JS引擎線程執行。

 

執行階段

咱們先分析一個典型的例子(來自Tasks, microtasks, queues and schedules,建議英文基礎好的閱讀,很是不錯的文章):

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

 

這裏我直接劃分例子的代碼結構,簡單描述分析執行過程,暫不解釋該過程當中的概念和原理,概念和原理將會在下面具體講解以下:

  1. 宏任務(macro-task),宏任務又按執行順序分爲同步任務和異步任務

    • 同步任務

      console.log('script start');

      console.log('script end');
    • 異步任務

      setTimeout(function() {
      console.log('setTimeout');
      }, 0);
  2. 微任務(micro-task)


    Promise.resolve().then(function() {
    console.log('promise1');
    }).then(function() {
    console.log('promise2');
    });

在JS引擎執行過程當中,進入執行階段後,代碼的執行順序以下:

宏任務(同步任務) --> 微任務 --> 宏任務(異步任務)

 

輸出結果爲:

script start
script end
promise1
promise2
setTimeout

 

進入ES6或Node環境中,JS的任務分爲兩種,分別是宏任務(macro-task)微任務(micro-task),在最新的ECMAScript中,微任務稱爲jobs,宏任務稱爲task,他們的執行順序如上。可能不少人對上面的分析並不理解,那麼咱們接下來繼續對上面例子進行詳細分析。

 

宏任務

宏任務(macro-task)可分爲同步任務異步任務

  • 同步任務指的是在JS引擎主線程上按順序執行的任務,只有前一個任務執行完畢後,才能執行後一個任務,造成一個執行棧(函數調用棧)。

  • 異步任務指的是不直接進入JS引擎主線程,而是知足觸發條件時,相關的線程將該異步任務推動任務隊列(task queue),等待JS引擎主線程上的任務執行完畢,空閒時讀取執行的任務,例如異步Ajax,DOM事件,setTimeout等。

理解宏任務中同步任務和異步任務的執行順序,那麼就至關於理解了JS異步執行機制–事件循環(Event Loop)

 

事件循環

事件循環能夠理解成由三部分組成,分別是:

  • 主線程執行棧

  • 異步任務等待觸發

  • 任務隊列

任務隊列(task queue)就是以隊列的數據結構對事件任務進行管理,特色是先進先出,後進後出


這裏直接引用一張著名的圖片(參考自Philip Roberts的演講《Help, I’m stuck in an event-loop》),幫助咱們理解,以下:
Event Loop

在JS引擎主線程執行過程當中:

  • 首先執行宏任務的同步任務,在主線程上造成一個執行棧,可理解爲函數調用棧;

  • 當執行棧中的函數調用到一些異步執行的API(例如異步Ajax,DOM事件,setTimeout等API),則會開啓對應的線程(Http異步請求線程,事件觸發線程和定時器觸發線程)進行監控和控制

  • 當異步任務的事件知足觸發條件時,對應的線程則會把該事件的處理函數推動任務隊列(task queue)中,等待主線程讀取執行

  • 當JS引擎主線程上的任務執行完畢,則會讀取任務隊列中的事件,將任務隊列中的事件任務推動主線程中,按任務隊列順序執行

  • 當JS引擎主線程上的任務執行完畢後,則會再次讀取任務隊列中的事件任務,如此循環,這就是事件循環(Event Loop)的過程

若是仍是不能理解,那麼咱們再次拿上面的例子進行詳細分析,該例子中宏任務的代碼部分是:

console.log('script start');

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

console.log('script end');

 

代碼執行過程以下:

  1. JS引擎主線程按代碼順序執行,當執行到console.log('script start');,JS引擎主線程認爲該任務是同步任務,因此馬上執行輸出script start,而後繼續向下執行;

  2. JS引擎主線程執行到setTimeout(function() { console.log('setTimeout'); }, 0);,JS引擎主線程認爲setTimeout是異步任務API,則向瀏覽器內核進程申請開啓定時器線程進行計時和控制該setTimeout任務。因爲W3C在HTML標準中規定setTimeout低於4ms的時間間隔算爲4ms,那麼當計時到4ms時,定時器線程就把該回調處理函數推動任務隊列中等待主線程執行,而後JS引擎主線程繼續向下執行

  3. JS引擎主線程執行到console.log('script end');,JS引擎主線程認爲該任務是同步任務,因此馬上執行輸出script end

  4. JS引擎主線程上的任務執行完畢(輸出script start和script end)後,主線程空閒,則開始讀取任務隊列中的事件任務,將該任務隊裏的事件任務推動主線程中,按任務隊列順序執行,最終輸出setTimeout,因此輸出的結果順序爲script start script end setTimeout

以上即是JS引擎執行宏任務的整個過程。

理解該過程後,咱們作一些拓展性的思考:

咱們都知道setTimeout和setInterval是異步任務的定時器,須要添加到任務隊列等待主線程執行,那麼使用setTimeout模擬實現setInterval,會有區別嗎?

答案是有區別的,咱們不妨思考一下:

  • setTimeout實現setInterval只能經過遞歸調用

  • setTimeout是在到了指定時間的時候就把事件推到任務隊列中,只有當在任務隊列中的setTimeout事件被主線程執行後,纔會繼續再次在到了指定時間的時候把事件推到任務隊列,那麼setTimeout的事件執行確定比指定的時間要久,具體相差多少跟代碼執行時間有關

  • setInterval則是每次都精確的隔一段時間就向任務隊列推入一個事件,不管上一個setInterval事件是否已經執行,因此有可能存在setInterval的事件任務累積,致使setInterval的代碼重複連續執行屢次,影響頁面性能。

綜合以上的分析,使用setTimeout實現計時功能是比setInterval性能更好的。固然若是不須要兼容低版本的IE瀏覽器,使用requestAnimationFrame是更好的選擇。

咱們繼續再作進一步的思考,以下:

高頻率觸發的事件(例如滾動事件)觸發頻率太高會影響頁面性能,甚至形成頁面卡頓,咱們是否能夠利用計時器的原理進行優化呢?

是能夠的,咱們能夠利用setTimeout實現計時器的原理,對高頻觸發的事件進行優化,實現點在於將多個觸發事件合併成一個,這就是防抖節流,本文先不作具體講解,你們能夠自行研究,有機會我再另開文章分析。

 

微任務

微任務是在es6和node環境中出現的一個任務類型,若是不考慮es6和node環境的話,咱們只須要理解宏任務事件循環的執行過程就已經足夠了,可是到了es6和node環境,咱們就須要理解微任務的執行順序了。
微任務(micro-task)的API主要有:Promise, process.nextTick

這裏咱們直接引用一張流程圖幫助咱們理解,以下:
task

在宏任務中執行的任務有兩種,分別是同步任務異步任務,由於異步任務會在知足觸發條件時纔會推動任務隊列(task queue),而後等待主線程上的任務執行完畢,再讀取任務隊列中的任務事件,最後推動主線程執行,因此這裏將異步任務即任務隊列看做是新的宏任務。執行的過程如上圖所示:

  1. 執行宏任務中同步任務,執行結束;

  2. 檢查是否存在可執行的微任務,有的話執行全部微任務,而後讀取任務隊列的任務事件,推動主線程造成新的宏任務;沒有的話則讀取任務隊列的任務事件,推動主線程造成新的宏任務

  3. 執行新宏任務的事件任務,再檢查是否存在可執行的微任務,如此不斷的重複循環

這就是加入微任務後的詳細事件循環,若是尚未理解,那麼們對一開始的例子作一個全面的分析,以下:

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

 

執行過程以下:

  1. 代碼塊經過語法分析和預編譯後,進入執行階段,當JS引擎主線程執行到console.log('script start');,JS引擎主線程認爲該任務是同步任務,因此馬上執行輸出script start,而後繼續向下執行;

  2. JS引擎主線程執行到setTimeout(function() { console.log('setTimeout'); }, 0);,JS引擎主線程認爲setTimeout是異步任務API,則向瀏覽器內核進程申請開啓定時器線程進行計時和控制該setTimeout任務。因爲W3C在HTML標準中規定setTimeout低於4ms的時間間隔算爲4ms,那麼當計時到4ms時,定時器線程就把該回調處理函數推動任務隊列中等待主線程執行,而後JS引擎主線程繼續向下執行

  3. JS引擎主線程執行到Promise.resolve().then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); });,JS引擎主線程認爲Promise是一個微任務,這把該任務劃分爲微任務,等待執行

  4. JS引擎主線程執行到console.log('script end');,JS引擎主線程認爲該任務是同步任務,因此馬上執行輸出script end

  5. 主線程上的宏任務執行完畢,則開始檢測是否存在可執行的微任務,檢測到一個Promise微任務,那麼馬上執行,輸出promise1promise2

  6. 微任務執行完畢,主線程開始讀取任務隊列中的事件任務setTimeout,推入主線程造成新宏任務,而後在主線程中執行,輸出setTimeout

最後的輸出結果即爲:

script start
script end
promise1
promise2
setTimeout

 

 

總結

以上即是JS引擎執行的所有過程,JS引擎的執行過程其實並不複雜,只要多思考多研究就能夠理解,理解該過程後能夠在必定程度上提升對JS的認識。

 [原文連接](https://heyingye.github.io/2018/03/26/js%E5%BC%95%E6%93%8E%E7%9A%84%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B%EF%BC%88%E4%BA%8C%EF%BC%89/)

參考文獻

轉自:http://www.javashuo.com/article/p-ewqzxgmz-t.html

相關文章
相關標籤/搜索