Node.js | 深入理解事件循環機制(瀏覽器篇)
問題:
- 單線程如何做到異步?
- 事件循環的過程是怎樣的?
macrotask
和microtask
是什麼,它們有何區別?
單線程和異步
提到js
,就會想到單線程、異步,那麼單線程是如何做到異步的呢?
概念先行,先要了解下單線程和異步之間的關係。
js
的任務分為同步和異步兩種,它們的處理方式也不同,同步任務是直接在主線程上排隊執行,異步任務則會被放到任務隊列中,若有多個任務(異步任務)則要在任務隊列中排隊等待,任務隊列類似一個緩衝區,任務下一步會被移到調用棧(call stack
),然後主線程執行調用棧的任務。
單線程是指js
引擎中負責解析執行js
程式的線程只有一個(主線程),即每次只能做一件事,而我們知道一個ajax
請求,主線程在等待它響應的同時是會去做其它事的,瀏覽器先在事件表註冊ajax
的回調函數,響應回來後回調函數被添加到任務隊列中等待執行,不會造成線程阻塞,所以說js
處理ajax
請求的方式是異步的。
總而言之,檢查調用棧是否為空,以及確定把哪個task
加入調用棧的這個過程就是事件循環,而js
實現異步的核心就是事件循環。
調用棧和任務隊列
顧名思義,調用棧是一個棧結構,函數調用會形成一個棧幀,幀中包含了當前執行函數的參數和局部變量等上下文訊息,函數執行完後,它的執行上下文會從棧中彈出。
下圖就是調用棧和任務隊列的關係圖:
事件循环
關於事件循環,HTML規範的介紹。
There must be at least one event loop per user agent, and at most one event loop per unit of related similar-origin browsing contexts.
An event loop has one or more task queues.
Each task is defined as coming from a specific task source.
從規範理解,瀏覽器至少有一個事件循環,一個事件循環至少有一個任務隊列(macrotask
),每個外任務都有自己的分組,瀏覽器會為不同的任務組設置優先級。
macrotask & microtask
規範有提到兩個概念:
macrotask
:包含執行整體的js
程式,事件回調,XHR
回調,定時器(setTimeout/setInterval/setImmediate
),I/O
操作,UI render
。microtask
:更新應用程序狀態的任務,包括promise
回調,MutationObserver
,process.nextTick
,Object.observe
。
其中setImmediate
和process.nextTick
是Node.js
的實現,在Node.js篇會詳細介紹。
事件處理過程
關於macrotask
和microtask
的理解,光這樣看會有些晦澀難懂,結合事件循壞的機制理解清晰很多,下面這張圖可以說是介紹得非常清楚了。
總結起來,一次事件循環的步驟包括:
- 檢查
macrotask
隊列是否為空,非空則到2
,為空則到3
- 執行
macrotask
中的一個任務 - 繼續檢查
microtask
隊列是否為空,若有則到4
,否則到5
- 取出
microtask
中的任務執行,執行完成返回到步驟3
- 執行視圖更新
mactotask & microtask 執行順序
範例:
1 | console.log('start') |
執行結果(瀏覽器):
1 | start |
動態說明:
首先,全局程式(main()
)壓入調用棧執行,印出start
。
接下來setTimeout
壓入macrotask
隊列,promise.then
回調放入microtask
隊列,最後執行console.log('end')
,印出出end
。
至此,調用棧中的程式被執行完成,回顧macrotask
的定義,我們知道全局程式屬於macrotask
,macrotask
執行完,那接下來就是執行microtask
隊列的任務了,執行promise
回調印出promise1
。
promise
回調函數默認返回undefined
,promise
狀態變為fullfill
觸發接下來的then
回調,繼續壓入microtask
隊列,event loop
會把當前的microtask
隊列一直執行完,此時執行第二個promise.then
回調印出出promise2
。
這時microtask
隊列已經為空,從上面的流程圖可以知道,接下來主線程會去做一些UI
渲染工作(不一定會做),然後開始下一輪event loop
,執行setTimeout
的回調,印出出setTimeout
。
這個過程會不斷重複,也就是所謂的事件循環。
視圖渲染的時機
回顧上面的事件循環示意圖,update rendering
(視圖渲染)發生在本輪事件循環的microtask
隊列被執行完之後,也就是說執行任務的耗時會影響視圖渲染的時機。通常瀏覽器以每秒60幀(60fps
)的速率刷新頁面,據說這個幀率最適合人眼交互,大概16.7ms
渲染一幀,所以如果要讓用戶覺得順暢,單個macrotask
及它相關的所有microtask
最好能在16.7ms
內完成。
但也不是每輪事件循環都會執行視圖更新,瀏覽器有自己的優化策略,例如把幾次的視圖更新累積到一起重繪,重繪之前會通知requestAnimationFrame
執行回調函數,也就是說requestAnimationFrame
回調的執行時機是在一次或多次事件循環的UI render
階段。
1 | setTimeout(function() {console.log('timer1')}, 0) |
執行結果(瀏覽器):
1 | promise 1 |
總結
1.事件循環是js實現異步的核心
2.每輪事件循環分為3個步驟:
a) 執行macrotask隊列的一個任務
b) 執行完當前microtask隊列的所有任務
c) UI render
3.瀏覽器只保證requestAnimationFrame的回調在重繪之前執行,沒有確定的時間,何時重繪由瀏覽器決定
註:以上參考了
深入理解js事件循环机制(浏览器篇)