JavaScript事件循環(huán)和任務(wù)隊(duì)列

引言

首先需要知道的是JavaScript是門\color{red}{單線程,非阻塞}的語言。之所以如此設(shè)計(jì),是因?yàn)镴avaScript主要應(yīng)用于瀏覽器的互動(dòng),即操作DOM。所以一次只能完成一件任務(wù)。如果有多個(gè)任務(wù),就必須排隊(duì),前面一個(gè)任務(wù)完成,再執(zhí)行后面一個(gè)任務(wù),以此類推。我們假設(shè)JavaScript是多線程的,那么多個(gè)線程同時(shí)進(jìn)行,兩個(gè)線程同時(shí)操作一個(gè)DOM,那么以誰的操作為準(zhǔn)呢?
\color{red}{非阻塞}是當(dāng)代碼需要進(jìn)行一項(xiàng)異步任務(wù)(無法立刻返回結(jié)果,需要花一定時(shí)間才能返回的任務(wù),如I/O事件)的時(shí)候,主線程會(huì)掛起(pending)這個(gè)任務(wù),然后在異步任務(wù)返回結(jié)果的時(shí)候再根據(jù)一定規(guī)則去執(zhí)行相應(yīng)的回調(diào)。
單線程雖然實(shí)現(xiàn)起來比較簡(jiǎn)單,執(zhí)行環(huán)境相對(duì)單純;但是只要有一個(gè)任務(wù)耗時(shí)很長(zhǎng),后面的任務(wù)都必須排隊(duì)等著,會(huì)拖延整個(gè)程序的執(zhí)行。因此為了解決這個(gè)問題Javascript語言將任務(wù)的執(zhí)行模式分成兩種:同步(Synchronous)和異步(Asynchronous)。

同步模式

就是后一個(gè)任務(wù)等待前一個(gè)任務(wù)結(jié)束,然后再執(zhí)行,程序的執(zhí)行順序與任務(wù)的排列順序是一致的、同步的。

異步模式

每一個(gè)任務(wù)有一個(gè)或多個(gè)回調(diào)函數(shù)(callback),前一個(gè)任務(wù)結(jié)束后,不是執(zhí)行隊(duì)列上的后一個(gè)任務(wù),而是執(zhí)行回調(diào)函數(shù);后一個(gè)任務(wù)則是不等前一個(gè)任務(wù)的回調(diào)函數(shù)的執(zhí)行而執(zhí)行,所以程序的執(zhí)行順序與任務(wù)的排列順序是不一致的、異步的。
"異步模式"非常重要。在瀏覽器端,耗時(shí)很長(zhǎng)的操作都應(yīng)該異步執(zhí)行,避免瀏覽器失去響應(yīng),最好的例子就是Ajax操作。在服務(wù)器端,"異步模式"甚至是唯一的模式,因?yàn)閳?zhí)行環(huán)境是單線程的,如果允許同步執(zhí)行所有http請(qǐng)求,服務(wù)器性能會(huì)急劇下降,很快就會(huì)失去響應(yīng)。

JavaScript為何能執(zhí)行異步任務(wù)

Javascript是單線程的,但是卻能執(zhí)行異步任務(wù),這主要是因?yàn)?JS 中存在\color{red}{事件循環(huán)}(Event Loop)和\color{red}{任務(wù)隊(duì)列}(Task Queue)。
示例

setTimeout(function(){
    console.log(2);
},0);
 
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});
 
console.log(6);
 
setTimeout(function(){
    console.log(7);
},0);
異步代碼測(cè)試結(jié)果.png

事件循環(huán)(Event Loop)

JS 會(huì)創(chuàng)建一個(gè)類似于 while (true) 的循環(huán),每執(zhí)行一次循環(huán)體的過程稱之為Tick。每次Tick的過程就是查看是否有待處理事件,如果有則取出相關(guān)事件及回調(diào)函數(shù)放入執(zhí)行棧中由主線程執(zhí)行。待處理的事件會(huì)存儲(chǔ)在一個(gè)任務(wù)隊(duì)列中,也就是每次Tick會(huì)查看任務(wù)隊(duì)列中是否有需要執(zhí)行的任務(wù)。

示例

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

事件循環(huán)會(huì)按照上圖所示的模式進(jìn)行操作,queue.waitForMessage() 會(huì)同步地等待消息到達(dá)(如果當(dāng)前沒有任何消息等待被處理)。

任務(wù)隊(duì)列

和事件循環(huán)聯(lián)系在一起的是任務(wù)隊(duì)列,
-所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧(execution context stack)。
-主線程之外,還存在一個(gè)”任務(wù)隊(duì)列”(task queue)。只要異步任務(wù)有了運(yùn)行結(jié)果,就在”任務(wù)隊(duì)列”之中放置一個(gè)事件。
-一旦”執(zhí)行?!敝械乃型饺蝿?wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取”任務(wù)隊(duì)列”,看看里面有哪些事件。那些對(duì)應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進(jìn)入執(zhí)行棧,開始執(zhí)行。
-主線程不斷重復(fù)上面的第三步。

異步任務(wù)

異步操作會(huì)將相關(guān)回調(diào)添加到任務(wù)隊(duì)列中。而不同的異步操作添加到任務(wù)隊(duì)列的時(shí)機(jī)也不同,如onclick, setTimeout,ajax 處理的方式都不同,這些異步操作是由瀏覽器內(nèi)核的webcore來執(zhí)行的,webcore包含下圖中的3種 webAPI,分別是DOM Binding、network、timer模塊。
-DOM Binding 模塊處理一些DOM綁定事件,如onclick事件觸發(fā)時(shí),回調(diào)函數(shù)會(huì)立即被-webcore添加到任務(wù)隊(duì)列中。
-network 模塊處理Ajax請(qǐng)求,在網(wǎng)絡(luò)請(qǐng)求返回時(shí),才會(huì)將對(duì)應(yīng)的回調(diào)函數(shù)添加到任務(wù)隊(duì)列中。
-timer 模塊會(huì)對(duì)setTimeout等計(jì)時(shí)器進(jìn)行延時(shí)處理,當(dāng)時(shí)間到達(dá)的時(shí)候,才會(huì)將回調(diào)函數(shù)添加到任務(wù)隊(duì)列中。


webApi.png

事件循環(huán)和任務(wù)隊(duì)列之間的關(guān)系

事件循環(huán)規(guī)范.png

規(guī)范中中提到,一個(gè)瀏覽器環(huán)境,只能有一個(gè)事件循環(huán),而一個(gè)事件循環(huán)可以多個(gè)任務(wù)隊(duì)列,每個(gè)任務(wù)都有一個(gè)任務(wù)源(Task source)。相同任務(wù)源的任務(wù),只能放到一個(gè)任務(wù)隊(duì)列中。
不同任務(wù)源的任務(wù),可以放到不同任務(wù)隊(duì)列中。
簡(jiǎn)單來說:一個(gè)事件循環(huán)可以有多個(gè)任務(wù)隊(duì)列,隊(duì)列之間可有不同的優(yōu)先級(jí),同一隊(duì)列中的任務(wù)按先進(jìn)先出的順序執(zhí)行,但是不保證多個(gè)任務(wù)隊(duì)列中的任務(wù)優(yōu)先級(jí),具體實(shí)現(xiàn)可能會(huì)交叉執(zhí)行。

不同任務(wù)隊(duì)列的優(yōu)先級(jí)

在異步代碼測(cè)試結(jié)果的圖中看到,代碼的執(zhí)行順序并不是按著,代碼書寫順序依次執(zhí)行的,這是因?yàn)椴煌漠惒饺蝿?wù)之間也有優(yōu)先級(jí)的區(qū)別。異步任務(wù)分為兩類,macrotask(宏任務(wù))和 microtask(微任務(wù))

宏任務(wù)(macro task)

script(你的全部JS代碼,“同步代碼”), setTimeout, setInterval, setImmediate, I/O,UI rendering

微任務(wù)(micro task)

process.nextTick,Promises(這里指瀏覽器原生實(shí)現(xiàn)的 Promise), Object.observe, MutationObserver

執(zhí)行順序

瀏覽器為了能夠使得JS內(nèi)部task與DOM任務(wù)能夠有序的執(zhí)行,會(huì)在一個(gè)task執(zhí)行結(jié)束后,在下一個(gè) task 執(zhí)行開始前,對(duì)頁面進(jìn)行重新渲染 (task->渲染->task->...),鼠標(biāo)點(diǎn)擊會(huì)觸發(fā)一個(gè)事件回調(diào),需要執(zhí)行一個(gè)宏任務(wù),然后解析HTMl。微任務(wù)通常來說就是需要在當(dāng)前 task 執(zhí)行結(jié)束后立即執(zhí)行的任務(wù),比如對(duì)一系列動(dòng)作做出反饋,或或者是需要異步的執(zhí)行任務(wù)而又不需要分配一個(gè)新的 task,這樣便可以減小一點(diǎn)性能的開銷。所有微任務(wù)總會(huì)在下一個(gè)宏任務(wù)之前全部執(zhí)行完畢。
所以,瀏覽器環(huán)境中,js執(zhí)行任務(wù)的流程是這樣的:
1.第一個(gè)事件循環(huán),先執(zhí)行script中的所有同步代碼(即 macrotask 中的第一項(xiàng)任務(wù))
2.再取出 microtask 中的全部任務(wù)執(zhí)行(先清空process.nextTick隊(duì)列,再清空promise.then隊(duì)列)
3.下一個(gè)事件循環(huán),再回到 macrotask 取其中的下一項(xiàng)任務(wù)
4.再重復(fù)2
5.反復(fù)執(zhí)行事件循環(huán)…

一些常見的異步任務(wù)

setTimeout()

將事件插入到了事件隊(duì)列,必須等到當(dāng)前代碼(執(zhí)行棧)執(zhí)行完,主線程才會(huì)去執(zhí)行它指定的回調(diào)函數(shù)。
當(dāng)主線程時(shí)間執(zhí)行過長(zhǎng),無法保證回調(diào)會(huì)在事件指定的時(shí)間執(zhí)行。
瀏覽器端每次setTimeout會(huì)有4ms的延遲,當(dāng)連續(xù)執(zhí)行多個(gè)setTimeout,有可能會(huì)阻塞進(jìn)程,造成性能問題。

setImmediate()

事件插入到事件隊(duì)列尾部,主線程和事件隊(duì)列的函數(shù)執(zhí)行完成之后立即執(zhí)行。和setTimeout(fn,0)的效果差不多。
服務(wù)端node提供的方法。瀏覽器端最新的api也有類似實(shí)現(xiàn):window.setImmediate,但支持的瀏覽器很少。

process.nextTick()

插入到事件隊(duì)列尾部,但在下次事件隊(duì)列之前會(huì)執(zhí)行。也就是說,它指定的任務(wù)總是發(fā)生在所有異步任務(wù)之前,當(dāng)前主線程的末尾。
大致流程:當(dāng)前”執(zhí)行?!钡奈膊卡C>下一次Event Loop(主線程讀取”任務(wù)隊(duì)列”)之前–>觸發(fā)process指定的回調(diào)函數(shù)。
服務(wù)器端node提供的辦法。用此方法可以用于處于異步延遲的問題。
可以理解為:此次不行,預(yù)約下次優(yōu)先執(zhí)行。

Promise

Promise本身是同步的立即執(zhí)行函數(shù), 當(dāng)在 executor 中執(zhí)行 resolve 或者 reject 的時(shí)候, 此時(shí)是異步操作, 會(huì)先執(zhí)行 then/catch 等,當(dāng)主棧完成后,才會(huì)去調(diào)用 resolve/reject 中存放的方法執(zhí)行,打印 p 的時(shí)候,是打印的返回結(jié)果,一個(gè) Promise 實(shí)例。

async await

Async/Await就是一個(gè)自執(zhí)行的generate函數(shù)。利用generate函數(shù)的特性把異步的代碼寫成“同步”的形式。
async 函數(shù)返回一個(gè) Promise 對(duì)象,當(dāng)函數(shù)執(zhí)行的時(shí)候,一旦遇到 await 就會(huì)先返回,等到觸發(fā)的異步操作完成,再執(zhí)行函數(shù)體內(nèi)后面的語句。可以理解為,是讓出了線程,跳出了 async 函數(shù)體。

參考資料

https://blog.csdn.net/happyqyt/article/details/90644667
https://blog.csdn.net/github_35549695/article/details/82390345
https://www.cnblogs.com/nayek/p/11729923.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/EventLoop#%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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