详解队列在前端的应用,深剖JS中的事件循环Eventloop,再了解微任务和宏任务
隊(duì)列在前端中的應(yīng)用
- 一、隊(duì)列是什么
- 二、應(yīng)用場(chǎng)景
- 三、前端與隊(duì)列:事件循環(huán)與任務(wù)隊(duì)列
- 1、event loop
- 2、JS如何執(zhí)行
- 3、event loop過(guò)程
- 4、 DOM 事件和 event loop
- 5、event loop 總結(jié)
- 四、宏任務(wù)和微任務(wù)
- 1、引例
- 2、宏任務(wù)和微任務(wù)
- (1)常用的宏任務(wù)和微任務(wù)
- (2)宏任務(wù)和微任務(wù)的優(yōu)先級(jí)
- (3)代碼實(shí)現(xiàn)微任務(wù)和宏任務(wù)
- (4)event loop和DOM渲染
- (5)微任務(wù)、宏任務(wù)和DOM渲染的關(guān)系
- (6)為何微任務(wù)更早
- 五、結(jié)束語(yǔ)
隊(duì)列 在日常生活中的應(yīng)用非常廣泛,比如我們最熟悉不過(guò)的食堂排隊(duì)打飯、擊鼓傳花等等問(wèn)題。同時(shí),它在前端中的應(yīng)用也非常廣泛,比如,事件循環(huán) Event loop 、JS異步中的任務(wù)隊(duì)列。
所以呢,對(duì)于前端來(lái)說(shuō), 隊(duì)列 結(jié)構(gòu)是一個(gè)必學(xué)的知識(shí)點(diǎn)。在接下來(lái)的這篇文章中,將講解關(guān)于 隊(duì)列 在前端中的應(yīng)用。
一、隊(duì)列是什么
隊(duì)列是一種先進(jìn)先出(FIFO)的線性表。它只允許在表的一端進(jìn)行插入,而在另一端刪除元素。
二、應(yīng)用場(chǎng)景
- 需要先進(jìn)先出的場(chǎng)景。
- 比如:食堂排隊(duì)打飯、火車(chē)站排隊(duì)買(mǎi)票、JS異步中的任務(wù)隊(duì)列、計(jì)算最近請(qǐng)求次數(shù)……。
三、前端與隊(duì)列:事件循環(huán)與任務(wù)隊(duì)列
1、event loop
event loop,也被稱(chēng)為事件循環(huán)或事件輪詢。因?yàn)镴S是單線程運(yùn)行的,且異步需要基于回調(diào)來(lái)實(shí)現(xiàn),所以, event loop 就是異步回調(diào)的實(shí)現(xiàn)原理。
2、JS如何執(zhí)行
JS在程序中的執(zhí)行遵循以下規(guī)則:
- 從前到后,一行一行執(zhí)行
- 如果某一行執(zhí)行報(bào)錯(cuò),則停止下面代碼的執(zhí)行
- 先把同步代碼執(zhí)行完,再執(zhí)行異步
一起來(lái)看一個(gè)實(shí)例:
console.log('Hi');setTimeout(function cb1(){console.log('cb1'); //cb1 即callback回調(diào)函數(shù) }, 5000);console.log('Bye');//打印順序: //Hi //Bye //cb1從上例代碼中可以看到, JS 是先執(zhí)行同步代碼,所以先打印 Hi 和 Bye ,之后執(zhí)行異步代碼,打印出 cb1 。
以此代碼為例,下面開(kāi)始講解 event loop 的過(guò)程。
3、event loop過(guò)程
對(duì)于上面這段代碼,執(zhí)行過(guò)程如下圖所示。
從上圖中可以分析出這段代碼的運(yùn)行軌跡。首先 console.log('Hi') 是同步代碼,直接執(zhí)行并打印出 Hi 。接下來(lái)繼續(xù)執(zhí)行定時(shí)器 setTimeout ,定時(shí)器是異步代碼,所以這個(gè)時(shí)候?yàn)g覽器會(huì)將它交給 Web APIs 來(lái)處理這件事情,因此先把它放到 Web APIs 中,之后繼續(xù)執(zhí)行 console.log('Bye') , console.log('Bye') 是同步代碼,在調(diào)用堆棧 Call Stack 中執(zhí)行,打印出 Bye 。
到這里,調(diào)用堆棧 Call Stack 里面的內(nèi)容全部執(zhí)行完畢,當(dāng)調(diào)用堆棧的內(nèi)容為空時(shí),瀏覽器就會(huì)開(kāi)始去任務(wù)隊(duì)列尋找下一個(gè)任務(wù),此時(shí)任務(wù)隊(duì)列就會(huì)去 Web API 里面尋找任務(wù),遵循先進(jìn)先出原則,找到了定時(shí)器,且定時(shí)器里面是回調(diào)函數(shù) cb1 ,于是把回調(diào)函數(shù) cb1 傳入任務(wù)隊(duì)列中,此時(shí) Web API 也空了,任務(wù)隊(duì)列里面的任務(wù)就會(huì)傳入到調(diào)用堆棧里Call Stack 里執(zhí)行,最終打印出 cb1 。
4、 DOM 事件和 event loop
先來(lái)看兩段代碼。
console.log('Hi');setTimeout(function cb1(){console.log('cb1'); //cb 即 callback }, 5000);console.log('Bye');/*輸出結(jié)果: Hi Bye cb1 */ <button id = "btn1">提交</button><script> console.log('Hi');document.getElementById('btn1').click(function(e){console.log('button clicked');});console.log('Bye'); </script>/*輸出結(jié)果: Hi Bye button clicked */以上這兩段代碼中,第一段是關(guān)于 setTimeout 的事件循環(huán),第二段是關(guān)于 DOM 事件的事件循環(huán)。那有小伙伴就會(huì)有疑問(wèn)說(shuō), DOM 事件不是異步操作嗎,為什么輸出結(jié)果依然是在最后呢?
其實(shí), DOM 事件確實(shí)不是異步操作,但是它也使用回調(diào),基于 event loop 事件循環(huán)機(jī)制,所以當(dāng)我們點(diǎn)擊的時(shí)候,會(huì)觸發(fā) DOM 事件,并進(jìn)行打印。
總結(jié)下 DOM 事件和 event loop 的區(qū)別:
- JS 是單線程的;
- 異步( setTimeout , ajax 等)使用回調(diào),基于 event loop ;
- DOM 事件不是異步,但也使用回調(diào),基于 event loop 。
5、event loop 總結(jié)
初階認(rèn)識(shí)完event loop后,來(lái)做個(gè)總結(jié):
總結(jié)event loop 過(guò)程1
- 同步代碼,一行一行放在 Call Stack 執(zhí)行;
- 遇到異步,會(huì)先“記錄”下,等待時(shí)機(jī)(定時(shí)、網(wǎng)絡(luò)請(qǐng)求);
- 時(shí)機(jī)到了,就移動(dòng)到 Callback Queue。
總結(jié)event loop 過(guò)程2
- 如果 Call Stack 為空(即同步代碼執(zhí)行完),則 event Loop 開(kāi)始工作;
- 輪詢查找 Callback Queue ,如果有則移動(dòng)到 Call Stack 執(zhí)行;
- 然后繼續(xù)輪詢查找(跟永動(dòng)機(jī)一樣,不斷循環(huán)查找)。
四、宏任務(wù)和微任務(wù)
1、引例
我們先來(lái)看一段代碼。
console.log(100); setTimeout(() => {console.log(200); }); Promise.resolve().then(() => {console.log(300); }); console.log(400); /*** 打印結(jié)果:* 100* 400* 300* 200*/在上面這段代碼中,第一個(gè)和第二個(gè)打印結(jié)果是基于同步,我們都知道要打印 100 和 400 ,但是第三個(gè)和第四個(gè)打印結(jié)果,理論上按照打印順序應(yīng)該是 200 和 300 才是,為什么是打印 300 和 200 呢?這就涉及到一個(gè)宏任務(wù)和微任務(wù)的問(wèn)題。接下來(lái)將對(duì)宏任務(wù)和微任務(wù)進(jìn)行講解。
2、宏任務(wù)和微任務(wù)
(1)常用的宏任務(wù)和微任務(wù)
| 宏任務(wù) | script、setTimeout 、setInterval 、setImmediate、Ajax、DOM事件、I/O、UI Rendering |
| 微任務(wù) | process.nextTick()、Promise、async/await |
上述的 setTimeout 和 setInterval 等都是任務(wù)源,真正進(jìn)入任務(wù)隊(duì)列的是他們分發(fā)的任務(wù)。
注意: 微任務(wù)執(zhí)行時(shí)機(jī)比宏任務(wù)要早!!
(2)宏任務(wù)和微任務(wù)的優(yōu)先級(jí)
優(yōu)先級(jí)
- setTimeout = setInterval 一個(gè)隊(duì)列
- setTimeout > setImmediate
- process.nextTick > Promise
(3)代碼實(shí)現(xiàn)微任務(wù)和宏任務(wù)
for(const macroTask of macroTaskQueue){handleMacroTask();for(const microTask of microTaskQueue){handleMicroTask();} }(4)event loop和DOM渲染
在上面的主題三第4點(diǎn)中講過(guò), DOM 事件基于回調(diào),也是基于 event loop 機(jī)制的。那DOM事件在程序執(zhí)行到什么時(shí)候,才會(huì)渲染呢?
同樣來(lái)看這段代碼。
<button id = "btn1">提交</button><script> console.log('Hi');document.getElementById('btn1').click(function(e){console.log('button clicked');});console.log('Bye'); </script>/*輸出結(jié)果: Hi Bye button clicked */由上圖可知,當(dāng)程序調(diào)用棧空閑時(shí),程序會(huì)先嘗試去進(jìn)行 DOM 渲染,最后再觸發(fā) Event Loop 機(jī)制。所以,在上面的這段代碼中,程序會(huì)先打印同步代碼 Hi 和 Bye ,等待同步代碼打印完畢后,會(huì)再查找 DOM 事件,進(jìn)行渲染,最后再觸發(fā) event loop 。
總結(jié) event loop 和 DOM 渲染的關(guān)系:
-
在程序執(zhí)行的時(shí)候, JS 是單線程的,且和 DOM 渲染共用一個(gè)線程;
-
所以 JS 在執(zhí)行的時(shí)候,得留一些時(shí)機(jī)提供給 DOM 渲染。
-
每次 Call Stack 清空(即每次輪詢結(jié)束),表示同步任務(wù)執(zhí)行完成;
-
程序會(huì)一直給 DOM 重新渲染的機(jī)會(huì), DOM 結(jié)構(gòu)如有改變則重新渲染;
-
然后再去觸發(fā)下一次 Event Loop 。
(5)微任務(wù)、宏任務(wù)和DOM渲染的關(guān)系
先了解微任務(wù)、宏任務(wù)和 DOM 渲染的關(guān)系:
- 宏任務(wù): DOM 渲染后觸發(fā),如 setTimeout 。
- 微任務(wù): DOM 渲染前觸發(fā),如 Promise 。
我們先來(lái)演示現(xiàn)象,再追究其原理。
1)演示1
const $p1 = $('<p>一段文字</p>'); const $p2 = $('<p>一段文字</p>'); const $p3 = $('<p>一段文字</p>'); $('#container').append($p1).append($p2).append($p3);//微任務(wù):DOM 渲染前觸發(fā) Promise.resolve().then(() => {console.log('length', $('#container').children().length);alert('Promise then');//(alert 會(huì)阻斷 js 執(zhí)行, 也會(huì)阻斷 DOM 渲染,便于查看效果) });以上這段代碼中,瀏覽器顯示效果如下。
在圖中可以看出,微任務(wù) promise 在 DOM 渲染前就觸發(fā)了,所以 DOM 對(duì)應(yīng)的文字還沒(méi)顯示時(shí), Promise 就已經(jīng)打印。
2)演示2
const $p1 = $('<p>一段文字</p>'); const $p2 = $('<p>一段文字</p>'); const $p3 = $('<p>一段文字</p>'); $('#container').append($p1).append($p2).append($p3);//宏任務(wù):DOM 渲染后觸發(fā) setTimeout(() => {console.log('length1', $('#container').children().length);alert('SetTimeout');//(alert 會(huì)阻斷 js 執(zhí)行, 也會(huì)阻斷 DOM 渲染,便于查看效果) });以上這段代碼中,瀏覽器顯示效果如下。
在圖中可以看出,當(dāng) DOM 對(duì)應(yīng)的文字已經(jīng)顯示時(shí), setTimeout 彈框才出現(xiàn),所以宏任務(wù) setTimeout 是在 DOM 渲染后(即 DOM 渲染并顯示結(jié)束)才觸發(fā)。
講到這里,回到我們前面所說(shuō)的知識(shí)點(diǎn)。
- 宏任務(wù): DOM 渲染后觸發(fā),如 setTimeout 。
- 微任務(wù): DOM 渲染前觸發(fā),如 Promise 。
從上面的演示后,相信大家應(yīng)該明白了微任務(wù)、宏任務(wù)和 DOM 的關(guān)系。在第一個(gè)演示中,微任務(wù) Promise 在 DOM 還沒(méi)有渲染時(shí)就觸發(fā)了,所以微任務(wù)都是在 DOM 渲染前觸發(fā)。在第二個(gè)演示中,宏任務(wù) setTimeout 在文字顯示結(jié)束后才觸發(fā) alert ,所以微任務(wù)都是在 DOM 渲染后才進(jìn)行觸發(fā)。
(6)為何微任務(wù)更早
理解完微任務(wù)和宏任務(wù)與DOM的關(guān)系后,我們也大致基本了解了為什么微任務(wù)比宏任務(wù)更早。接下來(lái)我們?cè)趶?eventloop 層面來(lái)看,為什么微任務(wù)會(huì)比宏任務(wù)更早,為什么會(huì)在DOM渲染前就開(kāi)始觸發(fā)呢?
先用一張圖來(lái)表示。
微任務(wù)在執(zhí)行時(shí)不會(huì)經(jīng)過(guò) Web APIs ,它會(huì)把它放到一個(gè)叫做 micro task queue(即宏任務(wù)隊(duì)列)當(dāng)中。且微任務(wù)是ES6` 語(yǔ)法規(guī)定的,宏任務(wù)是由瀏覽器規(guī)定的,所以它會(huì)比宏任務(wù)更早。
到這里,我們講完了 event loop 以及與其相關(guān)的宏任務(wù)和微任務(wù),下面我們?cè)儆靡粡垐D來(lái)總結(jié)實(shí)際運(yùn)用的執(zhí)行順序。
從上圖中可以得出結(jié)論:
第一步,程序先程序 Call Stack 里面的內(nèi)容,待 Call Stack 清空時(shí),執(zhí)行當(dāng)前的微任務(wù);
第二步,程序找到微任務(wù)隊(duì)列的任務(wù),執(zhí)行微任務(wù);
第三步,待微任務(wù)執(zhí)行完畢后,嘗試執(zhí)行DOM渲染;
第四步, DOM 渲染結(jié)束后,觸發(fā) event loop ,執(zhí)行宏任務(wù)。
五、結(jié)束語(yǔ)
隊(duì)列在前端中的應(yīng)用可以算是很非常頻繁了。基本上我們寫(xiě)的異步函數(shù)在執(zhí)行過(guò)程中,都會(huì)涉及到事件循環(huán)問(wèn)題。且在前端的面試當(dāng)中,經(jīng)常會(huì)被問(wèn)到 event loop 、事件循環(huán)或者事件輪詢是什么,很多面試者就很容易在這塊內(nèi)容吃虧。相信通過(guò)上文的學(xué)習(xí),大家都對(duì) eventloop 、微任務(wù)和宏任務(wù)有了一個(gè)更深的認(rèn)識(shí)。
隊(duì)列在前端中的應(yīng)用就講到這里啦!如有不理解或者文章有誤歡迎評(píng)論區(qū)留言或私信我交流~
-
關(guān)注公眾號(hào) 星期一研究室 ,第一時(shí)間關(guān)注學(xué)習(xí)干貨,更多有趣的專(zhuān)欄待你解鎖~
-
如果這篇文章對(duì)你有用,記得點(diǎn)個(gè)贊加個(gè)關(guān)注再走哦~
-
我們下期見(jiàn)!🥂🥂🥂
總結(jié)
以上是生活随笔為你收集整理的详解队列在前端的应用,深剖JS中的事件循环Eventloop,再了解微任务和宏任务的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 小米、金山武汉园区即将建成,打造万人研发
- 下一篇: 王腾称Redmi K70系列每一款产品都