深入理解Javascript之Callstack&EventLoop

darjun發表於2019-03-02

1.概述

眾所周知,Javascript是一個單執行緒的語言。這意味著,在Javascript中,同一時間只能做一件事情。

這樣的設計有一些優點,例如簡單,避免了多執行緒中複雜的狀態同步,寫程式時不用考慮併發訪問。但同時也帶來了一些其他問題,其中比較突出的一個問題是:程式碼邏輯不直觀。由於Javascript是單執行緒的,其中只有一個執行序列。所以,在執行非同步操作(例如定時,網路請求這些不能立即完成的操作)時,Javascript執行時不可能在那裡等著操作完成。否則整個執行時都被阻塞在那裡了,導致其他所有的操作都無法進行,例如網頁渲染,使用者點選、滾動頁面等操作。這樣的使用者體驗是非常糟糕的。

正因為如此,Javascript使用回撥函式處理非同步操作結果。進行非同步操作時傳入一個回撥,操作完成之後由Javascript引擎執行這個回撥,將結果傳入。慢慢地,Javascript中充斥著大量的回撥。過多的使用回撥讓一段完整的邏輯被拆分成了很多片段,非常不利於閱讀與維護。回撥過多的問題在NodeJS中更為突出,故而出現了Promise(見我的前一篇部落格)和async/await

那麼非同步操作完成時,Javascript執行時是怎樣感知到並呼叫對應的回撥函式的呢?

答案是EventLoop(事件迴圈)。

要了解EventLoop是怎樣運作的,我們首先需要了解Javascript是怎樣處理一個個任務,呼叫一個個函式的。這就是CallStack(呼叫棧)所做的事。

2.呼叫棧

相信有過其他語言程式設計經驗的讀者都聽說過CallStack的概念。Javascript中的CallStack類似。

CallStack是一個棧結構,棧的特點是LIFO(後入先出),出棧入棧只會在一端(也就是棧頂)進行。

CallStack是用來處理函式呼叫與返回的。每次呼叫一個函式,Javascript執行時會生成一個新的呼叫結構壓入CallStack。而函式呼叫結束返回時,JavaScript執行時會將棧頂的呼叫結構彈出。由於棧的LIFO特性,每次彈出的必然是最新呼叫的那個函式的結構。

Javascript啟動時,從檔案或標準輸入載入程式。載入完成時,Javascript執行時會生成一個匿名的函式,函式體就是輸入的程式碼。這個函式就有點類似於C/C++中的main()函式,是我們的入口函式。我們姑且稱之為<main>函式。Javascript啟動時,首先呼叫就是<main>函式。看下面程式碼:

function func1() {
    console.log('in function1');
}

function func2() {
    func1();
    console.log('in function2');
}

function func3() {
    func2();
    console.log('in function3');
}

func3();
複製程式碼

上面程式碼很好理解,我們來看看Javascript是如何執行這段程式碼的。

Javascript首先載入程式碼,建立一個匿名<main>包裹這段程式碼並呼叫該函式。<main>函式執行,依次定義函式func1func2func3,然後呼叫函式func3。為func3建立呼叫結構並壓棧。函式func3中呼叫func2,為func2建立呼叫結構並壓棧。函式func2中呼叫func1,為func1建立呼叫結構並壓棧。這個過程中,CallStack的變化如下。

Call Stack Push

然後,函式func1執行完成,從棧頂彈出呼叫結構。然後func2繼續執行,func2執行完成後從棧頂彈出其呼叫結構。然後func3繼續執行,func3執行完成後從棧頂彈出其呼叫結構。這個過程中,CallStack的變化如下。

Call Stack Pop

當然,我這裡有一個地方不太嚴謹。不知道讀者有沒有注意到,console.log也是函式函式哦。所以在func1中呼叫console.log時,CallStack上也會有對應的呼叫堆疊。func2func3中的console.log呼叫同樣如此。有興趣的話,可以自己畫一畫完整的呼叫流程,這樣可以加深理解?。

這裡我推薦大家使用Google Chrome的開發者工具來幫助我們理解CallStack。下圖是上面程式碼在開發者工具中的一步步執行的結果:

Call Stack Demo

  • Chrome中<main>稱為<anonymous>

  • 單步執行時,重點觀察右側工具欄中Call Stack一欄的變化。

在CallStack中執行的函式,我們稱之為一個task(任務)。

接下來,我們來思考這樣一個問題:setTimeoutsetIntervalAJAX請求這些功能是怎麼實現的?

一位牛人Philip Roberts曾經將V8引擎(Chrome內建的Javascript引擎)原始碼下載下來,然後用grep查詢,發現原始碼中並沒有實現這些函式的程式碼?。

那麼這些函式到底是如何實現的,又是如何與Javascript引擎互動的呢?

答案是:宿主(網頁中指的是瀏覽器,Node中指的是Node引擎)提供實現,並在操作完成時將結果(非同步的,會有延遲)放入Javascript引擎的task佇列,由Javascript引擎處理。

3.事件迴圈

EventLoop顧名思義,其實就是Javascript引擎中的一個迴圈,它就是一個不停地從任務佇列(task queue)中取出任務執行的過程。

我們前面詳細瞭解了CallStack以及Javascript啟動時是如何處理的。但是<main>退出後,Javascript引擎就沒事做了嗎?當然不是,有很多工會不定時的觸發需要Javascript引擎去處理。例如,使用者點選按鈕,定時器,頁面渲染等。

其實,Javascript引擎中維護著一個任務佇列。當CallStack中沒有任務在執行時,引擎會從任務佇列中取出任務壓入CallStack處理。我們通過程式碼來具體看看(引用jesstelford):

setTimeout(() => {
    console.log('hi');
}, 1000);
複製程式碼

我們的Js程式碼,call stack,task queue和Web APIs(瀏覽器中實現)關係如下:

        [code]        |   [call stack]    | [task queue] | |    [Web APIs] |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  |                   |              | |               |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |
複製程式碼

開始時,程式碼未執行,所有都是空的。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  | <main>            |              | |               |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |
複製程式碼

開始執行程式碼,壓入我們的<main>函式。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
> setTimeout(() => {  | <main>            |              | |               |
    console.log('hi') | setTimeout        |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |
複製程式碼

執行第一行程式碼,呼叫函式setTimeout。我們前面說過,每個函式呼叫都會建立一個新的呼叫記錄壓到棧上。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  | <main>            |              | | timeout, 1000 |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |
複製程式碼

setTimeout執行完成,從棧中移除對應呼叫記錄。Web APIs記錄超時和回撥,超時機制由瀏覽器實現。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  |                   |              | | timeout, 1000 |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |
複製程式碼

程式碼中沒有其他邏輯,<main>函式結束,從棧移除呼叫資訊。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  |                   | function   <-----timeout, 1000 |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |
複製程式碼

超時時間到了,Web APIs將回撥放入task queue中。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  | function        <---function     | |               |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |
複製程式碼

EventLoop檢測到Javascript沒有任務處理,從task queue中取出任務執行。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  | function          |              | |               |
>   console.log('hi') | console.log       |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |
複製程式碼

執行該回撥函式,回撥函式中又呼叫了console.log函式。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  | function          |              | |               |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |
> hi
複製程式碼

console.log執行完成,輸出"hi"。

        [code]        |   [call stack]    | [task queue] | |   [Web APIs]  |
  --------------------|-------------------|--------------| |---------------|
  setTimeout(() => {  |                   |              | |               |
    console.log('hi') |                   |              | |               |
  }, 1000)            |                   |              | |               |
                      |                   |              | |               |
> hi
複製程式碼

回撥函式執行完成,CallStack再次為空。

上面就是setTimeout的執行過程,從中開始看出EventLoop在幕後做的工作。

下面我們再來看一段程式碼:

console.log("start");

setTimeout(() => {
    console.log("timeout");
}, 0);

Promise.resolve()
  .then(() => {
      console.log("promise1");
  })
  .then(() => {
      console.log("promise2");
  });

console.log("end");
複製程式碼

這段程式的輸出是什麼?建議大家先思考一下,最好能動筆畫一畫圖?。

4.微任務佇列

接著上一節的程式碼,符合標準(很多舊版本的瀏覽器實現都是不符合標準的,具體參見參考連結)的輸出應該是:

start
end
promise1
promise2
timeout
複製程式碼

但是,為?什?麼?

實際上,Javascript中有另外一種佇列。Promise的回撥是被放入這個佇列的。這個佇列叫做microtask queue(微任務佇列),ES6標準中叫Job Queue。microTask的優先順序是比task高的,也就是說microtask佇列中的任務要先處理。

  • EventLoop檢測到當前沒有任務在執行,首先檢查microtask佇列中有沒有需要處理的任務。如果有那麼一個個執行,直到microtask佇列為空。

  • microtask佇列中沒有任務了,執行task佇列中的任務。這裡需要注意,**每執行一個task佇列中的任務,就檢查一下microtask佇列狀態。將microtask佇列中所有任務都執行完成之後,再從task佇列中取出任務執行。

下面我們看看上面那段程式碼是怎麼一步步執行的:

為方便起見我們稱setTimeout回撥為timeoutcb,稱第一個then成功回撥為promisecb1,第二個then回撥為promisecb2

        [code]                    |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  --------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");        |                   |              | |                   | |               |
  2.                              |                   |              | |                   | |               |
  3. setTimeout(() => {           |                   |              | |                   | |               |
  4.     console.log("timeout");  |                   |              | |                   | |               |
  5. }, 0);                       |                   |              | |                   | |               |
  6.                              |                   |              | |                   | |               |
  7. Promise.resolve()            |                   |              | |                   | |               |
  8. .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1"); |                   |              | |                   | |               |
  10.})                           |                   |              | |                   | |               |
  11..then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2"); |                   |              | |                   | |               |
  13.});                          |                   |              | |                   | |               |
  14.                             |                   |              | |                   | |               |
  15.console.log("end");          |                   |              | |                   | |               |
複製程式碼

開始時,call stack、task queue、microtask queue都為空。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
> 1. console.log("start");         |     <main>        |              | |                   | |               |
  2.                               |     console.log   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
複製程式碼

程式開始執行,壓入<main>函式。首先執行第一行程式碼,console.log("start"),將console.log壓棧。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
> 1. console.log("start");         |     <main>        |              | |                   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
複製程式碼

console.log執行完成,輸出"start"。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    <main>         |              | |                   | |               |
  2.                               |    setTimeout     |              | |                   | |               |
> 3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
複製程式碼

第二行為空跳過,開始執行第三行程式碼,setTimeout壓棧。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    <main>         |   timeoutcb <------------------------~~timeoutcb, 0~~|
  2.                               |                   |              | |                   | |               |
> 3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
複製程式碼

setTimeout執行完成,由於超時是0,所以立即回撥立即進入task queue中。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    <main>         |  timeoutcb   | |     promisecb1    | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
> 7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
複製程式碼

程式碼執行到第7行:

  • 首先Promise.resolve壓棧,執行完成後返回一個Promise物件。

  • 然後呼叫該物件的then方法,該方法壓棧,執行完成後返回一個全新的Promise物件,我們稱該物件為promise1。由於Promise.resolve返回物件的狀態為resolved,所以promise1回撥直接進入microtask佇列

  • 接著又執行新物件的then方法,我們稱該物件為promise2

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    <main>         |    timeout   | |     promisecb1    | |               |
  2.                               |    console.log    |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
> 15. console.log("end");          |                   |              | |                   | |               |
> start
複製程式碼

程式碼執行到第15行,console.log壓棧。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    <main>         |    timeout   | |     promisecb1    | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
> 15. console.log("end");          |                   |              | |                   | |               |
> start
> end
複製程式碼

console.log("end")執行完成,輸出"end",出棧。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |                   |    timeout   | |     promisecb1    | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
> 15. console.log("end");          |                   |              | |                   | |               |
> start
> end
複製程式碼

<main>函式沒有邏輯需要執行了,出棧。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    promisecb1     |    timeout   | |                   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
> 15. console.log("end");          |                   |              | |                   | |               |
> start
> end
複製程式碼

EventLoop檢查到microtask佇列中有任務需要執行,將promisecb1取出壓入call stack。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |                   |    timeout   | |      promisecb2   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
> end
> promise1
複製程式碼

promisecb1執行完成,輸出"promise1",並且返回undefined。(其實在這裡還有一個console.log壓棧出棧的過程,我就不畫了,下同)

深入理解Javascript之Promise中看到,如果返回一個值,那麼物件立刻變為resolved所以第二個then的回撥需要安排執行,進入microtask佇列

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |     promisecb2    |    timeout   | |                   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
> end
> promise1
複製程式碼

EventLoop檢測到call stack中沒有正在執行的任務,同時microtask佇列不為空。從microtask佇列取出任務壓入call stack。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |                   |    timeout   | |                   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
> end
> promise1
> promise2
複製程式碼

promisecb2執行完成,輸出"promise2"。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |    timeoutcb      |              | |                   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
> end
> promise1
> promise2
複製程式碼

接著,EventLoop檢測到call stack和microtask佇列都為空,從task佇列中取出timeoutcb壓入棧。

        [code]                     |   [call stack]    | [task queue] | | [microtask queue] | |   [Web APIs]  |
  ---------------------------------|-------------------|--------------| |-------------------| |---------------|
  1. console.log("start");         |                   |              | |                   | |               |
  2.                               |                   |              | |                   | |               |
  3. setTimeout(() => {            |                   |              | |                   | |               |
  4.     console.log("timeout");   |                   |              | |                   | |               |
  5. }, 0);                        |                   |              | |                   | |               |
  6.                               |                   |              | |                   | |               |
  7. Promise.resolve()             |                   |              | |                   | |               |
  8.  .then(() => {                |                   |              | |                   | |               |
  9.     console.log("promise1");  |                   |              | |                   | |               |
  10. })                           |                   |              | |                   | |               |
  11. .then(() => {                |                   |              | |                   | |               |
  12.    console.log("promise2");  |                   |              | |                   | |               |
  13. });                          |                   |              | |                   | |               |
  14.                              |                   |              | |                   | |               |
  15. console.log("end");          |                   |              | |                   | |               |
> start
> end
> promise1
> promise2
> timeout
複製程式碼

timeoutcb執行完成,輸出"timeout"。

5.總結

通過這篇文章,我們瞭解到Javascript時如何通過call stack來處理函式的呼叫與返回的。setTimeout等非同步機制其實是宿主提供實現,並在非同步操作完成負責將回撥放入任務佇列,最後由EventLoop在適合的時機取出壓入call stack實際執行。

我們還看到了另外一種佇列——microtask佇列。該佇列中存放的一般是優先順序較高的任務,例如Promise的回撥處理函式。

每當call stack中沒有正在執行的任務時,EventLoop會優先從microtask佇列中取出任務執行,當該佇列為空時才會從task佇列取。

在使用一門框架或語言時,對於是否需要了解底層運作機制和原理,往往會有比較大的爭論。有人說,我不瞭解內部原理同樣可以寫出好程式,那為什麼還需要花時間去研究呢? 對此,我覺得了解底層原理還是非常有必要的。有下面幾個好處:

  • 可以讓我們看到全貌,瞭解整個系統是如何運作的。

  • 底層原理大多是相通的,例如幾乎所有語言的函式呼叫底層都是利用CallStack來實現的。學會了Javascript的CallStack運作機制,在學習其他語言的相關概念時往往能事半功倍。

  • 瞭解底層可以讓我們心中有數,明白什麼事情能做,什麼事情不能做。例如Javascript是單執行緒的,我們寫程式碼時一定不能讓執行緒阻塞了?。

  • 瞭解底層可以讓我們更好的優化程式碼。當程式效能出現瓶頸時,可以更快地定位問題。

6.參考連結

  1. Philip Roberts在歐洲JSConf上的演講(必看)
  2. What is the JS Event Loop and Call Stack? — Jess Telford
  3. Tasks, microtasks, queues and schedules
  4. Understanding the JavaScript call stack

關於我: 個人主頁 簡書 掘金

相關文章