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