無痛理解 JS | 非同步怎麼運作?
總以為世界的形狀我們早已熟悉,但到頭來卻發現並不是。 筆者每每在寫 Javascript 時,處處都會發現驚喜。 下面想舉一個曾經印象深刻的例子給大家看:
console.log('Start');
console.log('Stop');
印出的結果為
Start
Stop
那中間如果加了
setTimeOut(()=>{
console.log('0 sec later');
},0);
變成
console.log('Start');
setTimeOut(()=>{
console.log('0 sec later');
},0);
console.log('Stop');
則結果竟會變成
Start
Stop
0 sec later
疑?這是什麼令人驚訝 奇葩 的結果?明明在中間加入了 0 sec later
,為什麼 console 出來的順序會是這樣? 要搞懂這個結果,就必須先來了解 Javascript 擁有的非同步特性
。
阻塞(blocking) : 如果不存在非同步?
不過,我們先來試著思考一個問題:非同步真的很重要嗎?沒有非同步的世界會怎樣呢?
阻塞
的概念可以想成每年過年高速公路必塞車的現象,高速公路
可以想成等等會提到的 Call Stack
,而車子
相當於 程式
。
每跑一段程式就要花時間等待執行完畢,從 Call Stack
中跳走後,下一個程式才能執行,於是當前面某個程式很慢就會連帶影響之後的程式。在瀏覽器上我們會看到類似當機
的畫面,什麼事都做不了。
想要避免造成當機的模樣,那麼我們來了解怎麼疏通高速公路吧!
一起來瞧瞧非同步的骨架
JS 的主要特點有兩個:
Single Thread (單線程)
Synchronous (同步)
說明 Javascript 一次只執行一件事,程式會逐行執行
。
那為什麼在網頁我們仍可以處理像是滑鼠點擊之類看起來「一次處理很多事件」的非同步 ( asynchronous ) 事件而不會阻塞塞車呢?原因是當 Javascript 在 V8 Engine 執行時,其實也會執行瀏覽器提供的 API ( Web APIs,這裡已經不是在 V8 Engine 的範疇 ),前文提及的滑鼠點擊 (click
)即屬於 Web APIs。
Web APIs 讓我們可以非同步執行程式, 也就是說 JS 本身並無法非同步執行, 需要借助 Web APIs 才能達到非同步的效果。
那非同步是怎麼運作的呢?
首先,要先了解下面四點的定義:
Call Stack
Call Stack 本身為 Stack,
Stack 是資料結構的一種,會遵守 LIFO (Last In, First Out) 的原則。
Call Stack 會繼承 Stack 原本的特性,即「最後被 call 的 function 會在最上層,
而執行完後會最先從該 stack 離開(pop off)」。
而 Javascript 是 Single Thread, 所以只會有一個 Call Stack。
圖片來源 / wiki - Stack
Web APIs
瀏覽器提供的 API,例如常被使用的 setTimeout。
setTimeout 是瀏覽器所提供的計時器,常常搭配 callback function 使用。
Task Queue
又可以稱作 Callback Queue,
放在 Web APIs 的 function 在執行完後,會到 Task Queue 待命。
(e.g. setTimeOut 在秒數執行完後,會移至 Task Queue)
Event Loop
偵測到 Call Stack 為空時,
把在 Task Queue 裡等待的第一個 callback function 放到 stack 中去執行。
好啦!解說到此,應該可以理解下面這張圖:
Web APIs function 執行路線圖
非同步可以解決的事
非同步的設計是讓需要執行較久時間的程式能夠移到別的地方去執行,
不要阻塞到 Main Thread(Call Stack),
讓程式能夠暢通在 Call Stack 上執行。
舉個實例,以前面提的滑鼠點擊為例
console.log('Started.')
$.on('button', 'click', function onClick () {
console.log('Buttton Clicked!')
})
console.log('Done.')
Started
console 會執行,會先進入Call Stack
中,再來執行滑鼠事件click
function,最後執行 consoleDone
click
為Web APIs
,所以之後會從Call Stack
脫離到Web APIs
滑鼠點擊後,callback 就會到
Task Queue
中。Event Loop
在偵測Call Stack
為空時,便會把 callback 放到Call Stack
中去執行,onClick
這個 function 就會執行
所以我們可以理解為在點擊滑鼠時,
onClick function 並不會立即執行,
會先到 Task Queue 中等待,
直到成為 Queue 中第一位且 Stack 為空時才會執行!
操作自 Loupe - by Philip Roberts: Loupe 是一個由 Philip 做的視覺化工具,對瞭解 Javascript function 執行流程很有幫助
以上為 JS 如何在 Single Thread
下實現看似擁有 Multi Thread
的效果。
再來幾個例子
筆者在學習這個概念查找資料時,發現了一場精彩演講:What the heck is the event loop anyway?
講解頗為精闢,於是下面擇幾個非同步相關例子與讀者分享。
非同步的 AJAX Request
Philip Roberts 曾在 JSConf 舉使用屬於 WebAPIs 的 AJAX 抓取資料的例子:
console.log('Hi')
$.get('url', function cb(data) {
console.log(data)
})
console.log('JSConfEU')
因為 AJAX 為 WebAPIs
其中一種,所以並不在 Javascript Runtime 中。
console
Hi
會先執行,再來 AJAX 執行請求,這時 callback 移到了Web APIs
,然後 consoleJSConfEU
緊接著執行AJAX 發出的 request 回應後(success 或 error),callback function 會移到
Task Queue
中Event Loop
在偵測Call Stack
為空時且 AJAX callback 在Task Queue
的第一位時,就會把 callback 丟到Call Stack
中執行
所以執行結果為:
Hi
JSConfEU
data(類型可能為 xml、html、text、script、json)
非同步讓 AJAX 從 sever 抓取資料到 client 的時間,可以讓 Javascript 並行(非同步)做別的事
,減少阻塞程式運行的機率。
setTimeOut 不是真的就在那幾秒之後執行 callback function
setTimeout(function timeout() {
console.log('5sec later?')
}, 5000)
點我看 Loupe 視覺化
這會怎麼執行呢?
setTimeOut
是屬於WebAPIs
,在執行setTimeOut
時,會從Call Stack
跳出至WebAPIs
執行callback function
經過 5 秒後會跳至Callback Queue
- 等待最後跳至
Call Stack
被執行
這整個過程中,其實已經花費不只 5 秒,也就是說 setTimeOut
只能保證最小執行時間是 5 秒
,但並不能說 執行時間是 5 秒
。
Callback Queue 阻塞:scroll event
scroll 屬於 WebAPIs
中 Events
的一種,一般在捲動滑鼠時,短時間內會頻繁的送出 scroll 的 event,造成 DOM 節點不斷的重新運算, Callback Queue
阻塞,滑動的頁面會非常的遲緩。
要解決這個問題,lodash 有提供 throttle 跟 debounce 兩種 method 可以快速方便解決。
結語
講到這裡,我們可以理解文章開頭所舉的例子了吧!
點我看 Loupe 視覺化
我們也可以清楚的知道 Javascript 如何非同步運作,達成同時具有同步以及非同步的特性。正因為如此,Javacript 是一個極為靈活彈性的語言,能夠悠遊於前端與後端,無往不利!