0%

Javascript 非同步 & Event Loop!10 分鐘輕鬆圖解學習!


有聽過 Javascript 是個『非同步』的語言嗎?為什麼它可以做到『非同步』的效果呢?一起用圖解的方式來學習 Javascript 的『同步』與『非同步』吧!


HIHI~😍 如果你是第一次來的話,『Chan-Chan-Dev』是一個專門用簡單的圖文與故事講解網路程式技術的網站。
若你也喜歡用這種方式學習的話,歡迎加入 Chan-Chan-Dev Facebook 的粉絲團,在發佈的時候就有比較多機會收到通知喔!😍

在學習 Javascript 過程中,應該多少都會耳聞過 Javascript 非同步 的特性,在聊聊 非同步 的特性之前,也許我們要先知道何爲 同步 囉 😆。

同步 (Sync)

我們用小明早上起床出門上班前的小例子來學習這兩者的差別吧 😀
他是小明 👆
假如小明從起床到上班出門前他需要完成以下幾件事情:

  • 刷牙、洗臉 (10 分鐘)
  • 用義式咖啡機泡咖啡(10 分鐘)
  • 用電鍋準備午餐(10 分鐘)

若今天小明 一件事情完成後,在繼續執行下一件事情

例如:刷牙、洗臉 (10 分鐘) 完接著用 用義式咖啡機泡咖啡(10 分鐘) ,然後 用電鍋準備午餐(10 分鐘),最後出門。我們可以說小明上述的步驟爲 同步(Sync) 的執行每一件事情,所以總共所花的時間是 30 分鐘。

同步執行總共花了 30 分鐘

上述的 同步 不等於 同時,一開始接觸到學習的時候很容易將 同步同時 劃上等號,這邊要特別注意是不一樣的意思喔 😀

同步:一件事情完成後,在繼續執行下一件事情

Javascript 同步

1
2
3
4
5
console.log('a');

console.log('b');

console.log('c');

在『一般』的情況下,Javascript 是以 同步 的方式執行程式碼,因此上述的程式碼會依序地一個執行完之後在執行下一個

  1. 執行第 1 行,在 console 呈現 a
  2. 執行第 3 行,在 console 呈現 b
  3. 執行第 5 行,在 console 呈現 c

因此在 console 會呈現:

1
2
3
10    // a
20 // b
30 // c

那至於什麼是『非同步』呢?讓我們看下去囉 😆

非同步 (Async)

每次出門的都要花上 30 分鐘也沒有不好,只是小明最近剛好看到一本時間管理的書,教你如何有效率的利用時間!其中有提到一點可以把事情同時進行,也就是說 一次可以同時做好幾件的事情,不需要等前面的事情完成後,才能做後面的任務

經過啓發後的小明就很開心地將 一次可以同時做好幾件的事情 的方式套用在他出門前的準備事項。

於是他可以去刷牙前,先按下咖啡的按鈕、又按下電鍋的按鈕,他就可以跑去刷牙了。所以 刷牙泡咖啡準備午餐 三件事情在 同一時間內一起進行 。因此原本要花 30 分鐘時間,一瞬間就可以縮短爲 10 分鐘就完成了 😍

非同步執行總共花了 10 分鐘

非同步:一次同時做好幾件的事情


為什麼 Javascript 需要非同步呢 🤔

我們剛剛說過 同步 就是 一次完成一件事情,不管後面有幾件事情,都需等待上一件事情的完成才能依序執行

至於為什麼 Javascript 需要非同步呢?也許帶入情境會更有感覺:

小明在公司午休的時候邊吃他早上用電鍋蒸好的中餐,邊瀏覽網站。

在 Javascript 『同步』 的情境下,在電腦上的操作都需要等待前一件事情的完成,所以會是怎麼樣的情況呢?讓我們用簡單來模擬一下吧 😀

網頁同步狀況瀏覽

上面的例子當然是有點誇張了 😅

若使用同步的方式向伺服器端發出請求的話,使用者就會被強迫等待伺服器處理完,並且回傳資料回來的這段時間,而這段時間網頁可能會呈現使用者任何的操作都沒有反應的狀態,因此會讓使用者誤會以爲是當機了,直到資料送回來的任務已經完成後,網頁才恢復有反應的狀態。

問題點:Javascript 是一個在瀏覽器執行的語言(先不聊 Node.js 啦 😆),讓使用者必須強迫等待上一個任務執行完畢之後,網頁才會有反應的話,會造成極差的使用者體驗!😭

因此預期想要達到的效果是:

當一個任務需要花一段時間的時候(例如向伺服器發出請求取得資料),讓使用者不需要等待也可以繼續地使用網頁,等待資料回來之後,在顯示在使用者眼前,不會因此而中斷使用體驗。

網頁非同步狀況瀏覽

還記得非同步嗎? 因爲非同步可以一次同時做好幾件的事情 ,讓瀏覽器可以在發出請求的同時,一樣可以繼續回應使用者的操作,並不會因此而讓使用者無法操作網頁 😍

非同步請求

上述的情境其實就是 非同步請求(Ajax Asynchronous JavaScript and XML) 的使用情境。若對 Ajax 的用法有興趣的大大,請先參考 W3school 的教學,未來有機會在撰寫一篇 Ajax 的介紹文囉 😆

Javascript 爲單一執行緒

意思就是 Javascript 一次只能做一件事情,等上一件事情完成才能執行下一件事情,就如同上述的 同步 的樣子。

那 Javascript 為什麼可以做到 非同步 的效果呢? 🤔

Web APIs

當我們撰寫網頁程式碼時候,有很多常用方便的功能瀏覽器已經替我們提供好了,他們統稱叫做 Web APIs
例如:Dom 物件的操作、Ajax 相關的 XMLHTTPRequestFetch API,以及時常被使用到的 setTimeout 等等。

在呼叫 Web APIs 時,很常會使用 非同步的方式 處理 。

我們簡單用 setTimeout 看看非同步的效果吧。

setTimeout

setTimeout() 可以爲一段程式碼設定一個記時計,預計在一段時間後在執行這段程式碼。

1
setTimeout(function /* 要執行的 callback function */, delay /* 延遲時間 (毫秒) */);

以下的程式碼就會在 3000 毫秒(3秒)過後,才跳出 hihihi~的 alert 訊息。

1
2
3
setTimeout(function(){
alert('hihihi ~');
}, 3000);
1
2
3
4
5
6
7
8
console.log('a');

// 我們將 console.log('b') 放入 setTimeout 的程式碼裏面,等待 0 秒
setTimeout(function(){
console.log('b in setTimeout');
}, 0);

console.log('c');
1
2
3
a
c
b in setTimeout

上面程式碼執行的順序因爲非同步的關係變成以下:

  1. 執行第 1 行,在 console 呈現 a
  2. 執行第 8 行,在 console 呈現 c
  3. 執行第 5 行,在 console 呈現 b 👈 最後執行

現在知道了使用 Web APIs 可以達到非同步的效果,但是為什麼會是上述的這個結果呢? 🥸

Event Loop

在回答上述的問題前,請容許我外插一個 Event Loop 的話題,我們先來看個大概的流程圖吧 😆
Event Loop 架構與流程圖

我們可以從上面觀察到三大區塊,分別是:Call StackWeb APIsCallback Queue

由上面的順序,我們會看到 Call Stack 會呼叫 Web APIsWeb APIs 等到完成後,會將完成後要執行的 callback function 丟到 Callback Queue 內排隊,等待 Call Stack 若是空的狀態的話,就可以將要執行的 callback functionCall Stack 執行。

瞭解大概的順序後,我們就來一個個瞭解這些 Keyword 吧 😆

Call Stack

在我們撰寫 Javascript 的 function 並且呼叫的時候,都有默默地使用到 Call Stack ,只是我們不知道原來我們用過的機制。

Call Stack 是一種 Javascript 用來追蹤 Function 執行狀態的機制,他有著 『後進先出(Last-In-First-Out, LIFO)』 的特性。

假如一段簡單的小程式:

1
2
3
4
5
6
7
8
9
10
11
12
function A(){
console.log('A Start');
B();
console.log('A End');
}

function B(){
console.log('B Start');
console.log('B End');
}

A(); // 呼叫 function A

我們就會在 Console 看到一下的輸出

1
2
3
4
A Start
B Start
B End
A End

Call Stack 過程分解圖

接著我們來用慢動作示意圖分解看一下在這段程式碼的過程發生了什麼事情吧 😆

到 12 行,執行 A(),並且將 A() 放入 Call Stack 內
到 2 行,執行 console.log('A Start'),並且將它放入 Call Stack 內
執行完 console.log('A Start'),並且將內容顯示在 Console 上,將 console.log('A Start') 從 Call Stack 中移除
到 3 行,執行 B(),並且將 B() 放入 Call Stack 內
到 8 行,執行 console.log('B Start'),並且將它放入 Call Stack 內
執行完 console.log('B Start'),並且將內容顯示在 Console 上,將 console.log('B Start') 從 Call Stack 中移除
到 9 行,執行 console.log('B End'),並且將它放入 Call Stack 內
執行完 console.log('B End'),並且將內容顯示在 Console 上,將 console.log('B End') 從 Call Stack 中移除
執行完 B(),並且從 Call Stack 中移除
到 4 行,執行 console.log('A End'),並且將它放入 Call Stack 內
執行完 console.log('A End'),並且將內容顯示在 Console 上,將 console.log('A End') 從 Call Stack 中移除
執行完 A(),並且從 Call Stack 中移除
以上的程式碼全部執行完畢啦!

從上面的圖解過程就可以簡單發現一件事情,Call Stack 依循着 『後進先出(Last-In-First-Out, LIFO)』 的特性在記錄並且執行函數的狀態,A() 在第一張圖最先跳進 Call Stack 內,卻在倒數第二張圖才被移出來。

以上是用圖解的方式呈現,若想看動態的執行的話,可以在 loupe 看到即時的執行呈現喔 😍

後進先出 (Last-In-First-Out LIFO)

Last-In-First-Out, LIFO
想像手上有一疊撲克牌,在手上的由上到下順序是 1, 2, 3, 4,第一張抽出的爲 1,並且將 1 放在桌面,接著繼續抽出 2 放在 1 的上面,接著將 3 疊在 2 的上面,接著放 43 的上面。因爲 4 是最後才被放上去的,所以在最上面,因此當要從桌面取牌的時候,第一張會拿到的爲 4,接著爲 321

Web APIs & Callback Queue

剛剛有提到 setTimeout 爲 Web APIs 的功能,因此當執行這段程式碼的時候會走以下的流程

1
2
3
4
5
6
7
8
console.log('a');

// 我們將 console.log('b') 放入 setTimeout 的程式碼裏面,等待 0 秒
setTimeout(function(){
console.log('b in setTimeout');
}, 0);

console.log('c');

當執行到第 4 行的時候,會將這段程式碼移動到 Web APIs 的區塊內,等待 0 秒
將 setTimeout 的 Callback Function 放入 Callback Queue 內排隊
若 Call Stack 爲空的狀態
就將 Callback Queue 的 Callback Function 推到 Call Stack 執行

相對於 Call Stack 的『後進先出(Last-In-First-Out, LIFO)』, Callback Queue 卻是先進先出(First-In-First-Out, FIFO)的運作模式。

先進先出 First-In-First-Out FIFO

最簡單的解釋就是用 排隊吃飯 概念的概念來理解:越早排隊的人,應當最早進入餐廳,最後排隊的人,則是最後進入。不照著這個規則大家會生氣氣喔!

執行順序解答

因此我們終於可以來回答在 Web APIs 的問題,為什麼會呈現以下的順序呢?

1
2
3
4
5
6
7
8
console.log('a');

// 我們將 console.log('b') 放入 setTimeout 的程式碼裏面,等待 0 秒
setTimeout(function(){
console.log('b in setTimeout');
}, 0);

console.log('c');
1
2
3
a
c
b in setTimeout
  1. 在第 1 行 console.log('a') 被推入 Call Stack 之後,馬上被執行,因此 Console 印出 a
  2. 在第 4 行執行 setTimeout API 方法後,將這段程式碼丟進 Web APIs 的區塊內
  3. 在第 8 行 console.log('c') 被推入 Call Stack 之後,馬上被執行,因此 Console 印出 c
  4. setTimeout 在 Web APIs 等待時間失效後(等待 0 秒), Callback Function 被丟入 Callback Queue 排隊,等到 Call Stack 有空檔的時候,在將在Callback Queue 排隊的第一個 Function(也就是 console.log('b in setTimeout');)丟進 Call Stack 執行。
  5. 最後才在 Console 印出 b in setTimeout

上述的步驟若想看動態的執行的話,可以在 loupe 看到即時的執行呈現喔 😍

結論

這篇其實本來只是想針對介紹 Promise 之前的前情提要介紹文,結果寫著寫著發現好像又牽扯出 Event Loop 這個話題才能回答上述的問題,本來還想聊聊 Javascript 的 Async Callback 的話題,但看看文章的長度還是另開一篇好了 😆

我們從一開始先瞭解 Javascript 的同步所帶來的不方便,來瞭解非同步的好處,在一路沿伸探討到 Javascript 可以非同步原因,用 Event Loop 模型作爲解答。

而本文介紹的 Event Loop 的概念也是來自於這篇 Talk 《What The Heck is The Event Loop Anyway》,並且使用了這個講者開發的 loupe 協助理解。


最後感謝你願意看到這裏,希望這篇文章對你有所幫助。

若你想到身邊有需要這篇文章內容的朋友,也請你幫他一個忙把這篇文章分享給他 😍

若文章的內容有錯誤的地方,也歡迎隨時一起討論交流。😘

最後感謝你的閱讀囉,我們下次見!Bye ~ 🚀

參考