js異步編程的演變:回調(diào)函數(shù)、Promise、?async/await?(代碼原理演示)
前端開發(fā)里的異步場景:比如先請求“用戶信息”,再用用戶ID請求“訂單列表”,最后用訂單ID請求“訂單詳情”,同時(shí)不能讓頁面卡住(比如按鈕點(diǎn)不動(dòng)、滾動(dòng)不流暢)。而 async/await 就是解決這種場景的“最優(yōu)解”——讓異步代碼寫起來像“等快遞”一樣順理成章,卻不會(huì)阻塞主線程。
一、異步編程的“進(jìn)化史”:從“亂糟糟”到“清爽”
在async/await出現(xiàn)前,前端工程師為了處理異步任務(wù),踩過不少坑。我們以“按順序請求三個(gè)API”為例,看看每一代方案的問題。
1. 第一代:回調(diào)地獄(Callback Hell)——嵌套到“頭皮發(fā)麻”
最原始的異步處理靠回調(diào)函數(shù),比如用setTimeout模擬API請求:
// 模擬API請求:根據(jù)參數(shù)返回?cái)?shù)據(jù),1秒后執(zhí)行回調(diào)
function requestData(url, callback) {
setTimeout(() => {
// 模擬返回?cái)?shù)據(jù)(比如用戶信息、訂單等)
const data = `來自${url}的數(shù)據(jù)`;
callback(null, data); // 成功回調(diào):err為null,data為結(jié)果
}, 1000);
}
// 需求:先請求用戶信息,再請求訂單,最后請求詳情
requestData("/api/user", (err, userData) => {
if (err) return console.error("用戶請求失敗", err);
console.log("拿到用戶數(shù)據(jù):", userData);
// 用用戶數(shù)據(jù)里的userId請求訂單
requestData(`/api/orders?userId=${userData.id}`, (err, orderData) => {
if (err) return console.error("訂單請求失敗", err);
console.log("拿到訂單數(shù)據(jù):", orderData);
// 用訂單id請求詳情
requestData(`/api/orderDetail?orderId=${orderData.id}`, (err, detailData) => {
if (err) return console.error("詳情請求失敗", err);
console.log("拿到詳情數(shù)據(jù):", detailData);
});
});
});問題顯而易見:
- 嵌套金字塔:每多一個(gè)異步任務(wù),就多一層嵌套,代碼像“千層餅”,后期維護(hù)時(shí)找bug要“逐層扒”;
- 錯(cuò)誤處理麻煩:每個(gè)回調(diào)里都要寫
if(err),一旦漏寫就可能導(dǎo)致報(bào)錯(cuò)無法捕獲; - 邏輯分散:“先做A、再做B、最后做C”的邏輯被拆在不同回調(diào)里,閱讀時(shí)要“跳著看”。
2. 第二代:Promise 鏈?zhǔn)秸{(diào)用——“扁平”但仍需“手動(dòng)銜接”
為了解決回調(diào)地獄,Promise應(yīng)運(yùn)而生:它把異步任務(wù)包裝成一個(gè)“容器”,用.then()鏈?zhǔn)秸{(diào)用,讓代碼變扁平:
// 用Promise重寫請求函數(shù):不再需要回調(diào),直接返回Promise
function requestDataPromise(url) {
return new Promise((resolve) => {
setTimeout(() => {
const data = `來自${url}的數(shù)據(jù)`;
resolve(data); // 異步成功后,用resolve返回結(jié)果
}, 1000);
});
}
// 鏈?zhǔn)秸{(diào)用:按順序請求
requestDataPromise("/api/user")
.then((userData) => {
console.log("拿到用戶數(shù)據(jù):", userData);
// 返回下一個(gè)Promise,銜接下一個(gè).then
return requestDataPromise(`/api/orders?userId=${userData.id}`);
})
.then((orderData) => {
console.log("拿到訂單數(shù)據(jù):", orderData);
return requestDataPromise(`/api/orderDetail?orderId=${orderData.id}`);
})
.then((detailData) => {
console.log("拿到詳情數(shù)據(jù):", detailData);
})
.catch((err) => {
// 集中捕獲所有環(huán)節(jié)的錯(cuò)誤,不用每個(gè)步驟寫if(err)
console.error("某個(gè)請求失敗:", err);
});進(jìn)步很大,但仍有瑕疵:
- 雖然扁平了,但需要頻繁寫
.then(),代碼里穿插大量“銜接符”,不夠直觀; - 邏輯順序還是“鏈?zhǔn)降?rdquo;,如果任務(wù)多,
.then()鏈條會(huì)很長,閱讀時(shí)要“順著鏈條找”。
3. 第三代:async/await——“像寫同步代碼一樣寫異步”
終于到了async/await出場的時(shí)候。它是Promise的“語法糖”,但把異步代碼的可讀性拉到了頂峰:
// 用async/await重寫:邏輯順序和代碼順序完全一致
async function fetchAllData() {
try {
// 1. 先請求用戶數(shù)據(jù):await“等待”Promise完成,再往下走
const userData = await requestDataPromise("/api/user");
console.log("拿到用戶數(shù)據(jù):", userData);
// 2. 用用戶數(shù)據(jù)請求訂單:自然銜接,像同步代碼一樣
const orderData = await requestDataPromise(`/api/orders?userId=${userData.id}`);
console.log("拿到訂單數(shù)據(jù):", orderData);
// 3. 用訂單數(shù)據(jù)請求詳情
const detailData = await requestDataPromise(`/api/orderDetail?orderId=${orderData.id}`);
console.log("拿到詳情數(shù)據(jù):", detailData);
return "所有數(shù)據(jù)請求完成!";
} catch (err) {
// 集中捕獲所有await環(huán)節(jié)的錯(cuò)誤,和同步代碼的try/catch完全一致
console.error("請求失敗:", err);
}
}
// 調(diào)用async函數(shù)
console.log("程序開始執(zhí)行");
const result = fetchAllData();
console.log("等待數(shù)據(jù)請求..."); // 這行會(huì)先執(zhí)行,證明沒有阻塞
result.then((msg) => console.log(msg));輸出順序(關(guān)鍵!看“非阻塞”證明):
程序開始執(zhí)行 等待數(shù)據(jù)請求... // 1秒后(第一個(gè)請求完成) 拿到用戶數(shù)據(jù):來自/api/user的數(shù)據(jù) // 又1秒后(第二個(gè)請求完成) 拿到訂單數(shù)據(jù):來自/api/orders?userId=xxx的數(shù)據(jù) // 再1秒后(第三個(gè)請求完成) 拿到詳情數(shù)據(jù):來自/api/orderDetail?orderId=xxx的數(shù)據(jù) 所有數(shù)據(jù)請求完成!
核心優(yōu)勢直接“封神”:
- 代碼即邏輯:“先做A,再做B,最后做C”的邏輯,和同步代碼寫法完全一致,不用跳著看;
- 錯(cuò)誤處理簡單:用
try/catch包裹所有異步步驟,和處理同步錯(cuò)誤的方式一樣; - 完全不阻塞:等待異步任務(wù)時(shí),主線程會(huì)去執(zhí)行其他代碼(比如上面的“等待數(shù)據(jù)請求...”),頁面不會(huì)卡住。
二、async/await 到底是“怎么干活”的?本質(zhì)是 Generator 的“自動(dòng)版”
很多人只知道async/await好用,卻不知道它背后的“黑魔法”——其實(shí)它是 Generator函數(shù) + Promise自動(dòng)執(zhí)行器 的封裝。我們一步步拆解開看。
1. 先認(rèn)識“半成品”:Generator函數(shù)
Generator函數(shù)是ES6引入的一種“可暫停、可恢復(fù)”的函數(shù),用function*定義,內(nèi)部用yield關(guān)鍵字暫停執(zhí)行:
// Generator函數(shù):處理三個(gè)異步請求
function* fetchGenerator() {
// yield會(huì)暫停函數(shù),返回后面的Promise;恢復(fù)時(shí),把Promise的結(jié)果賦值給userData
const userData = yield requestDataPromise("/api/user");
console.log("拿到用戶數(shù)據(jù):", userData);
const orderData = yield requestDataPromise(`/api/orders?userId=${userData.id}`);
console.log("拿到訂單數(shù)據(jù):", orderData);
const detailData = yield requestDataPromise(`/api/orderDetail?orderId=${orderData.id}`);
console.log("拿到詳情數(shù)據(jù):", detailData);
return "完成";
}但Generator有個(gè)“缺點(diǎn)”:需要手動(dòng)“驅(qū)動(dòng)”它執(zhí)行
Generator函數(shù)調(diào)用后不會(huì)直接執(zhí)行,而是返回一個(gè)“迭代器(iterator)”,需要調(diào)用iterator.next()才能讓函數(shù)繼續(xù)執(zhí)行:
// 手動(dòng)驅(qū)動(dòng)Generator執(zhí)行
const iterator = fetchGenerator();
// 第一次調(diào)用next():函數(shù)執(zhí)行到第一個(gè)yield,返回{ value: Promise, done: false }
iterator.next().value.then((userData) => {
// 第二次調(diào)用next(userData):把userData傳給第一個(gè)yield的左邊,函數(shù)執(zhí)行到第二個(gè)yield
iterator.next(userData).value.then((orderData) => {
// 第三次調(diào)用next(orderData):函數(shù)執(zhí)行到第三個(gè)yield
iterator.next(orderData).value.then((detailData) => {
// 第四次調(diào)用next(detailData):函數(shù)執(zhí)行完,done變?yōu)閠rue
iterator.next(detailData);
});
});
});你看,Generator已經(jīng)實(shí)現(xiàn)了“暫停異步任務(wù)、按順序執(zhí)行”,但需要手動(dòng)寫嵌套的.then()來驅(qū)動(dòng)——這顯然不夠方便。
2. 給Generator加個(gè)“自動(dòng)檔”:寫一個(gè)簡單的自動(dòng)執(zhí)行器
既然手動(dòng)驅(qū)動(dòng)太麻煩,我們可以寫一個(gè)函數(shù),自動(dòng)幫我們調(diào)用next(),直到Generator執(zhí)行完成:
// Generator自動(dòng)執(zhí)行器:接收Generator函數(shù),自動(dòng)驅(qū)動(dòng)它完成
function runGenerator(generatorFunc) {
// 1. 創(chuàng)建迭代器
const iterator = generatorFunc();
// 2. 遞歸調(diào)用next()
function autoNext(value) {
// 執(zhí)行next(),拿到{ value: Promise, done: 布爾值 }
const result = iterator.next(value);
// 如果執(zhí)行完了,就退出
if (result.done) return;
// 如果沒執(zhí)行完,等待Promise完成后,遞歸調(diào)用autoNext
result.value.then((data) => {
autoNext(data); // 把Promise的結(jié)果傳給下一個(gè)next()
}).catch((err) => {
iterator.throw(err); // 捕獲錯(cuò)誤,傳給Generator的try/catch
});
}
// 啟動(dòng)自動(dòng)執(zhí)行
autoNext();
}
// 現(xiàn)在,調(diào)用自動(dòng)執(zhí)行器就夠了,不用手動(dòng)寫嵌套!
runGenerator(fetchGenerator);這下Generator終于“自動(dòng)化”了——而 async/await 本質(zhì)上就是把“Generator函數(shù) + 自動(dòng)執(zhí)行器”封裝成了更簡潔的語法。
3. async/await 是怎么“封裝”的?
我們可以把async function理解為“自帶自動(dòng)執(zhí)行器的Generator函數(shù)”,瀏覽器或Node.js內(nèi)部幫我們做了這些事:
async關(guān)鍵字:告訴引擎“這是一個(gè)需要自動(dòng)執(zhí)行的異步函數(shù)”,調(diào)用時(shí)會(huì)自動(dòng)創(chuàng)建迭代器并驅(qū)動(dòng)執(zhí)行;await關(guān)鍵字:相當(dāng)于yield的“語法糖”,自動(dòng)等待后面的Promise完成,并把結(jié)果返回,不用手動(dòng)處理next();- 錯(cuò)誤處理:內(nèi)部自動(dòng)捕獲Promise的
reject狀態(tài),拋給外層的try/catch,不用手動(dòng)寫iterator.throw()。
簡單來說:async/await = Generator函數(shù) + 內(nèi)置自動(dòng)執(zhí)行器 + 更友好的語法。
三、關(guān)鍵誤區(qū):async/await 不是“同步”,而是“偽同步”
很多人用多了async/await,會(huì)誤以為它是“同步代碼”——但實(shí)際上它只是“看起來同步”,本質(zhì)還是異步,不會(huì)阻塞主線程。我們用一個(gè)“事件循環(huán)”的例子來證明:
console.log("1. 全局同步代碼開始");
// async函數(shù)
async function asyncDemo() {
console.log("2. async函數(shù)內(nèi)部同步代碼");
// await后面的Promise會(huì)暫停函數(shù),把后續(xù)代碼放進(jìn)“微任務(wù)隊(duì)列”
await Promise.resolve("模擬異步完成");
console.log("5. await之后的代碼(微任務(wù))");
}
// 調(diào)用async函數(shù)
asyncDemo();
// 其他同步代碼
console.log("3. 全局同步代碼繼續(xù)");
setTimeout(() => {
console.log("6. setTimeout回調(diào)(宏任務(wù))");
}, 0);
console.log("4. 全局同步代碼結(jié)束");最終輸出順序:
1. 全局同步代碼開始 2. async函數(shù)內(nèi)部同步代碼 3. 全局同步代碼繼續(xù) 4. 全局同步代碼結(jié)束 5. await之后的代碼(微任務(wù)) 6. setTimeout回調(diào)(宏任務(wù))
為什么會(huì)這樣?拆解執(zhí)行流程:
- 主線程先執(zhí)行所有同步代碼:1→2→3→4;
- 遇到
await Promise.resolve()時(shí),引擎會(huì):
- 暫停
asyncDemo函數(shù),把await后面的代碼(console.log("5..."))放進(jìn)“微任務(wù)隊(duì)列”; - 主線程繼續(xù)執(zhí)行其他同步代碼(3→4);
- 同步代碼執(zhí)行完后,主線程會(huì)清空“微任務(wù)隊(duì)列”,執(zhí)行console.log("5...");
- 微任務(wù)執(zhí)行完后,再執(zhí)行“宏任務(wù)隊(duì)列”里的setTimeout回調(diào)(6)。
這就證明了:await不會(huì)阻塞主線程,它只是讓函數(shù)內(nèi)部的代碼“按順序等”,主線程該干嘛干嘛——這就是“偽同步”的本質(zhì)。
四、實(shí)際開發(fā)中的“避坑指南”
async/await雖好,但新手容易踩坑,分享兩個(gè)高頻注意點(diǎn):
1. 并行任務(wù)別用“串行await”,用Promise.all
如果多個(gè)異步任務(wù)之間沒有依賴(比如同時(shí)請求“商品列表”和“用戶信息”),不要用多個(gè)await串行執(zhí)行,會(huì)浪費(fèi)時(shí)間:
// 錯(cuò)誤寫法:串行執(zhí)行,總耗時(shí)=1秒+1秒=2秒
async function badFetch() {
const goods = await requestDataPromise("/api/goods"); // 1秒
const user = await requestDataPromise("/api/user"); // 又1秒
console.log(goods, user);
}
// 正確寫法:并行執(zhí)行,總耗時(shí)=1秒(兩個(gè)請求同時(shí)發(fā))
async function goodFetch() {
// 用Promise.all同時(shí)發(fā)起多個(gè)請求,await等待所有請求完成
const [goods, user] = await Promise.all([
requestDataPromise("/api/goods"),
requestDataPromise("/api/user")
]);
console.log(goods, user);
}2. try/catch的范圍要“合理”
如果希望某個(gè)異步任務(wù)失敗后,其他任務(wù)還能繼續(xù)執(zhí)行,不要把所有await都放進(jìn)一個(gè)try/catch:
async function fetchWithError() {
try {
const user = await requestDataPromise("/api/user"); // 假設(shè)這個(gè)請求失敗
console.log("用戶數(shù)據(jù):", user);
} catch (err) {
console.error("用戶請求失敗:", err); // 只捕獲用戶請求的錯(cuò)誤
}
// 即使上面失敗,這里仍會(huì)執(zhí)行
const goods = await requestDataPromise("/api/goods");
console.log("商品數(shù)據(jù):", goods);
}五、總結(jié):async/await 的核心邏輯
1. 原理公式
async/await = Generator函數(shù)(暫停/恢復(fù)) + Promise自動(dòng)執(zhí)行器(驅(qū)動(dòng)流程) + 事件循環(huán)(調(diào)度微任務(wù))
2. 核心優(yōu)勢對比
特性 | 回調(diào)函數(shù) | Promise鏈?zhǔn)?/p> | async/await |
代碼可讀性 | 差(嵌套) | 中(鏈?zhǔn)剑?/p> | 優(yōu)(同步寫法) |
錯(cuò)誤處理 | 繁瑣(每層判斷) | 中(.catch()) | 優(yōu)(try/catch) |
是否阻塞主線程 | 否 | 否 | 否 |
學(xué)習(xí)成本 | 低 | 中 | 中(需理解原理) |
并行任務(wù)處理 | 復(fù)雜(需手動(dòng)管理) | 優(yōu)(Promise.all) | 優(yōu)(配合Promise.all) |
3. 面試怎么答?
“async/await是Promise的語法糖,本質(zhì)基于Generator函數(shù)和自動(dòng)執(zhí)行器:
async標(biāo)記函數(shù)為異步,調(diào)用時(shí)自動(dòng)創(chuàng)建迭代器并驅(qū)動(dòng)執(zhí)行;await會(huì)暫停函數(shù),等待后面的Promise完成后,把結(jié)果返回并恢復(fù)函數(shù);- 它讓代碼看起來像同步,但實(shí)際是通過事件循環(huán)調(diào)度微任務(wù)實(shí)現(xiàn)異步,不會(huì)阻塞主線程。”
理解async/await,不只是會(huì)用,更要明白它是JavaScript異步編程“從復(fù)雜到簡潔”的必然結(jié)果——它解決了“按順序處理異步任務(wù)”的核心痛點(diǎn),同時(shí)保留了異步的高效性,這也是它能成為前端開發(fā)“標(biāo)配”的原因。
到此這篇關(guān)于js異步編程的演變:回調(diào)函數(shù)、Promise、async/await(代碼原理演示)的文章就介紹到這了,更多相關(guān)js異步:回調(diào)函數(shù)、Promise、async/await內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js中for...in循環(huán)對象時(shí)輸出key值順序混亂問題解決
很久之前就有前輩告訴我用for...in循環(huán)對象屬性的順序不是固定的,xiam?這篇文章主要給大家介紹了關(guān)于js中for...in循環(huán)對象時(shí)輸出key值順序混亂問題解決方法,需要的朋友可以參考下2023-11-11
javascript輸出AscII碼擴(kuò)展集中的字符方法
下面小編就為大家?guī)硪黄猨avascript輸出AscII碼擴(kuò)展集中的字符方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧,祝大家游戲愉快哦2016-12-12
JavaScript設(shè)計(jì)模式之工廠方法模式介紹
這篇文章主要介紹了JavaScript設(shè)計(jì)模式之工廠方法模式介紹,本文講解了簡單工廠模式、多個(gè)工廠方法模式等內(nèi)容,需要的朋友可以參考下2014-12-12
layui實(shí)現(xiàn)左側(cè)菜單點(diǎn)擊右側(cè)內(nèi)容區(qū)顯示
這篇文章主要為大家詳細(xì)介紹了layui實(shí)現(xiàn)左側(cè)菜單點(diǎn)擊右側(cè)內(nèi)容區(qū)顯示,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-07-07
整理關(guān)于Bootstrap導(dǎo)航的慕課筆記
這篇文章主要為大家整理了關(guān)于Bootstrap導(dǎo)航的慕課筆記,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
Bootstrap字體圖標(biāo)無法正常顯示的解決方法
這篇文章主要為大家詳細(xì)介紹了Bootstrap字體圖標(biāo)無法正常顯示的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-10-10

