深入理解js event loop機制

問題

? ? 1. 單線程如何做到異步

? ? 2. 事件循環(huán)的過程是怎么樣的

? ? 3. macrotask和microtask是什么,它們有什么區(qū)別

單線程和異步

? ? js是單線程、異步的,那么單線程是如何做到異步呢?先了解下單線程和異步的關(guān)系。

? ? js的任務(wù)分為同步和異步兩種,它們的處理方式不同,同步任務(wù)是直接在主線程上排隊執(zhí)行,異步任務(wù)則會被放到任務(wù)隊列中,若有多個任務(wù)(異步任務(wù)) 則要在任務(wù)隊列中排隊等待,任務(wù)隊列類似一個緩沖區(qū),任務(wù)下一步會被移到調(diào)用棧(call stack),然后主線程執(zhí)行調(diào)用棧的任務(wù)。

? ? 單線程是指js引擎中負責解析執(zhí)行js代碼的線程只有一個(主線程),即每次只能做一件事,而我們知道一個ajax請求,主線程在等待它響應(yīng)的同時會去做其他事情,瀏覽器先在事件表注冊ajax的回調(diào)函數(shù),響應(yīng)回來后調(diào)用函數(shù)會被添加到任務(wù)隊列中等待執(zhí)行,不會造成線程阻塞,所有說js處理ajax請求的方式是異步的。

? ? 總而言之,檢查調(diào)用棧是否為空,以及確定把哪個task加入到調(diào)用棧的這個過程就是事件循環(huán),而js實現(xiàn)異步的核心就是事件循環(huán)。

調(diào)用棧和任務(wù)隊列

? ? 調(diào)用棧是一個棧結(jié)構(gòu),函數(shù)調(diào)用會形成一個棧幀,幀中包含了當前執(zhí)行函數(shù)的參數(shù)和局部變量等上下文信息,函數(shù)執(zhí)行完畢后,它的執(zhí)行上下文會從棧中彈出。

? ? 下圖是調(diào)用棧和任務(wù)隊列的關(guān)系圖

事件循環(huán)

? ? 關(guān)于事件循環(huán),HTML規(guī)范的介紹

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.

? ? 從規(guī)范理解,瀏覽器至少有一個事件循環(huán),一個事件循環(huán)至少有一個任務(wù)隊列(macrotask),每個任務(wù)都有自己的分組,瀏覽器會為不同的任務(wù)設(shè)置優(yōu)先級。

macrotask & microtask

? ? 規(guī)范提到兩個概念,但沒有詳細介紹,查閱一些資料大概可以總結(jié)如下:

? ? macrotask: 包含執(zhí)行整體的js代碼,事件回調(diào),XHR回調(diào),定時器(setTimeout/setInterval/setImmediate),IO操作,UI render

? ? microtask: 更新應(yīng)用程序狀態(tài)的任務(wù),包括promise調(diào)用,MutationObserver,process.nextTick,Object.observe

? ? 其中serImmediate和process.nextTick是nodejs的實現(xiàn)

事件處理過程

? ? 關(guān)于macrotask和microtask的理解,下面這張圖介紹非常清楚:

總結(jié)起來,一次事件循環(huán)的步驟包括:

1. 檢查macrotask隊列是否為空,非空則到2,為空則到3

2. 執(zhí)行macrotask中的一個任務(wù)

3. 繼續(xù)檢查microtask隊列是否為空,若有則為4,否則到5

4. 取出microtask中的任務(wù)執(zhí)行,執(zhí)行完成返回到步驟3

5. 取出視圖更新

macrotask & microtask的執(zhí)行順序

下面一段代碼感受:

控制臺輸出的log順序是什么?下圖分析:

? ? 首先,全局代碼(main())壓入調(diào)用棧執(zhí)行,打印start,接下來setTimeout壓入macrotask隊列,promise.thrn回調(diào)放入microtask隊列,最后執(zhí)行console.log('end'),打印end。

? ? 至此,調(diào)用棧中的代碼被執(zhí)行完成,回顧macrotask的定義,我們知道全局代碼屬于macrotask,macrotask執(zhí)行完,接下來就是執(zhí)行microtask隊列的任務(wù),執(zhí)行promise回調(diào)打印promise1。

? ? promise回調(diào)函數(shù)默認返回undefined,promise狀態(tài)變?yōu)閒ullfill觸發(fā)接下來的then回調(diào),繼續(xù)壓入microtask隊列,event loop會把當前的microtask隊列一直執(zhí)行完,此時執(zhí)行第二個promise.then回調(diào)打印出promise2。

? ? 這時microtask隊列已經(jīng)為空,從上面的流程圖可以知道,接下來主線程會做一些UI渲染工作(不一定會做),然后開始下一輪event loop,執(zhí)行setTimeout的回調(diào),打印setTimeout。這個過程會不斷重復(fù),也就是所謂的實際循環(huán)。

視圖渲染的時機

? ? 回顧上面的時機循環(huán)示意圖,update rendering(視圖渲染)發(fā)生在本輪事件循環(huán)的microtask隊列被執(zhí)行完之后,也就是說執(zhí)行任務(wù)的耗時會影響視圖渲染的時機。通常瀏覽器會以每秒60幀(60fps)的速度刷新頁面,據(jù)說這個幀率最適合人眼交互,大概16.7ms渲染一幀,所有如果要讓用戶覺得順暢,單個macrotask及它相關(guān)的所有microtask最好能在16.7ms內(nèi)完成。

? ? 不是每輪事件循環(huán)都會執(zhí)行視圖更新,瀏覽器有自己的優(yōu)化策略,例如把幾次的視圖更新累積到一起重繪,重繪之前會通知requestAnimationFrame執(zhí)行回調(diào)函數(shù),也就是說requestAnimationFrame回調(diào)的執(zhí)行時機是在一次或多次事件循環(huán)的UI render階段。

? ? 一下代碼可以驗證

運行結(jié)果如下:

總結(jié)

1.事件循環(huán)是js實現(xiàn)異步的核心

2.每輪事件循環(huán)分為3個步驟:

? 2.1 執(zhí)行macrotask隊列的一個任務(wù)

? 2.2 執(zhí)行完當前microtask隊列的所有任務(wù)

? 2.3 UI render

3.瀏覽器只保證requestAnimationFrame的回調(diào)在重繪之前執(zhí)行,沒有確定的時間,何時重繪由瀏覽器決定。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容