#?深入理解?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。