JavaScript中的執行機制

嘿_那個誰發表於2018-01-22

眾所周知JavaScript語言是單執行緒語言,單執行緒就意味著所有的任務都需要按序執行,只有上一個任務結束後才能繼續執行下一個任務,那JavaScript當中它的執行機制又是怎麼樣的呢?下面我們就將以程式碼為例,逐一的來理解。

JavaScript中的呼叫堆疊和任務佇列

為了更好的理解JavaScript中的呼叫堆疊和任務佇列,請看下圖(轉引自Philip Roberts的演講《Help, I'm stuck in an event-loop》):

image

以上圖說明主執行緒在執行的時候產生堆(heap)和棧(stack),當執行環境的堆疊中的一個任務(task)在執行的時候,其它的任務都要處於等待狀態。當主程式執行到非同步操作的時候就會將非同步操作對應的task回撥放置到對應的任務佇列中,當主程式的呼叫堆疊中所有的task都執行完成後再去執行任務佇列當中的task(回撥函式);如下:

例子1:

  console.log(1);
  function test() {
      setTimeout(function () {
          console.log('test');
      })
  }
  test();
  console.log(3);
  //執行結果:1、3、test
複製程式碼

以上程式碼的執行如下圖所示:

image

首先是在執行環境棧中壓入執行上下文的main函式,再次是按照順序執行將console.log(1);壓入到執行環境棧中,於是執行環境棧中就有了一個task——console.log(1),於是並開始執行該task,就輸出了1,輸出後程式碼開始繼續往執行,得到下圖所示環境:

image

在執行環境棧中會加入一個什麼test函式的task,於是會宣告一個test函式,程式碼繼續往下執行,圖示如下:

image
在執行環境棧中會加入一個test()的task,於是會開始執行test(),在執行的時候執行機制如圖:

image
test()在執行的時候會執行setTimeout,在執行setTimeout的時候就會建立一個任務佇列的task,建立完該task後執行環境棧繼續執行,如下圖:

image

在執行環境棧中會建立一個console.log(3)的task,並執行它,任務佇列當中setTimeout建立的task處於等待狀態,於是控制檯會輸出3,那麼此時控制檯的輸出結果當中已經有了1、3兩個數字,此時執行環境棧中的task已經都執行完成了,執行環境棧出現控制,那麼這個時候就會去看任務佇列裡面的task是否有需要執行的,這個時候setTimeout建立的task就會被發現,該task的執行函式將會被新增到回撥佇列裡面,因為執行環境棧中沒有task,於是改回撥函式將會被拿到執行環境棧中去執行,如下圖所示:

image

這時候執行環境棧中的task會開始執行,於是會輸出‘test’,輸出完成後,執行環境棧、任務佇列、回撥佇列都不存在task,於是整個過程執行完成。

但是任務佇列分為:macro-task(巨集任務)、micro-task(微任務)。

  1. macro-task包括:script(整體程式碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。
  2. micro-task包括:process.nextTick, Promises, Object.observe, MutationObserver

事件迴圈的順序是從script開始第一次迴圈,隨後全域性上下文進入函式呼叫棧,碰到macro-task就將其交給處理它的模組處理完之後將回撥函式放進macro-task的佇列之中,碰到micro-task也是將其回撥函式放進micro-task的佇列之中。直到函式呼叫棧清空只剩全域性執行上下文,然後開始執行所有的micro-task。當所有可執行的micro-task執行完畢之後。迴圈再次執行macro-task中的一個任務佇列,執行完之後再執行所有的micro-task,就這樣一直迴圈。

下面我將以process.nextTick,Promise,setImmediate、setTimeout為例; 程式碼如下:

   setTimeout(function () {
       console.log(1);
   },0);
   console.log(2);
   process.nextTick(() => {
       console.log(3);
   });
   new Promise(function (resolve, rejected) {
       console.log(4);
       resolve()
   }).then(res=>{
       console.log(5);
   })
   setImmediate(function () {
       console.log(6)
   })
   console.log('end');
複製程式碼

有了之前的執行分析,將上述程式碼劃分為如圖所示程式碼塊:

image
程式碼開始按需執行,執行1—setTimeout的時候,將setTimeout的回撥函式當成一個macro-task任務佇列新增到macro-task任務佇列裡面,如圖:

image

繼續執行下面程式碼2—console.log(2);於是控制檯會輸出2,如圖:

image

緊接著開始執行3—process.nextTick,因為process.nextTick是micro-task任務,於是將該任務的回撥函式加入到micro-task任務佇列當中,形成下圖:

image

控制檯輸出還是隻有2,緊接著開始執行4-Promise,在執行new Promise的時候,建立Promise例項的時候,傳入的函式將在執行環境棧中執行,於是會在控制檯輸出4,再講回撥函式then新增到micro-task任務佇列當中,形成下圖:

image

緊接著開始執行5-setImmediate,於是會將5的回撥函式新增到macro-task任務佇列當中,形成下圖:

image

繼續執行6-console,於是執行環境棧中會增加一個console.log('end)的任務,於是控制檯會輸出6,形成下圖:

image

執行到這裡的時候,主執行緒的執行環境棧中已經沒有任何任務了,那麼這個時候Event Loop機制就會開始將micro-task任務佇列當中滿足執行條件的一個(3-callback)拿到執行環境棧中執行,形成如下圖:

image

那麼控制檯中將出現輸出3,當該micro-task任務佇列的任務執行完成後,同樣的原理,再次將滿足條件的(4-then)拿到執行環境棧中去執行,形成下圖:

image

這個時候micro-task任務佇列裡面的任務也執行完了,那麼這個時候Event Loop機制將會到macro-task任務佇列當中去將滿足條件的任務拿到執行環境棧中去執行,與micro-task任務佇列執行的時候是一樣的原理,這裡就不再畫圖了,先是將1-callback拿到執行環境中去執行,控制檯會輸出1,執行完成後再將5-callback拿到執行環境棧中去執行,控制檯輸出6; 所以最後控制檯的輸出結果是:2、4、end、3、5、1、6;

總結:

  1. 主執行緒的執行環境棧上首先執行同步任務,然後再依靠Event Loop機制來不斷迴圈將任務佇列中的各個task放到執行環境棧中執行;

  2. 任務分為macro-task、micro-task,各有各的任務佇列,即macro-task任務佇列、micro-task任務佇列;

  3. 總的執行順序是 主執行緒上的task——micro-task——macro-task;

參考資料:

JavaScript 執行機制詳解:再談Event Loop

深入淺出JavaScript事件迴圈機制(下)

node中文網

相關文章