iT邦幫忙

2021 iThome 鐵人賽

DAY 14
2

前言

非同步一直是困擾著 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 底層會依序做這些事:

  1. 把 JS 的指令一行一行放到 Call Stack,並且執行
  2. 途中如果遇到不屬於 JS 自身 (如: Web API) 的指令,因為 JS 看不懂,會交由 Web API 處理
  3. Web API 處理後的(如: setTimeout 秒數數完)程式碼會放到 Callback Queue
  4. 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");
  1. $.on() 放到 Call Stack
  2. $.on() 交由 Web API 處理(因為不是原生 JS)
  3. Web API 開始等待按鈕點擊事件
  4. console.log("Hi!") 放到 Call Stack,執行
  5. setTimeout() 放到 Call Stack
  6. setTimeout() 交由 Web API 處理(因為不是原生 JS)
  7. Web API 開始等待 5 秒
  8. console.log("Welcome") 放到 Call Stack,執行

至此,Call Stack 已淨空,Event Loop 會把 Callback Queue 裡面的指令搬到 Call Stack 執行

  1. (過了 5 秒)
  2. Web API 內的 setTimeout() 的 callback function 被搬到 Callback Queue
  3. Event Loop 把 setTimeout() 的 callback function 搬到 Call Stack,執行

至此,Call Stack 再度淨空

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

至此,Call Stack 再度淨空

  1. (過了 2 秒)
  2. Web API 內的 setTimeout() 的 callback function 被搬到 Callback Queue
  3. 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 只用一句很簡單的「等了幾秒就執行」來概括,就會在這個範例被卡住,因為這個範例一秒都不用等,那是不是就會立刻執行呢?

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

最後答案是:

1
3
2

沒錯,只要是非同步(如 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


上一篇
Day 13 - OOP 初探 (3) - 實戰地圖遊戲
下一篇
Day 15 - Asynchronous 非同步進化順序 - Callback 與 Promise
系列文
Javascript 從寫對到寫好30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
TD
iT邦新手 4 級 ‧ 2021-10-11 22:44:53

同步:在同一個步道接力跑,第二棒一定要等第一棒結束才跑
非同步:在不(非)同步道同時跑,誰都不等誰,該跑就跑

我之前也覺得這兩個詞的(表面)意義好像顛倒了,看到你這樣解釋我覺得很不錯!

ycchiuuuu iT邦新手 4 級 ‧ 2021-10-12 12:25:28 檢舉

也有可能真的顛倒了,是我們強行幫它合理化而已XD ((拖走

我要留言

立即登入留言