---
# System prepended metadata

title: 頭好痛的同步/非同步
tags: [學習筆記, javascript]

---

# 頭好痛的同步/非同步
###### 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**

![](https://i.imgur.com/3EPd7Ix.gif)

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

![](https://i.imgur.com/v4GQGzc.png =400x170)
(圖片取用自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的方式排序，那這順序又是怎麼來的呢?

光用說的很難明白，那就來看一下實際怎麼運行的吧

![](https://i.imgur.com/4MCKHlK.gif)

有發現嗎？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錄影的解析度太差請見諒)
![](https://i.imgur.com/rwTD3Tm.gif =1200x400)
會發現它其實不影響到渲染的操作，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/)