添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

非同步一直是困擾著 Javascript 新手的小魔王,以前常常會有「為什麼這行先跑到沒有先執行?」這種困擾。

隨著時間過去,踩了幾次 bug,也漸漸抓到非同步的節奏,但在這個階段,往往也只是知道,非同步就是不斷地等:

setTimeout 就是等幾秒就會執行 addEventListener 就是要等事件觸發 fetch 就是要等後端回應

我們知道非同步「怎麼用」,卻不一定知道「為什麼」,讓我們來看看非同步的核心究竟是怎麼運作的。

Javascript Runtime

首先需要提到的是 Javascript Runtime,中文大概可以翻成 Javascript 的「執行環境」吧,比如 Chrome、Firefox、node,每個 runtime 提供的 API 都不同,所以不是所有地方都有 window 物件, setTimeout 之類的 Web API。

Runtime 會隨著環境而不同,但有兩個機制,是屬於 Javascript 的機制,因此任何地方都一樣: Call Stack(存放指令)、Memory Heap(存放資料)

另外還有兩個名詞也先介紹一下:

Callback Queue :用來存放從 Web api 過來,準備要進入 Call Stack 的指令 Event Loop :會不斷監看 Call Stack,如果空了就會把 Callback Queue 的指令放到 Call Stack 執行

程式碼在背景的處理順序

對於一段程式碼,Javascript engine 底層會依序做這些事:

  • 把 JS 的指令一行一行放到 Call Stack,並且執行
  • 途中如果遇到不屬於 JS 自身 (如: Web API) 的指令,因為 JS 看不懂,會交由 Web API 處理
  • Web API 處理後的(如: setTimeout 秒數數完)程式碼會放到 Callback Queue
  • Event Loop 不斷地監看 Call Stack 是否空了,如果空了就會把 Callback Queue 的指令放到 Call Stack 執行
  • 以下程式碼為例,用到 $.on (類似 addEventListener ) 跟 setTimeout 這種非同步的程式碼,但中間也夾雜了一些 console ,在 background 會怎麼運作呢?

    $.on('button', 'click', function onClick() {
        setTimeout(function timer() {
            console.log('clicked');    
        }, 2000);
    console.log("Hi!");
    setTimeout(function timeout() {
        console.log("Click the button!");
    }, 5000);
    console.log("Welcome");
    
  • $.on() 放到 Call Stack
  • $.on() 交由 Web API 處理(因為不是原生 JS)
  • Web API 開始等待按鈕點擊事件
  • console.log("Hi!") 放到 Call Stack,執行
  • setTimeout() 放到 Call Stack
  • setTimeout() 交由 Web API 處理(因為不是原生 JS)
  • Web API 開始等待 5 秒
  • console.log("Welcome") 放到 Call Stack,執行
  • 至此,Call Stack 已淨空,Event Loop 會把 Callback Queue 裡面的指令搬到 Call Stack 執行

  • (過了 5 秒)
  • Web API 內的 setTimeout() 的 callback function 被搬到 Callback Queue
  • Event Loop 把 setTimeout() 的 callback function 搬到 Call Stack,執行
  • 至此,Call Stack 再度淨空

  • (使用者點擊了按鈕)
  • Web API 內的 $.on() 的 callback function 被搬到 Callback Queue
  • Event Loop 把 $.on() 的 callback function 搬到 Call Stack,執行
  • setTimeout() 交由 Web API 處理(因為不是原生 JS)
  • Web API 開始等待 2 秒
  • 至此,Call Stack 再度淨空

  • (過了 2 秒)
  • Web API 內的 setTimeout() 的 callback function 被搬到 Callback Queue
  • Event Loop 把 setTimeout() 的 callback function 搬到 Call Stack,執行
  • 至此,Call Stack 再度淨空

    如果我提早點擊按鈕?

    上述的流程是比較順的正向流程,但真實情境下,哪會在那邊等 5 秒才按按鈕啊,如果我們提早點擊按鈕,會發生什麼事?

    你會發現,因為你點擊,Callback Queue 很早就有指令了,但那個指令只能乖乖排隊,等程式碼跑完最後一行程式,才會輪到它,因為 Event Loop 要等 Call Stack 的指令都跑完,才會放 Callback Queue 的人進來。

    setTimeout 0 秒算是同步還非同步?

    console.log('1');
    setTimeout(() => {
        console.log('2');
    }, 0);
    console.log('3');
    

    這問題真的很妙,「我等了你 0 秒,請問我算是有等你嗎?」,彷彿成了哲學思考問題XD

    如果 setTimeout 只用一句很簡單的「等了幾秒就執行」來概括,就會在這個範例被卡住,因為這個範例一秒都不用等,那是不是就會立刻執行呢?

    大家可以試著自己想想上述的程式碼會印出什麼,其實核心問題跟上一題提早點擊是一樣的。

    最後答案是:

    沒錯,只要是非同步(如 Web Api)的程式碼,就一定要進 callback queue 蹲著,不管有多快進去,都一定要等 call stack 的程式碼都跑完,才有可能輪到它。

    對於新手比較容易理解的會是這樣:「要等同步都執行完,才會輪到非同步」。

    視覺化的 Playground

    這個網站是我極力推薦的地方,我的抽象思維不太好,沒辦法在腦袋中把同步跟非同步攪在一起,可以透過這個 playground,把你寫的 code 實際在 background 執行起來,連 background 在做的事情都清楚顯示給你看,特別適合像我一樣的視覺化動物(?)。

    可以看到當你將上面的程式貼上去,按下 Save and Run,程式並不會咻一聲就跑完,而是用大約每一秒 2 個指令的速度,把這行程式碼是被放到 Call Stack 還是 Callback Queue,清楚顯示在畫面上,可以很清楚知道電腦現在正在處理哪個指令。如果還是嫌太快,作者也準備了 Pause 按鈕,按照自己的步調調整。

    前面兩題關於「提早點擊」與「setTimeout 0 秒」的問題,你都可以在這個 playground 找到解答。

    同步與非同步

    如果你很有耐心,把上述一大串都看完,就會知道「非同步」誕生的原因,真的不是憑空誕生的,而是因為就像上面的 Web Api 那樣,對於 Javascript 以外的指令,透過背景運作的機制來完成,就是所謂的「非同步」。

    看到這裡,下次被問到「setTimeout 為什麼是非同步?」的時候,你會怎麼回答呢?筆者曾經只能回答「因為要等」這種答案XD,現在才知道背後的學問可大了呢!

    另外也有人會疑惑說:「我懂同步跟非同步的意思了,但我不明白,同步兩個字聽起來像是大家一起、同時的感覺,但是為什麼在 Javascript 卻是代表一個接一個、陸續的感覺?」

    這比較是視角不同的緣故,或許可以改用這種方式重新理解:

  • 同步:在同一個步道接力跑,第二棒一定要等第一棒結束才跑
  • 非同步:在不(非)同步道同時跑,誰都不等誰,該跑就跑
  • 非同步難歸難,背後的世界卻非常廣闊,當我們真的學會了 Javascript 是如何處理非同步程式碼,才更能在每一次程式運作不如預期時,一步步推敲出問題核心,就離更好的 developer 更近了一步!

    在一個世界線
    交織著兩個平行宇宙
    踏著不同的步調
    寫著同一個故事

    the-call-stack-and-memory-heap