#?深入理解?js?事件循環(huán)機(jī)制(瀏覽器篇)
javascript?eventloop
-?拋在前面的問題:
??單線程如何做到異步树灶?
??事件循環(huán)的過程是怎樣的喊式?
??macrotask?和?microtask?是什么硼莽,它們有何區(qū)別橄杨?
-?單線程和異步
??提到?js莺葫,就會(huì)想到單線程片习,異步捌肴,那么單線程是如何做到異步的呢?概念先行藕咏,先要了解下單線程和異步之間的關(guān)系状知。
??1.?js?的任務(wù)分為?【同步】?和?【異步】?兩種。
??2.?它們的處理方式也不同孽查,同步任務(wù)是直接在主線程上排隊(duì)執(zhí)行饥悴,異步任務(wù)則會(huì)被放到【任務(wù)隊(duì)列】中。
??3.?若有多個(gè)任務(wù)(異步任務(wù))則要在【任務(wù)隊(duì)列】中排隊(duì)等待盲再,【任務(wù)隊(duì)列】類似一個(gè)緩沖區(qū)西设,任務(wù)完成會(huì)被移到【調(diào)用棧(call?stack)】,然后由主線程執(zhí)行【調(diào)用棿鹋螅】的任務(wù)贷揽。
??4.?單線程是指?js?引擎中負(fù)責(zé)解析執(zhí)行?js?代碼的線程只有一個(gè)(主線程),即每次只能做一件事梦碗,而我們知道一個(gè)?ajax?請(qǐng)求禽绪,主線程在等待它響應(yīng)的同時(shí)是會(huì)去做其它事的,瀏覽器先在【事件表】注冊(cè)?ajax?的回調(diào)函數(shù)洪规,響應(yīng)回來后回調(diào)函數(shù)被添加到【任務(wù)隊(duì)列】中等待執(zhí)行印屁,不會(huì)造成線程阻塞,所以說?js?處理?ajax?請(qǐng)求的方式是異步的斩例。
??-?總而言之雄人,檢查【調(diào)用棧】是否為空樱拴,以及確定把哪個(gè)?task?加入調(diào)用棧的這個(gè)過程就是事件循環(huán)柠衍,而?[js?實(shí)現(xiàn)異步的核心就是事件循環(huán)]洋满。
-?調(diào)用棧和任務(wù)隊(duì)列
??-?調(diào)用棧是一個(gè)棧結(jié)構(gòu),函數(shù)調(diào)用會(huì)形成一個(gè)棧幀珍坊,幀中包含了當(dāng)前執(zhí)行函數(shù)的參數(shù)和局部變量等上下文信息牺勾,函數(shù)執(zhí)行完后,它的執(zhí)行上下文會(huì)從棧中彈出阵漏。
??-?任務(wù)隊(duì)列?是用來存放任務(wù)的驻民,如果存放的是異步任務(wù),當(dāng)任務(wù)完成之后(比如定時(shí)器到了時(shí)間)履怯,就會(huì)被移入到?調(diào)用棧回还,等待?主線程?順序執(zhí)行調(diào)用棧的每一個(gè)事件。
-?事件循環(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ī)范理解柠硕,瀏覽器至少有一個(gè)事件循環(huán),一個(gè)事件循環(huán)至少有一個(gè)任務(wù)隊(duì)列(一個(gè)宏任務(wù)的任務(wù)隊(duì)列?macrotask)运提,每個(gè)外任務(wù)都有自己的分組蝗柔,瀏覽器會(huì)為不同的任務(wù)組設(shè)置優(yōu)先級(jí)。
-?macrotask?&?microtask
??規(guī)范有提到兩個(gè)概念民泵,但沒有詳細(xì)介紹癣丧,查閱一些資料大概可總結(jié)如下:
??1.?(宏任務(wù))macrotask:包含執(zhí)行整體的?js?代碼,事件回調(diào)栈妆,XHR?回調(diào)胁编,定時(shí)器(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?的實(shí)現(xiàn)莽鸿,在?nodejs?篇會(huì)詳細(xì)介紹昧旨。
-?事件處理過程
??關(guān)于?macrotask?和?microtask?的理解,光這樣看會(huì)有些晦澀難懂祥得,結(jié)合事件循壞的機(jī)制理解清晰很多兔沃,下面這張圖可以說是介紹得非常清楚了。
??-?event-loop?事件循環(huán)機(jī)制.jpg
總結(jié)起來级及,一次事件循環(huán)的步驟包括:
1.?檢查?macrotask?隊(duì)列是否為空乒疏,非空則直接步驟?2,為空則直接步驟?3
2.?執(zhí)行?macrotask?中的一個(gè)任務(wù)
3.?繼續(xù)檢查?microtask?隊(duì)列是否為空饮焦,若有則直接步驟?4怕吴,否則直接步驟?5
4.?取出?microtask?中的任務(wù)執(zhí)行窍侧,執(zhí)行完成返回到步驟?3
5.?執(zhí)行視圖更新
???mactotask?&?microtask?的執(zhí)行順序?(一般事件循環(huán)執(zhí)行一次瀏覽器會(huì)有一個(gè)?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???//其實(shí)這里就是瀏覽器的多線程機(jī)制?可能是ui渲染線程伟件。
????setTimeout????VM110:4
????setTimeout2???VM110:8
*?過程詳解:?
??1.?首先,全局代碼(main())壓入調(diào)用棧執(zhí)行议经,打印?start斧账;
??2.?接下來?time1?壓入?macrotask?隊(duì)列,緊接著?time2?壓入?macrotask?隊(duì)列中煞肾;
??3.?promise.resolve()?壓入調(diào)用棧執(zhí)行,?但是promise.then?回調(diào)放入?microtask?隊(duì)列咧织,所以瀏覽器會(huì)先執(zhí)行?console.log(‘end’),打印出?end籍救;
??4.?執(zhí)行完同步事件開始執(zhí)行微任務(wù)习绢,也就是promise1,?promise2蝙昙。解釋:?調(diào)用棧中的代碼被執(zhí)行完成毯炮,回顧?macrotask?的定義,我們知道全局代碼屬于?macrotask耸黑,macrotask?執(zhí)行完桃煎,那接下來就是執(zhí)行?microtask?隊(duì)列的任務(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?隊(duì)列缺菌,event?loop?會(huì)把當(dāng)前的?microtask?隊(duì)列一直執(zhí)行完葫辐,此時(shí)執(zhí)行第二個(gè)?promise.then?回調(diào)打印出?promise2;
??5.?這時(shí)?microtask?隊(duì)列已經(jīng)為空伴郁,從上面的流程圖可以知道耿战,接下來主線程會(huì)去做一些?UI?渲染工作(不一定會(huì)做),然后開始下一輪?event?loop焊傅,執(zhí)行?setTimeout?的回調(diào)剂陡,打印出?setTimeout;根據(jù)執(zhí)行時(shí)間和執(zhí)行順序先后setTimeout,setTimeout2狐胎。
??6.?這個(gè)過程會(huì)不斷重復(fù)鸭栖,也就是所謂的事件循環(huán)。
*?視圖渲染的時(shí)機(jī)
回顧上面的事件循環(huán)示意圖握巢,update?rendering(視圖渲染)發(fā)生在本輪事件循環(huán)的?microtask?隊(duì)列被執(zhí)行完之后晕鹊,也就是說執(zhí)行任務(wù)的耗時(shí)會(huì)影響視圖渲染的時(shí)機(jī)。通常瀏覽器以每秒?60?幀(60fps)的速率刷新頁面,據(jù)說這個(gè)幀率最適合人眼交互溅话,大概?16.7ms?渲染一幀晓锻,所以如果要讓用戶覺得順暢,單個(gè)?macrotask?及它相關(guān)的所有?microtask?最好能在?16.7ms?內(nèi)完成飞几。
但也不是每輪事件循環(huán)都會(huì)執(zhí)行視圖更新带射,瀏覽器有自己的優(yōu)化策略,例如把幾次的視圖更新累積到一起重繪循狰,重繪之前會(huì)通知?requestAnimationFrame?執(zhí)行回調(diào)函數(shù)窟社,也就是說?requestAnimationFrame?回調(diào)的執(zhí)行時(shí)機(jī)是在一次或多次事件循環(huán)的?UI?render?階段。
以下代碼可以驗(yàn)證
????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')
??*?運(yùn)行結(jié)果截圖如下
????1.?運(yùn)行結(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.?運(yùn)行結(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?實(shí)現(xiàn)異步的核心
??-?每輪事件循環(huán)分為?3?個(gè)步驟:
????a)?執(zhí)行?macrotask?隊(duì)列的一個(gè)任務(wù)
????b)?執(zhí)行完當(dāng)前?microtask?隊(duì)列的所有任務(wù)
????c)?UI?render
??-?瀏覽器只保證?requestAnimationFrame?的回調(diào)在重繪之前執(zhí)行匣吊,沒有確定的時(shí)間,何時(shí)重繪由瀏覽器決定寸潦。
原文:?http://lynnelv.github.io/js-event-loop-browser色鸳。