JavaScript中的執行機制

衆所周知JavaScript語言是單線程語言,單線程就意味着全部的任務都須要按序執行,只有上一個任務結束後才能繼續執行下一個任務,那JavaScript當中它的執行機制又是怎麼樣的呢?下面咱們就將以代碼爲例,逐一的來理解。javascript

JavaScript中的調用堆棧和任務隊列

爲了更好的理解JavaScript中的調用堆棧和任務隊列,請看下圖(轉引自Philip Roberts的演講《Help, I'm stuck in an event-loop》):html

image

以上圖說明主線程在執行的時候產生堆(heap)和棧(stack),當執行環境的堆棧中的一個任務(task)在執行的時候,其它的任務都要處於等待狀態。當主進程執行到異步操做的時候就會將異步操做對應的task回調放置到對應的任務隊列中,當主進程的調用堆棧中全部的task都執行完成後再去執行任務隊列當中的task(回調函數);以下:java

例子1:node

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

以上代碼的執行以下圖所示: vim

image

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

image

在執行環境棧中會加入一個什麼test函數的task,因而會聲明一個test函數,代碼繼續往下執行,圖示以下:異步

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

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

image

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

image

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

可是任務隊列分爲: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,就這樣一直循環。ui

下面我將以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; 因此最後控制檯的輸出結果是:二、四、end、三、五、一、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中文網

相關文章
相關標籤/搜索