事件循環(huán)分為兩種,分別是瀏覽器事件循環(huán)和node.js事件循環(huán),本文主要對瀏覽器事件循環(huán)進(jìn)行描述恭取。
我們都知道JavaScript是一門單線程語言,指主線程只有一個咽筋。Event Loop事件循環(huán)距误,其實就是JS引擎管理事件執(zhí)行的一個流程宪拥,具體由運行環(huán)境確定信夫。目前JS的主要運行環(huán)境有兩個窃蹋,瀏覽器和Node.js
Event loop
瀏覽器用來協(xié)調(diào)事件,用戶交互静稻,腳本警没,渲染,網(wǎng)絡(luò)的一種內(nèi)部機制振湾。
瀏覽器的事件循環(huán)分為同步任務(wù)和異步任務(wù)杀迹;所有同步任務(wù)都在call stack上執(zhí)行,形成一個函數(shù)調(diào)用棧(執(zhí)行棧)恰梢,而異步則先放到任務(wù)隊列(task queue)里佛南,任務(wù)隊列又分為宏任務(wù)(macro-task)與微任務(wù)(micro-task)。
call stack(LIFO后進(jìn)先出)
同步代碼的調(diào)用棧
- 每調(diào)用一個函數(shù)嵌言,解釋器就會把該函數(shù)的執(zhí)行上下文添加到調(diào)用棧并開始執(zhí)行;
- 正在調(diào)用棧中執(zhí)行的函數(shù)及穗,如果還調(diào)用了其他函數(shù)摧茴,那么新函數(shù)也會被添加到調(diào)用棧,并立即執(zhí)行埂陆;
- 當(dāng)前函數(shù)執(zhí)行完畢后苛白,解釋器會將其執(zhí)行上下文清除調(diào)用棧,繼續(xù)執(zhí)行剩余執(zhí)行上下文中的剩余代碼焚虱;
- 但分配的調(diào)用椆喝梗空間被占滿,會引發(fā)”堆棧溢出“的報錯鹃栽。
function a() {
console.log('a');
}
function b() {
console.log('b');
}
function c() {
console.log('c');
a();
b();
}
c();
/**
* 輸出結(jié)果:c a b
*/
執(zhí)行該段代碼的時候躏率,首先調(diào)用函數(shù)c()。因此function c() {}會被放入調(diào)用棧中,然后開始執(zhí)行函數(shù)c薇芝,執(zhí)行的第一個語句是console.log('c')蓬抄,因此解釋器也會將其放入調(diào)用棧中。當(dāng)console執(zhí)行完夯到,控制臺打印后嚷缭,調(diào)用棧會將其移除。接著調(diào)用a()耍贾,同理阅爽。當(dāng)b()執(zhí)行完畢,此時c()也算結(jié)束了荐开,調(diào)用棧將其移出优床。
task queue
一系列(異步)task的集合
注意:
task queue在數(shù)據(jù)結(jié)構(gòu)是一個集合,并不是一個隊列誓焦,因為事件循環(huán)處理模型會從選定的task queue獲取第一個可運行的任務(wù)胆敞,而不是順序上的第一個任務(wù)
同步任務(wù)的執(zhí)行,其實就是跟前面那個案例一樣杂伟,按照代碼順序和調(diào)用順序移层,支持進(jìn)入調(diào)用棧中并執(zhí)行,執(zhí)行結(jié)束后就移除調(diào)用棧赫粥。
而異步任務(wù)的執(zhí)行观话,首先它依舊會進(jìn)入調(diào)用棧中,然后發(fā)起調(diào)用越平,然后解釋器會將其響應(yīng)回調(diào)任務(wù)放入一個任務(wù)隊列频蛔,緊接著調(diào)用棧會將這個任務(wù)移除。當(dāng)主線程清空后秦叛,即所有同步任務(wù)結(jié)束后晦溪,解釋器會讀取任務(wù)隊列,并依次將已完成的異步任務(wù)加入調(diào)用棧中并執(zhí)行挣跋。
這里有個重點三圆,就是異步任務(wù)不是直接進(jìn)入任務(wù)隊列的。
宏任務(wù)和微任務(wù)都屬于異步任務(wù)避咆,它們的區(qū)別在于它們的執(zhí)行迅速舟肉。
- 宏任務(wù)macrotask(Task)
依次進(jìn)入task queue,包括:
- script(整塊代碼)
- setTimeout
- setInterval
- I/O
- UI rendering
- setImmediate(node環(huán)境)
- requestAnimationFrame(瀏覽器獨有)
- requestIdleCallback(瀏覽器獨有)
- 微任務(wù)microtask(Jobs)
是真的隊列(FIFO先進(jìn)先出隊列)
包括:
- new promise().then(回調(diào))
- MutationObserver(html5新特性查库,用于監(jiān)聽dom樹的變化)
- process.nextTick(node環(huán)境)
- async await(語法糖)
實宏任務(wù)隊列和微任務(wù)隊列的執(zhí)行路媚,就是事件循環(huán)的一部分了,所以放在這里一起說樊销。
事件循環(huán)的具體流程如下:
- 從宏任務(wù)隊列中整慎,按照入隊順序脏款,找到第一個執(zhí)行的宏任務(wù),放入調(diào)用棧院领,開始執(zhí)行弛矛;
- 執(zhí)行完該宏任務(wù)下所有同步任務(wù)后,即調(diào)用棧清空后比然,該宏任務(wù)被推出宏任務(wù)隊列丈氓,然后微任務(wù)隊列開始按照入隊順序,依次執(zhí)行其中的微任務(wù)强法,直至微任務(wù)隊列清空為止万俗;
- 當(dāng)微任務(wù)隊列清空后,一個事件循環(huán)結(jié)束饮怯;
- 接著從宏任務(wù)隊列中闰歪,找到下一個執(zhí)行的宏任務(wù),開始第二個事件循環(huán)蓖墅,直至宏任務(wù)隊列清空為止库倘。
這里有幾個重點:
- 當(dāng)我們第一次執(zhí)行的時候,解釋器會將整體代碼script放入宏任務(wù)隊列中论矾,因此事件循環(huán)是從第一個宏任務(wù)開始的教翩;
- 如果在執(zhí)行微任務(wù)的過程中,產(chǎn)生新的微任務(wù)添加到微任務(wù)隊列中贪壳,也需要一起清空饱亿;微任務(wù)隊列沒清空之前,是不會執(zhí)行下一個宏任務(wù)的闰靴。
console.log("a");
setTimeout(function () {
console.log("b");
}, 0);
new Promise((resolve) => {
console.log("c");
resolve();
})
.then(function () {
console.log("d");
})
.then(function () {
console.log("e");
});
console.log("f");
/**
* 輸出結(jié)果:a c f d e b
*/
首先彪笼,當(dāng)代碼執(zhí)行的時候,整體代碼script被推入宏任務(wù)隊列中蚂且,并開始執(zhí)行該宏任務(wù)配猫。
按照代碼順序,首先執(zhí)行console.log("a")
膘掰。
該函數(shù)上下文被推入調(diào)用棧章姓,執(zhí)行完后,即移除調(diào)用棧识埋。
接下來執(zhí)行setTimeout()
,該函數(shù)上下文也進(jìn)入調(diào)用棧中零渐。
因為setTimeout
是一個宏任務(wù)窒舟,因此將其callback
函數(shù)推入宏任務(wù)隊列中,然后該函數(shù)就被移除調(diào)用棧诵盼,繼續(xù)往下執(zhí)行惠豺。
緊接著是Promise
語句银还,先將其放入調(diào)用棧,然后接著往下執(zhí)行洁墙。
執(zhí)行console.log("c")
和resolve()
蛹疯,這里就不多說了。
接著來到new Promise().then()
方法热监,這是一個微任務(wù)捺弦,因此將其推入微任務(wù)隊列中。
這時new Promise
語句已經(jīng)執(zhí)行結(jié)束了孝扛,就被移除調(diào)用棧列吼。
接著做執(zhí)行console.log('f')
。
這時候苦始,script
宏任務(wù)已經(jīng)執(zhí)行結(jié)束了寞钥,因此被推出宏任務(wù)隊列。
緊接著開始清空微任務(wù)隊列了陌选。首先執(zhí)行的是Promise then
理郑,因此它被推入調(diào)用棧中。
然后開始執(zhí)行其中的console.log("d")
咨油。
執(zhí)行結(jié)束后您炉,檢測到后面還有一個then()
函數(shù),因此將其推入微任務(wù)隊列中臼勉。
此時第一個then()
函數(shù)已經(jīng)執(zhí)行結(jié)束了邻吭,就會移除調(diào)用棧和微任務(wù)隊列。
此時微任務(wù)隊列還沒被清空宴霸,因此繼續(xù)執(zhí)行下一個微任務(wù)囱晴。
執(zhí)行過程跟前面差不多,就不多說了瓢谢。
此時微任務(wù)隊列已經(jīng)清空了畸写,第一個事件循環(huán)已經(jīng)結(jié)束了。
接下來執(zhí)行下一個宏任務(wù)氓扛,即setTimeout callback
枯芬。
執(zhí)行結(jié)束后,它也被移除宏任務(wù)隊列和調(diào)用棧采郎。
這時候微任務(wù)隊列里面沒有任務(wù)千所,因此第二個事件循環(huán)也結(jié)束了。
宏任務(wù)也被清空了蒜埋,因此這段代碼已經(jīng)執(zhí)行結(jié)束了淫痰。
await
async關(guān)鍵字是將一個同步函數(shù)變成一個異步函數(shù),并將返回值變?yōu)閜romise整份。
而await可以放在任何異步的待错、基于promise的函數(shù)之前籽孙。在執(zhí)行過程中,它會暫停代碼在該行上火俄,直到promise完成犯建,然后返回結(jié)果值。而在暫停的同時瓜客,其他正在等待執(zhí)行的代碼就有機會執(zhí)行了
async function async1() {
console.log("a");
const res = await async2();
console.log("b");
}
async function async2() {
console.log("c");
return 2;
}
console.log("d");
setTimeout(() => {
console.log("e");
}, 0);
async1().then(res => {
console.log("f")
})
new Promise((resolve) => {
console.log("g");
resolve();
}).then(() => {
console.log("h");
});
console.log("i");
/**
* 輸出結(jié)果:d a c g i b h f e
*/
關(guān)于更多await和頁面渲染具體運行動圖适瓦,可以查看https://juejin.cn/post/6969028296893792286#heading-9,作者寫的很詳細(xì)忆家,容易理解
此文多處摘抄于此犹菇。