# 頭好痛的同步/非同步
###### tags: `學習筆記` `javascript`
在學習javascript的路程上,一定會碰上許多觀念很難釐清的問題,同步/非同步問題就是其中一個。
在理解這觀念之前,要先知道Javascript是**單執行緒**,單一個**call stack**
引用mdn文件講述的就是
> 主執行緒在同一時間只能做一件事情,直到目前操作完成為止其他的操作都會暫停(blocking)。
這其實就是**同步**的意思
那既然只有一個執行緒,那是要怎麼做到非同步呢?
就先來介紹幾個概念
## 爲什麼要用非同步
舉個例子,假設我們身體是同步運作的,當你在揮手時,你其他的身體器官必須要等到你的手揮完才能運作,當下這段時間就只能揮手,不能呼吸、講話,這很明顯會出大問題。
瀏覽器當然也是如此,如果操作瀏覽器是同步的情況下:
當呼叫一個api(jQuery的Ajax, axios...等),因為call stack 被當前的function佔據,就要等後端回傳資料才能執行,這個時候是不能進行任何操作的,包括渲染畫面,**只能等**
這樣可能要看到整個頁面跑好要等上數秒鐘,這樣是很不friendly的。
因此,為了讓我們只需要載入必須的資料就能顯示畫面,其餘的部分就慢慢執行,所以才需要使用非同步的方式。
## call stack
當js在執行function的時候,因為只有single-thread,所以可以理解成他是用一個stack在堆疊function的執行順序,我們稱之為**call stack**

在瀏覽器的開發者工具中,我們也可以發現call stack的蹤跡,相信在寫code的過程中或多或少都看過這種錯誤呈現方式

(圖片取用自JSConf的影片)
這就是call stack的trace,幫助你了解function的執行順序。
## Task Queue && Web APIs
先看一個簡單的例子
```javascript
console.log('1')
setTimeout(function callback() {
console.log('2')
}, 1000)
console.log('3')
```
此段程式碼的輸出結果會是什麼呢?
相信有寫過js的人應該都回得出來: `1 3 2`
咦? 奇怪,js既然是**單執行緒**,那`setTimeout`這種function是怎麼做到的呢?
如果是依照call stack的方式排序,那這順序又是怎麼來的呢?
光用說的很難明白,那就來看一下實際怎麼運行的吧

有發現嗎?function還是照著順序執行的,但`callback`卻憑空產生在call stack中,這裡就要來理解一下什麼是**Web APIs**
### Web APIs
要知道雖然js是單執行緒,但是瀏覽器不是啊,瀏覽器會擁有多個執行序在進行不同的事情,而js就可以透過呼叫的方式,去使用瀏覽器提供的API,這就稱為**Web API**
上述的`setTimeout`就是由瀏覽器提供的,而不是存在於js的V8引擎中,等於是外部的function。
### Task Queue
`註:雖然稱作Queue,但是其實結構是用sets實作,不過為說明方便,還是稱作Task Queue`
既然我們知道`setTimeout`是js呼叫外部提供的function,所以也不難理解爲何要傳入`callback` fucntion來當作參數,為的就是要在外部執行完成後的後續操作。
但如果外部執行的速度忽快忽慢,我們是不是就無法掌握輸出的順序,這樣寫出來的程式都是變的要碰運氣。
不用擔心,隨機性(race condition)這種事情,這當然是要想辦法解決的啦
所以我們會有一個地方專門接收外部回傳的callback,那就是**Task Queue**。
## Mircotasks Queue
`註:這裡真的是Queue`
為了因應開發者需要「以非同步的方式來執行同步」的指令,於是就有了**Mircotasks Queue**這個酷東西。
在早期瀏覽器為了讓開發者監控DOM,提供了一系列的Web APIs,這些稱作**Mutation Events**,主要都是寫成
```javascript
document.getElementById('list').addEventListener("DOMAttrModified", function(){
console.log('屬性被修改');
}, false);
```
而因為種種問題,後續又推出了`Mutation Observer`,就是**Mircotasks Queue**的應用,像Vue的`$nextTick`就是採用這種方式實作的。
至於大家最熟悉的Mircotasks Queue應用莫過於**Promise**了。
### Promise
`註:在此不講Promise的寫法,單純討論event loop中扮演的角色`
```javascript
Promise.resolve()
.then(() => console.log(1))
console.log(2)
```
想當然爾,輸出仍然會是`2 1`
Promise回傳的兩個callback: `resolve`, `reject` 就是擺在Mircotasks Queue。
> Promise內仍然是同步的喔~
> ```javascript
> Promise(() => {console.log(1)})
> console.log(2)
> //output: 1, 2
> ```
來做個簡單的測試,看看一下兩段code:
```javascript
function loop(){
setTimeout(loop, 1000);
}
loop()
```
```javascript
function loop(){
Promise.resolve().then(loop)
}
loop()
```
同樣都是無限迴圈,但結果卻有點不同。
接下來用loupe實際跑看看(JavaScript Visualizer 9000跑不動,因為他好像是要跑完code,才進行視覺化)
首先是setTimeout的(mac錄影的解析度太差請見諒)

會發現它其實不影響到渲染的操作,Render Queue還是不斷能運行。
由於Promise的一個沒支援,一個跑不動,只好用描述的方式。
**Promise會造成page的blocking**(如果有興趣可以自己開一個html page測試)
而這是為什麼?還記得原本所說的Mircotasks Queue的目的嗎?
是為了因應開發者需要「**以非同步的方式來執行同步**」,所以page仍然要等待Mircotask Queue的同步執行完成。
## event loop
至此,所有知識碎片都已收集完畢,該是時候把他們拼起來了,組合成的結果我們就叫做**event loop**。
event loop是由runtime提供的機制,和js本身是沒有關聯的,所以在瀏覽器上運行和node js上運行event loop其實流程會不一樣喔。
而在此講述的都是瀏覽器的機制。
簡單來說,**event loop**在做跟js有關的事情就是
1. 將function照順序塞入call stack中,同步執行
2. 當stack**為空**時,取出queue的第一個function丟入stack中執行直到結束
3. 執行所有的Mircrotasks
4. 畫面渲染,返回2
5. 持續執行直到stack和queue皆為空
在此提一下畫面渲染,在event loop中其實還有一個Render Queue,瀏覽器會不斷查看(大致上是16毫秒,根據硬體刷新率)是否可以渲染,他的優先級比Task Queue高,所以在**call stack清空**的時候產生的空檔,就能進行渲染。
當然event loop沒有那麼簡單,還有一堆步驟需要去判斷,如animation queue,在此我只列出跟此文比較重大關聯的步驟。
回到JS,所以即使使用`setTimeout`設定0秒就callback,也是要先被放入queue中等待stack的function執行完才會執行。
最後,再用個簡單的例子做結尾,順便可以測試看看自己有沒有懂:
```javascript
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
})
```
結果是:
```
promise
setTimeout
```
簡單描述一下步驟:
1. 掃同步程式碼,把setTimeout擺到Task Queue
2. 執行Promise,把Promise.resolve擺到 Mircrotasks Queue中
3. 執行Microtasks
4. 渲染,返回2
5. 取出task,執行task
## 結論
若簡單以程式來表達event loop,應該就是長成這樣:
```javascript=
// event loop
while(true){
queue = getNextQueue()
task = queue.pop()
execute(task)
while(microtaskQueue.hasTasks()){
doMircrotask()
}
if(isRenderTime()) render()
}
```
(參考至JSConf)
還有以下幾個重點:
* 同步執行完才跑非同步
* Task Queue => `setTimeout`, `setInterval`
* Mircotasks Queue => `Promise`, `process.nextTick`, `async function`
## 參考來源
- [所以說event loop到底是什麼玩意兒?| Philip Roberts | JSConf EU](https://www.youtube.com/watch?v=8aGhZQkoFbQ&t=252s&ab_channel=JSConf)
- [Jake Archibald on the web browser event loop, setTimeout, micro tasks, requestAnimationFrame, ...](https://www.youtube.com/watch?v=cCOL7MC4Pl0&t=28s&ab_channel=JSConf)
- [Further Adventures of the Event Loop - Erin Zimmer - JSConf EU 2018](https://www.youtube.com/watch?v=u1kqx6AenYw&ab_channel=JSConf)
- [HTML Living Standard#event-loops](https://html.spec.whatwg.org/#event-loops)
- 視覺化event loop的網頁:[JavaScript Visualizer 9000](https://www.jsv9000.app/)