深入理解 js 事件循環(huán)機制(瀏覽器篇)

#?深入理解?js?事件循環(huán)機制(瀏覽器篇)

javascript?eventloop

-?拋在前面的問題:

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

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

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

-?單線程和異步

??提到?js,就會想到單線程,異步,那么單線程是如何做到異步的呢?概念先行,先要了解下單線程和異步之間的關(guān)系。

??1.?js?的任務(wù)分為?【同步】?和?【異步】?兩種。

??2.?它們的處理方式也不同,同步任務(wù)是直接在主線程上排隊執(zhí)行,異步任務(wù)則會被放到【任務(wù)隊列】中。

??3.?若有多個任務(wù)(異步任務(wù))則要在【任務(wù)隊列】中排隊等待,【任務(wù)隊列】類似一個緩沖區(qū),任務(wù)完成會被移到【調(diào)用棧(call?stack)】,然后由主線程執(zhí)行【調(diào)用?!康娜蝿?wù)。

??4.?單線程是指?js?引擎中負(fù)責(zé)解析執(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)用會形成一個棧幀,幀中包含了當(dāng)前執(zhí)行函數(shù)的參數(shù)和局部變量等上下文信息,函數(shù)執(zhí)行完后,它的執(zhí)行上下文會從棧中彈出。

??-?任務(wù)隊列?是用來存放任務(wù)的,如果存放的是異步任務(wù),當(dāng)任務(wù)完成之后(比如定時器到了時間),就會被移入到?調(diào)用棧,等待?主線程?順序執(zhí)行調(diào)用棧的每一個事件。

-?事件循環(huán)

??1.?關(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ù)隊列(一個宏任務(wù)的任務(wù)隊列?macrotask),每個外任務(wù)都有自己的分組,瀏覽器會為不同的任務(wù)組設(shè)置優(yōu)先級。

-?macrotask?&?microtask

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

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

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

其中?setImmediate?和?process.nextTick?是?nodejs?的實現(xiàn),在?nodejs?篇會詳細(xì)介紹。

-?事件處理過程

??關(guān)于?macrotask?和?microtask?的理解,光這樣看會有些晦澀難懂,結(jié)合事件循壞的機制理解清晰很多,下面這張圖可以說是介紹得非常清楚了。

??-?event-loop?事件循環(huán)機制.jpg

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

1.?檢查?macrotask?隊列是否為空,非空則直接步驟?2,為空則直接步驟?3

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

3.?繼續(xù)檢查?microtask?隊列是否為空,若有則直接步驟?4,否則直接步驟?5

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

5.?執(zhí)行視圖更新

???mactotask?&?microtask?的執(zhí)行順序?(一般事件循環(huán)執(zhí)行一次瀏覽器會有一個?undefined)

-?看一段代碼感受下:

??console.log('start')

??var?time1?=?setTimeout(function()?{

??console.log('setTimeout')

??},?0);

??var?time2?=?setTimeout(function()?{

??console.log('setTimeout2')

??},?0);

??new?Promise(resolve?=>?{

????resolve();

????console.log(1);

??}).then(function()?{

??console.log('promise1')

??}).then(function()?{

??console.log('promise2')

??})

??console.log('end')

console?輸出的?log?順序是什么?結(jié)合上述的步驟分析,系不系?so?easy~:

????start????????VM110:1?

????1????????????VM110:13

????end??????????VM110:19

????promise1?????VM110:15

????promise2?????VM110:17

????undefined???//其實這里就是瀏覽器的多線程機制?可能是ui渲染線程。

????setTimeout????VM110:4

????setTimeout2???VM110:8

*?過程詳解:?

??1.?首先,全局代碼(main())壓入調(diào)用棧執(zhí)行,打印?start;

??2.?接下來?time1?壓入?macrotask?隊列,緊接著?time2?壓入?macrotask?隊列中;

??3.?promise.resolve()?壓入調(diào)用棧執(zhí)行,?但是promise.then?回調(diào)放入?microtask?隊列,所以瀏覽器會先執(zhí)行?console.log(‘end’),打印出?end;

??4.?執(zhí)行完同步事件開始執(zhí)行微任務(wù),也就是promise1,?promise2。解釋:?調(diào)用棧中的代碼被執(zhí)行完成,回顧?macrotask?的定義,我們知道全局代碼屬于?macrotask,macrotask?執(zhí)行完,那接下來就是執(zhí)行?microtask?隊列的任務(wù)了,執(zhí)行?promise?回調(diào)打印?promise1;promise?回調(diào)函數(shù)默認(rèn)返回?undefined,promise?狀態(tài)變?yōu)?fullfill?觸發(fā)接下來的?then?回調(diào),繼續(xù)壓入?microtask?隊列,event?loop?會把當(dāng)前的?microtask?隊列一直執(zhí)行完,此時執(zhí)行第二個?promise.then?回調(diào)打印出?promise2;

??5.?這時?microtask?隊列已經(jīng)為空,從上面的流程圖可以知道,接下來主線程會去做一些?UI?渲染工作(不一定會做),然后開始下一輪?event?loop,執(zhí)行?setTimeout?的回調(diào),打印出?setTimeout;根據(jù)執(zhí)行時間和執(zhí)行順序先后setTimeout,setTimeout2。

??6.?這個過程會不斷重復(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?階段。

以下代碼可以驗證

????setTimeout(function()?{console.log('timer1')},?0)

????requestAnimationFrame(function(){

????console.log('requestAnimationFrame')

????})

????setTimeout(function()?{console.log('timer2')},?0)

????new?Promise(function?executor(resolve)?{

????console.log('promise?1')

????resolve()

????console.log('promise?2')

????}).then(function()?{

????console.log('promise?then')

????})

????console.log('end')

??*?運行結(jié)果截圖如下

????1.?運行結(jié)果?1:

??????promise?1???????VM88:10?

??????promise?2???????VM88:12?

??????end?????????????VM88:17?

??????promise?then????VM88:14

??????undefined

??????requestAnimationFrame??VM88:4?

??????timer1?????????????????VM88:1?

??????timer2?????????????????VM88:7?

????2.?運行結(jié)果?2?:(還沒試出來)

??????promise?1???????

??????promise?2???????

??????end?????????????

??????promise?then????

??????undefined?//

??????timer1?????????????????

??????timer2?????????????????

??????requestAnimationFrame??

??*?可以看到,結(jié)果?1?中?requestAnimationFrame()是在一次事件循環(huán)后執(zhí)行,而在結(jié)果?2,它的執(zhí)行則是在三次事件循環(huán)結(jié)束后。

*?總結(jié)

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

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

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

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

????c)?UI?render

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

原文:?http://lynnelv.github.io/js-event-loop-browser。

最后編輯于
?著作權(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ù)。

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