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