JavaScript異步拆解之Event-Loop+async/await
前言
在前端開發(fā)中,是不是經(jīng)常被JS的異步代碼繞暈?明明寫的代碼順序一樣,運(yùn)行結(jié)果卻大相徑庭?其實(shí)核心原因就在于——JS是單線程語(yǔ)言,而異步操作全靠「Event-Loop(事件循環(huán))」來(lái)調(diào)度。今天就結(jié)合具體代碼案例,從進(jìn)程線程、V8引擎到Event-Loop、async/await,一步步拆解,讓你搞懂JS異步的底層邏輯~
一、先基礎(chǔ):進(jìn)程 vs 線程
在聊JS異步之前,我們得先分清兩個(gè)容易混淆的概念:進(jìn)程和線程,這是理解后續(xù)內(nèi)容的基石~
進(jìn)程 :簡(jiǎn)單來(lái)說(shuō),進(jìn)程就是CPU運(yùn)行指令、加載和保存上下文所需的“容器”,是程序運(yùn)行的獨(dú)立單位。比如你打開瀏覽器,每多開一個(gè)Tab頁(yè),就相當(dāng)于多開啟了一個(gè)進(jìn)程,每個(gè)進(jìn)程之間相互獨(dú)立,互不干擾。
線程 :線程是進(jìn)程內(nèi)的執(zhí)行單元,是CPU實(shí)際執(zhí)行指令的“最小單位”。一個(gè)進(jìn)程可以包含多個(gè)線程,這些線程共享進(jìn)程的資源,協(xié)同完成任務(wù)。
舉個(gè)瀏覽器的例子:每個(gè)瀏覽器Tab進(jìn)程中,會(huì)包含多個(gè)核心線程,其中和我們JS相關(guān)的有3個(gè):
渲染線程:負(fù)責(zé)渲染頁(yè)面(HTML、CSS渲染);
JS引擎線程:負(fù)責(zé)執(zhí)行JS代碼;
HTTP請(qǐng)求線程:負(fù)責(zé)發(fā)送網(wǎng)絡(luò)請(qǐng)求。
這里有個(gè)關(guān)鍵知識(shí)點(diǎn)??:因?yàn)镴S代碼可以修改DOM(比如document.write、appendChild),如果JS引擎線程和渲染線程同時(shí)運(yùn)行,會(huì)導(dǎo)致頁(yè)面渲染混亂,所以JS引擎線程和渲染線程是互斥的——也就是說(shuō),JS代碼執(zhí)行時(shí),渲染線程會(huì)暫停,等JS執(zhí)行完,渲染線程才會(huì)繼續(xù)工作。這也是為什么有時(shí)候JS代碼寫得太復(fù)雜,頁(yè)面會(huì)出現(xiàn)“卡頓”的原因~
二、V8引擎:JS單線程的“幕后推手”
我們寫的JS代碼,最終是由V8引擎來(lái)執(zhí)行的。而V8引擎在執(zhí)行JS代碼時(shí),默認(rèn)只開啟一個(gè)JS引擎線程——這就意味著,JS代碼只能“自上而下、依次執(zhí)行”,同一時(shí)間只能做一件事。
那問(wèn)題來(lái)了:如果JS遇到耗時(shí)操作(比如setTimeout、網(wǎng)絡(luò)請(qǐng)求、讀取文件),難道要一直等著操作完成,再繼續(xù)執(zhí)行后續(xù)代碼嗎?這樣會(huì)導(dǎo)致頁(yè)面卡死,用戶體驗(yàn)直接拉胯!
為了解決這個(gè)問(wèn)題,JS引入了「異步機(jī)制」:?jiǎn)尉€程處理代碼時(shí),遇到同步任務(wù),就立即執(zhí)行;遇到異步任務(wù),不等待、不阻塞,而是把它暫時(shí)存放到“任務(wù)隊(duì)列”中,等JS引擎線程空閑時(shí),再去執(zhí)行任務(wù)隊(duì)列中的異步任務(wù)。
三、核心重點(diǎn):Event-Loop 事件循環(huán)
Event-Loop(事件循環(huán))就是JS處理異步任務(wù)的“調(diào)度器”,它的執(zhí)行流程決定了所有同步、異步代碼的運(yùn)行順序。我們先明確兩個(gè)核心概念:微任務(wù)和宏任務(wù)——所有異步任務(wù),都會(huì)被分到這兩個(gè)隊(duì)列中。
3.1 微任務(wù) vs 宏任務(wù)
微任務(wù)和宏任務(wù)的區(qū)別,在于它們的“優(yōu)先級(jí)”:微任務(wù)優(yōu)先級(jí)高于宏任務(wù),會(huì)先于宏任務(wù)執(zhí)行。
微任務(wù)(優(yōu)先級(jí)高) :
Promise.then()、Promise.catch()、Promise.finally()
process.nextTick()(Node.js環(huán)境,瀏覽器不支持)
MutationObserver(監(jiān)聽DOM變化的API)
宏任務(wù)(優(yōu)先級(jí)低) :
整個(gè)script腳本(最外層的同步代碼,屬于宏任務(wù)的開端)
setTimeout()、setInterval()
AJAX請(qǐng)求、I/O操作(比如讀取文件)
UI渲染(頁(yè)面渲染操作)
易錯(cuò)點(diǎn)提醒??:很多人會(huì)誤以為“setTimeout(fn, 0)”會(huì)立即執(zhí)行,其實(shí)不然——setTimeout的延遲時(shí)間是“最小延遲”,不是“精確延遲”,即使設(shè)為0,也會(huì)被放入宏任務(wù)隊(duì)列,等待同步代碼、微任務(wù)全部執(zhí)行完畢后,才會(huì)執(zhí)行。
3.2 Event-Loop 執(zhí)行順序
記住這個(gè)順序,就能搞定80%的異步代碼輸出題,結(jié)合后面的代碼案例理解更透徹??:
先執(zhí)行同步代碼(最外層script腳本,屬于宏任務(wù)),執(zhí)行過(guò)程中遇到異步任務(wù),就分別存入微任務(wù)隊(duì)列、宏任務(wù)隊(duì)列;
同步代碼執(zhí)行完畢后,清空微任務(wù)隊(duì)列(所有微任務(wù)依次執(zhí)行,執(zhí)行過(guò)程中產(chǎn)生的新微任務(wù),也會(huì)在本次微任務(wù)隊(duì)列中執(zhí)行完畢);
微任務(wù)全部執(zhí)行結(jié)束后,若有需要(如DOM發(fā)生變化),瀏覽器會(huì)進(jìn)行頁(yè)面渲染;
渲染完成后,從宏任務(wù)隊(duì)列中取出第一個(gè)宏任務(wù)執(zhí)行(執(zhí)行該宏任務(wù)的過(guò)程中,遇到同步、異步任務(wù),重復(fù)步驟1-2);
重復(fù)步驟1-4,形成“循環(huán)”,這就是Event-Loop。
3.3 代碼實(shí)操:搞懂Event-Loop執(zhí)行順序
結(jié)合你給出的第一段代碼,我們一步步拆解執(zhí)行過(guò)程,看看為什么輸出結(jié)果是「1 2 7 3 5 4 6」:
console.log(1); // 同步代碼:輸出1
new Promise((resolve) => {
console.log(2); // Promise構(gòu)造函數(shù)內(nèi)是同步代碼:輸出2
resolve()
})
.then(() => {
console.log(3); // 微任務(wù):存入微任務(wù)隊(duì)列
setTimeout(() => {
console.log(4); // 宏任務(wù):存入宏任務(wù)隊(duì)列(延遲0ms)
}, 0)
})
setTimeout(() => {
console.log(5); // 宏任務(wù):存入宏任務(wù)隊(duì)列(延遲0ms)
setTimeout(() => {
console.log(6); // 宏任務(wù):存入宏任務(wù)隊(duì)列(延遲0ms)
}, 0)
}, 0)
console.log(7); // 同步代碼:輸出7執(zhí)行步驟拆解??:
console.log(1):同步,輸出「1」;
new Promise:構(gòu)造函數(shù)內(nèi)是同步代碼,console.log(2),輸出「2」;調(diào)用resolve(),將then回調(diào)存入微任務(wù)隊(duì)列(記為微1);
遇到setTimeout(延遲0ms):宏任務(wù),存入宏任務(wù)隊(duì)列(記為宏1);
console.log(7):同步,輸出「7」;
同步代碼執(zhí)行完畢,開始清空微任務(wù)隊(duì)列:執(zhí)行微1(then回調(diào)),console.log(3),輸出「3」;遇到setTimeout(延遲0ms),宏任務(wù),存入宏任務(wù)隊(duì)列(記為宏2);
微任務(wù)隊(duì)列清空,渲染頁(yè)面(本次無(wú)明顯渲染);
執(zhí)行宏任務(wù)隊(duì)列第一個(gè)宏任務(wù)(宏2):console.log(5),輸出「5」;遇到setTimeout(延遲0ms),宏任務(wù),存入宏任務(wù)隊(duì)列(記為宏3);
宏2執(zhí)行完畢,再次檢查微任務(wù)隊(duì)列(無(wú)新微任務(wù)),執(zhí)行下一個(gè)宏任務(wù)(宏2):console.log(4),輸出「4」;
宏3執(zhí)行完畢,檢查微任務(wù)隊(duì)列(無(wú)),執(zhí)行下一個(gè)宏任務(wù)(宏3):console.log(6),輸出「6」。
所以最終輸出順序就是:1 2 7 3 5 4 6 ?
3.4 再練一題:鞏固Event-Loop
再看這段代碼??:
console.log(1);
setTimeout(() => {
console.log(2);
setTimeout(() => {
console.log(3)
}, 1000)
}, 0)
setTimeout(() => {
console.log(4)
}, 2000)
console.log(5);執(zhí)行步驟拆解??:
執(zhí)行同步代碼:
console.log(1)→ 輸出1;遇到第一個(gè)
setTimeout(..., 0):延遲 0ms 后,把回調(diào)(輸出 2 + 嵌套定時(shí)器)推入宏任務(wù)隊(duì)列;遇到第二個(gè)
setTimeout(..., 2000):延遲 2000ms 后,把回調(diào)(輸出 4)推入宏任務(wù)隊(duì)列;執(zhí)行同步代碼:
console.log(5)→ 輸出5;同步代碼執(zhí)行完畢,開始處理宏任務(wù)隊(duì)列:
- 取出第一個(gè)宏任務(wù):執(zhí)行 → 輸出
2; - 執(zhí)行中遇到嵌套的
setTimeout(..., 1000):延遲 1000ms 后,把回調(diào)(輸出 3)推入宏任務(wù)隊(duì)列;
- 取出第一個(gè)宏任務(wù):執(zhí)行 → 輸出
此時(shí)宏任務(wù)隊(duì)列里,只有「延遲 2000ms 的輸出 4」在等待;
時(shí)間流逝:
- 1000ms 到:
輸出3被推入宏任務(wù)隊(duì)列 → 立刻執(zhí)行 → 輸出3; - 再等 1000ms(總計(jì) 2000ms):
輸出4被推入宏任務(wù)隊(duì)列 → 執(zhí)行 → 輸出4。
- 1000ms 到:
最終輸出順序:1 → 5 → 2 → 3 → 4。
宏任務(wù)隊(duì)列是先進(jìn)先出,為什么 3 比 4 先輸出?
關(guān)鍵點(diǎn):兩個(gè)定時(shí)器不是同時(shí)入隊(duì):
- 輸出 4 的定時(shí)器:一開始就設(shè)定了 2000ms 延遲,2000ms 后才入隊(duì);
- 輸出 3 的定時(shí)器:等第一個(gè)宏任務(wù)執(zhí)行完(瞬間完成),才設(shè)定 1000ms 延遲,1000ms 后就入隊(duì)執(zhí)行。1000ms < 2000ms,所以
3必然比4先執(zhí)行,和宏任務(wù)隊(duì)列順序無(wú)關(guān)。
JavaScript 中setTimeout的延遲時(shí)間是回調(diào)函數(shù)加入宏任務(wù)隊(duì)列的等待時(shí)間,而非執(zhí)行時(shí)間;宏任務(wù)隊(duì)列遵循先進(jìn)先出,但不同定時(shí)器的回調(diào)不是同時(shí)入隊(duì),誰(shuí)的延遲時(shí)間先耗盡,誰(shuí)就先入隊(duì)先執(zhí)行。
四、進(jìn)階:async/await 異步語(yǔ)法糖
async/await 是ES7引入的異步語(yǔ)法,本質(zhì)是Promise的“語(yǔ)法糖”,讓異步代碼寫起來(lái)更像同步代碼,可讀性大大提升。
4.1 async/await 核心規(guī)則
async關(guān)鍵字:函數(shù)前面加async,等同于函數(shù)內(nèi)部自動(dòng)返回一個(gè)Promise實(shí)例對(duì)象。比如:
async function fn() { return 1; // 等同于 return Promise.resolve(1); } fn().then(res => console.log(res)); // 輸出1await關(guān)鍵字:必須跟async配合使用,不能單獨(dú)使用;如果await后面接的不是Promise對(duì)象,await就無(wú)法“約束”它,會(huì)直接執(zhí)行后續(xù)代碼;如果await后面接Promise對(duì)象,會(huì)“暫停”當(dāng)前async函數(shù)的執(zhí)行,等待Promise狀態(tài)變?yōu)閞esolved(成功)或rejected(失?。?,再繼續(xù)執(zhí)行后續(xù)代碼。
關(guān)鍵原理:await fn() 之所以能“當(dāng)成同步看待”,核心是——await會(huì)把它后續(xù)的代碼(當(dāng)前async函數(shù)內(nèi),await后面的所有代碼),擠到微任務(wù)隊(duì)列中,等await后面的Promise執(zhí)行完成后,再執(zhí)行這個(gè)微任務(wù)。
4.2 代碼實(shí)操:async/await 執(zhí)行順序
先看這段基礎(chǔ)代碼,理解async/await和Promise的關(guān)聯(lián):
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('a'); // 宏任務(wù)
resolve()
}, 1000)
})
}
function b() {
console.log('b'); // 同步
}
// 案例1:Promise.then寫法
a().then(() => {
b()
})
console.log('hello'); // 同步輸出順序:hello → a → b(同步代碼先執(zhí)行,a()是Promise,then回調(diào)是微任務(wù),等待a()的宏任務(wù)執(zhí)行完,再執(zhí)行微任務(wù)b())
再看async/await寫法,對(duì)比差異:
function a() {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('a'); // 宏任務(wù)
resolve()
}, 1000)
})
}
function b() {
console.log('b'); // 同步
}
async function foo() {
setTimeout(() => {
console.log('c'); // 宏任務(wù)(延遲1500ms)
}, 1500)
await a() // 等待a()的Promise resolve,后續(xù)代碼進(jìn)入微任務(wù)
b()
console.log('hello');
}
foo()執(zhí)行步驟拆解??:
調(diào)用foo(),執(zhí)行async函數(shù)內(nèi)部代碼;
遇到setTimeout(延遲1500ms):宏任務(wù),存入宏任務(wù)隊(duì)列(記為宏A);
遇到await a():a()返回Promise,里面有setTimeout(延遲1000ms,宏任務(wù),記為宏B);此時(shí)foo()暫停執(zhí)行,等待宏B執(zhí)行完畢、Promise resolve;
同步代碼執(zhí)行完畢(此時(shí)foo()暫停,無(wú)其他同步代碼),檢查微任務(wù)隊(duì)列(無(wú)),執(zhí)行宏任務(wù)隊(duì)列;
先執(zhí)行宏B(延遲1000ms):console.log('a'),輸出「a」;調(diào)用resolve(),此時(shí)await等待結(jié)束,將foo()后續(xù)的代碼(b()、console.log('hello'))存入微任務(wù)隊(duì)列;
宏B執(zhí)行完畢,檢查微任務(wù)隊(duì)列,執(zhí)行微任務(wù):b()輸出「b」,console.log('hello')輸出「hello」;
微任務(wù)執(zhí)行完畢,執(zhí)行下一個(gè)宏任務(wù)(宏A,延遲1500ms):console.log('c'),輸出「c」。
最終輸出順序:a → b → hello → c ?
4.3 綜合案例:async/await + Promise + setTimeout
console.log('script start'); // 同步
async function async1() {
await async2() // 等待async2()執(zhí)行,后續(xù)代碼進(jìn)入微任務(wù)
console.log('async1 end'); // 微任務(wù)
}
async function async2() {
console.log('async2 end'); // 同步(async函數(shù)內(nèi),await前的代碼是同步)
}
async1()
setTimeout(() => {
console.log('setTimeout'); // 宏任務(wù)
}, 0)
new Promise((resolve, reject) => {
console.log('promise'); // 同步
resolve()
})
.then(() => {
console.log('then1'); // 微任務(wù)
})
.then(() => {
console.log('then2'); // 微任務(wù)(then1執(zhí)行完后存入)
});
console.log('script end'); // 同步輸出順序:script start → async2 end → promise → script end → async1 end → then1 → then2 → setTimeout
?? 關(guān)鍵提醒:async函數(shù)內(nèi),await前面的代碼是同步執(zhí)行的;await后面的代碼會(huì)被放入微任務(wù)隊(duì)列,和Promise.then的微任務(wù)優(yōu)先級(jí)相同,按順序執(zhí)行。
五、總結(jié):異步核心知識(shí)點(diǎn)梳理
1. JS是單線程,由V8引擎的JS引擎線程執(zhí)行,與渲染線程互斥;
2. 異步任務(wù)分為微任務(wù)(優(yōu)先級(jí)高)和宏任務(wù)(優(yōu)先級(jí)低);
3. Event-Loop執(zhí)行順序:同步代碼 → 微任務(wù)隊(duì)列 → 頁(yè)面渲染 → 宏任務(wù)隊(duì)列(循環(huán));
4. async/await是Promise語(yǔ)法糖,await會(huì)將后續(xù)代碼放入微任務(wù)隊(duì)列,等待Promise resolve后執(zhí)行。
到此這篇關(guān)于JavaScript異步拆解之Event-Loop+async/await的文章就介紹到這了,更多相關(guān)JS異步Event-Loop+async/await內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript判斷數(shù)據(jù)類型的四種方式總結(jié)
JavaScript 作為一門動(dòng)態(tài)語(yǔ)言,其靈活性是把雙刃劍,一方面帶來(lái)了開發(fā)的便利性,另一方面也給我們?cè)陬愋团袛鄷r(shí)帶來(lái)了挑戰(zhàn),特別是在處理類型轉(zhuǎn)換和隱式轉(zhuǎn)換的時(shí)候,所以本篇文章我們將探討 JavaScript 中的數(shù)據(jù)類型判斷方式及在實(shí)際項(xiàng)目中的應(yīng)用,需要的朋友可以參考下2025-04-04
PHP實(shí)現(xiàn)基于Redis的MessageQueue隊(duì)列封裝操作示例
這篇文章主要介紹了PHP實(shí)現(xiàn)基于Redis的MessageQueue隊(duì)列封裝操作,結(jié)合實(shí)例形式分析了Redis的PHP消息隊(duì)列封裝與使用相關(guān)操作技巧,需要的朋友可以參考下2019-02-02
AJAX XMLHttpRequest對(duì)象創(chuàng)建使用詳解
這篇文章主要介紹了AJAX XMLHttpRequest對(duì)象創(chuàng)建使用詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08
微信小程序修改swiper默認(rèn)指示器樣式的實(shí)例代碼
這篇文章主要介紹了微信小程序修改swiper默認(rèn)指示器樣式的實(shí)例代碼,代碼塊是從微信開發(fā)文檔中心復(fù)制的代碼塊,在此基礎(chǔ)上修改官方swiper樣式,需要的朋友可以參考下2018-07-07
JavaScript設(shè)置IFrame高度自適應(yīng)(兼容各主流瀏覽器)
IFrame高度的設(shè)置問(wèn)題一直都是前端的噩夢(mèng)而且還要兼容各主流瀏覽器更是難上加難了,下面與大家分享下一個(gè)不錯(cuò)的技巧,感興趣的你可以參考下哈2013-06-06
JavaScript數(shù)組與對(duì)象方法完全指南
JavaScript 中的數(shù)組和對(duì)象是開發(fā)中最常用的數(shù)據(jù)結(jié)構(gòu),掌握它們的方法對(duì)于高效編程至關(guān)重要,本文將深入解析各種數(shù)組和對(duì)象方法,提供詳細(xì)的概念解釋、參數(shù)說(shuō)明和實(shí)用代碼示例,并按照功能進(jìn)行分組展示,需要的朋友可以參考下2025-06-06

