JavaScript中異步編程的實(shí)現(xiàn)方式詳細(xì)講解
一:回調(diào)函數(shù)
通過將函數(shù)作為參數(shù)傳遞到異步任務(wù)中,在任務(wù)完成后執(zhí)行回調(diào)。
優(yōu)點(diǎn):實(shí)現(xiàn)簡單,兼容性極佳(支持所有JS環(huán)境)
缺點(diǎn):嵌套過深時(shí)會導(dǎo)致“回調(diào)地獄”,代碼可讀性和可維護(hù)性差
示例:
function fetchData(callback) {
setTimeout(() => callback("數(shù)據(jù)"), 1000);
}
fetchData(data => console.log(data));
二:事件監(jiān)聽
異步任務(wù)的執(zhí)行由事件觸發(fā),通過addEventListener綁定回調(diào)。
優(yōu)點(diǎn):支持多事件綁定,實(shí)現(xiàn)模塊化解耦
支持多事件綁定
一個(gè)目標(biāo)對象(如DOM元素)可同時(shí)綁定多個(gè)不同類型的事件(click/mouseover等),> 彼此互不干擾。
代碼組織更靈活,例如:
element.addEventListener(‘click’, handleClick);
element.addEventListener(‘mouseenter’, showTooltip);
模塊化解耦
生產(chǎn)者-消費(fèi)者分離:事件觸發(fā)方(如按鈕)無需知道誰在處理事件,只需發(fā)出事件。
低耦合架構(gòu):不同模塊通過事件通信,避免直接相互調(diào)用。例如:
// 模塊A:發(fā)布事件
document.dispatchEvent(new CustomEvent(‘dataLoaded’, { detail: data }));
// 模塊B:訂閱事件(無需知道模塊A的存在)
document.addEventListener(‘dataLoaded’, (e) => updateUI(e.detail));
缺點(diǎn):流程控制不直觀,需依賴事件驅(qū)動架構(gòu)
document.addEventListener('click', () => console.log("事件觸發(fā)"));
三:發(fā)布訂閱模式
通過事件中心調(diào)度訂閱者和發(fā)布者,實(shí)現(xiàn)完全解耦
優(yōu)點(diǎn):支持多對多通信,擴(kuò)展性強(qiáng)
缺點(diǎn):需手動管理訂閱關(guān)系,調(diào)試復(fù)雜度較高
【本菜鳥還不太熟悉,后續(xù)總結(jié)】
四、Promise【詳解】
Promise是異步編程的一種解決方案,Promise 是一個(gè)構(gòu)造函數(shù),接收一個(gè)函數(shù)作為參數(shù),返回一個(gè) Promise 實(shí)例。一個(gè) Promise 實(shí)例有三種狀態(tài),分別是pending、resolved 和 rejected,分別代表了進(jìn)行中、已成功和已失敗。實(shí)例的狀態(tài)只能由 pending 轉(zhuǎn)變 resolved 或者rejected 狀態(tài),并且狀態(tài)一經(jīng)改變,就凝固了,無法再被改變了。狀態(tài)的改變是通過 resolve() 和 reject() 函數(shù)來實(shí)現(xiàn)的,可以在異步操作結(jié)束后調(diào)用這兩個(gè)函數(shù)改變 Promise 實(shí)例的狀態(tài),
1:先手寫一個(gè)簡單的Promise構(gòu)造函數(shù),從手寫的過程可以大致理解Promise的原理
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
function MyPromise (fn) {
var self = this;// 保存初始化狀態(tài)
this.state = PENDING;// 初始化狀態(tài)
this.value = null;// 用于保存 resolve 或者 rejected 傳入的值
this.resolvedCallbacks = [];// 用于保存 resolve 的回調(diào)函數(shù)
this.rejectedCallbacks = [];// 用于保存 reject 的回調(diào)函數(shù)
// 狀態(tài)轉(zhuǎn)變?yōu)?resolved 方法
function resolve (value) {
// 判斷傳入元素是否為 Promise 值,如果是,則狀態(tài)改變必須等待前一個(gè)狀態(tài)改變后再進(jìn)行改變
if (value instanceof MyPromise) {
return value.then(resolve, reject);
}
// 保證代碼的執(zhí)行順序?yàn)楸据喪录h(huán)的末尾
setTimeout(() => {
// 只有狀態(tài)為 pending 時(shí)才能轉(zhuǎn)變,
if (self.state === PENDING) {
self.state = RESOLVED; // 修改狀態(tài)
self.value = value; // 設(shè)置傳入的值
// 執(zhí)行回調(diào)函數(shù)
self.resolvedCallbacks.forEach(callback => {
callback(value);
});
}
}, 0);
}
function reject (value) {
setTimeout(() => {
if (self.state === PENDING) {
self.state = REJECTED;
self.value = value;
self.rejectedCallbacks.forEach(callback => {
callback(value);
});
}
}, 0);
}
// 將兩個(gè)方法傳入函數(shù)執(zhí)行
try {
fn(resolve, reject);
} catch (e) {
// 遇到錯(cuò)誤時(shí),捕獲錯(cuò)誤,執(zhí)行 reject 函數(shù)
reject(e);
}
}
MyPromise.prototype.then = function (onResolved, onRejected) {
// 首先判斷兩個(gè)參數(shù)是否為函數(shù)類型,因?yàn)檫@兩個(gè)參數(shù)是可選參數(shù)
onResolved =
typeof onResolved === "function"
? onResolved
: function (value) {
return value;
};
onRejected =
typeof onRejected === "function"
? onRejected
: function (error) {
throw error;
};
// 如果是等待狀態(tài),則將函數(shù)加入對應(yīng)列表中
if (this.state === PENDING) {
this.resolvedCallbacks.push(onResolved);
this.rejectedCallbacks.push(onRejected);
}
// 如果狀態(tài)已經(jīng)凝固,則直接執(zhí)行對應(yīng)狀態(tài)的函數(shù)
if (this.state === RESOLVED) {
onResolved(this.value);
}
if (this.state === REJECTED) {
onRejected(this.value);
}
};
看上去有點(diǎn)復(fù)雜,時(shí)間有限,所以我決定放棄這個(gè)promise的手寫和原理,將各種用法弄清楚。
2:promise使用示例:
構(gòu)造函數(shù)Promise接收一個(gè)函數(shù)作為參數(shù),這個(gè)函數(shù)帶有兩個(gè)參數(shù),一個(gè)是resolve,一個(gè)是reject,這個(gè)函數(shù)內(nèi)部的代碼本身是同步的立即執(zhí)行函數(shù), 只有執(zhí)行resolve或者reject的時(shí)候是異步操作, 實(shí)際會先執(zhí)行對應(yīng)的then/catch等,將then/catch里的代碼放進(jìn)微任務(wù)隊(duì)列中,當(dāng)主棧完成后,才會去調(diào)用resolve/reject中存放的方法執(zhí)行。
附:宏任務(wù)微任務(wù)相關(guān)js事件循環(huán)機(jī)制見另一篇博:js的事件循環(huán)機(jī)制
// Promise構(gòu)造函數(shù)接受一個(gè)函數(shù)(執(zhí)行器函數(shù))作為參數(shù),
// 該函數(shù)的兩個(gè)參數(shù)分別是resolve和reject。它們是兩個(gè)函數(shù),
// 由 JavaScript 引擎提供,不用自己部署。
const promise = new Promise(function (resolve, reject) {
// ... some code
if (/* 異步操作成功 */) {
// 在異步操作成功時(shí)調(diào)用,并將異步操作的結(jié)果,作為參數(shù)value傳遞出去;
resolve(value);
} else {
// 在異步操作失敗時(shí)調(diào)用,并將異步操作報(bào)出的錯(cuò)誤,作為參數(shù)error/reason傳遞出去。
reject(error);
}
});
Promise實(shí)例的then方法接收兩個(gè)回調(diào)函數(shù)作為參數(shù),第一個(gè)回調(diào)當(dāng)resolve()時(shí)執(zhí)行,是成功回調(diào),第二個(gè)回調(diào)reject()時(shí)執(zhí)行,是失敗回調(diào)。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
但同時(shí)Promise實(shí)例的catch方法也可以捕獲異常,這時(shí)候我有點(diǎn)疑惑,既然then方法已經(jīng)可以指出失敗回調(diào),那catch方法的意義是什么?先查了一下區(qū)別
then的失敗回調(diào)
僅能捕獲當(dāng)前Promise鏈中前一個(gè)Promise的reject狀態(tài)或拋出的錯(cuò)誤
catch方法
能捕獲整個(gè)Promise鏈中任意位置的未處理錯(cuò)誤(包括then中拋出的異常)
這個(gè)解釋明確的說明catch在promise的鏈?zhǔn)秸{(diào)用中更好用,可以將錯(cuò)誤處理集中在鏈?zhǔn)秸{(diào)用的末尾。這時(shí)候我又產(chǎn)生了兩個(gè)疑惑,第一個(gè)是鏈?zhǔn)秸{(diào)用是什么,第二個(gè)是如果集中處理了,那catch如何直到捕獲的錯(cuò)誤是哪一步的錯(cuò)誤
然后針對第一個(gè)問題,我開始學(xué)習(xí)promise的鏈?zhǔn)秸{(diào)用
3:promise的鏈?zhǔn)秸{(diào)用
let p1 = new Promise((resolve, reject) => {
setTimeout(() => resolve("成功!"), 1000);
});
p1.then(result => {
console.log(result); // 輸出:成功!
return "下一步"; // 返回一個(gè)值,可以被下一個(gè).then()捕獲
}).then(nextStep => {
console.log(nextStep); // 輸出:下一步
return new Promise((resolve, reject) => {
setTimeout(() => resolve("最終結(jié)果"), 500);
});
}).then(finalResult => {
console.log(finalResult); // 輸出:最終結(jié)果
});
首先,我已知promise的then方法返回的是一個(gè)新的promise,從這段代碼可以看出,p1的then方法的成功回調(diào)中返回一個(gè)字符串—‘下一步’,直接被下一個(gè)then方法捕獲了。換種說法就是,首先,第一個(gè)then函數(shù)本身返回了一個(gè)新的promise-------------
當(dāng)你then里面的回調(diào)函數(shù)返回一個(gè)非promise的值的時(shí)候,這個(gè)新的promise會立即resolve該值,也就是直接執(zhí)行第二個(gè)then方法的第一個(gè)回調(diào)。
當(dāng)你then里面的回調(diào)函數(shù)返回一個(gè)promise時(shí)候,then返回新的promise會跟隨你在回調(diào)函數(shù)中返回的promise的狀態(tài)[你也可以看成同一個(gè)promise,沒有差別,反正都是執(zhí)行相同的回調(diào)]。
略繞,總之這樣鏈?zhǔn)秸{(diào)用最大好處就是整個(gè)鏈條能保持清晰的執(zhí)行順序和統(tǒng)一的錯(cuò)誤處理
4:鏈?zhǔn)秸{(diào)用的catch回調(diào)
【1】.catch()會捕獲第一個(gè)錯(cuò)誤(無論是reject的錯(cuò)誤還是throw 的異常),后續(xù)錯(cuò)誤會被靜默忽略。
【2】如果同時(shí)存在then的錯(cuò)誤回調(diào)和獨(dú)立的catch方法,遵循就近原則和互斥執(zhí)行【只執(zhí)行最近那個(gè)】
let promise = new Promise((resolve, reject) => {
setTimeout(() => reject("失敗"), 1000);
});
promise.then(result => {
return "下一步";
},(error)=>{
return 'error' // 只執(zhí)行這個(gè)
}).then(nextStep => {
console.log(nextStep);
return new Promise((resolve, reject) => {
setTimeout(() => reject("第二步報(bào)錯(cuò)"), 500);
});
}).then(finalResult => {
console.log(finalResult);
}).catch(error=>{
console.log(error)
});
5:promise的其他方法
all()
使用場景:并行請求多個(gè)接口后統(tǒng)一處理數(shù)據(jù),批量文件上傳全部完成后觸發(fā)通知
// 模擬三個(gè)異步API請求
const fetchUser = () =>
new Promise(resolve => setTimeout(() => resolve({id: 1, name: 'Alice'}), 800));
const fetchOrders = () =>
new Promise(resolve => setTimeout(() => resolve([101, 102, 103]), 500));
const fetchProducts = () =>
new Promise(resolve => setTimeout(() => resolve(['Laptop', 'Phone']), 300));
// 使用Promise.all并行執(zhí)行
Promise.all([fetchUser(), fetchOrders(), fetchProducts()])
.then(([user, orders, products]) => {
console.log('整合數(shù)據(jù):', { user, orders, products });
})
.catch(err => console.error('請求失敗:', err));
race()
采用競速模式,返回第一個(gè)敲定狀態(tài)(無論成功/失?。┑腜romise結(jié)果
使用場景:請求超時(shí)控制:將目標(biāo)請求與setTimeout的reject Promise競速?!粳F(xiàn)在axios等工具直接傳入timeout參數(shù)即可】
// 模擬API請求(2秒返回)
const apiRequest = new Promise(resolve =>
setTimeout(() => resolve("數(shù)據(jù)獲取成功"), 2000)
);
// 設(shè)置1秒超時(shí)
const timeout = new Promise((_, reject) =>
setTimeout(() => reject("請求超時(shí)"), 1000)
);
// 競速執(zhí)行
Promise.race([apiRequest, timeout])
.then(res => console.log(res))
.catch(err => console.error(err));
finally()
使用場景:隱藏加載動畫無論請求成功與否
fetchData()
.then(res => console.log(res))
.catch(err => console.error(err.message))
.finally(() => {
console.log("無論成功失敗,都會執(zhí)行清理工作");
document.getElementById('loading').style.display = 'none';
});
6:附加一個(gè)執(zhí)行順序判斷的示例,測試下對promise的了解程度
console.log('script start')
let promise1 = new Promise(function (resolve) {
console.log('promise1')
resolve()
console.log('promise1 end')
}).then(function () {
console.log('promise2')
})
setTimeout(function(){
console.log('settimeout')
})
console.log('script end')
// 輸出順序: script start->promise1->promise1 end->script end->promise2->settimeout
五、Async/Await
async 將一個(gè)函數(shù)標(biāo)記為異步函數(shù),await 需要在異步函數(shù)中使用,標(biāo)記當(dāng)前操作是異步操作。async + await 必須配合 promise 使用,同時(shí) async 和 await 必須一起使用。即 await 必須在 async 標(biāo)記的函數(shù)中使用。
以下是一個(gè)使用示例:
function getProfile() {
return new Promise((resolve, reject) => {
// 使用定時(shí)器模擬接口請求
setTimeout(() => {
resolve({
code: 200,
msg: "用戶信息",
data: {
id: 1,
name: "liang"
}
})
}, 3000);
});
}
// 以下代碼會執(zhí)行 先輸出 123 再執(zhí)行輸出 res
function loadData() {
getProfile().then(res => {
console.log(res);
})
console.log(123);
}
// 下面寫法會使 getProfile() 先執(zhí)行
// 等待三秒后執(zhí)行完再把得到的結(jié)果賦值給左邊的res,然后再繼續(xù)往下執(zhí)行
async function loadData() {
const res = await getProfile()
console.log(res);
console.log(123);
}
我的理解:async創(chuàng)建了一個(gè)異步環(huán)境,在這個(gè)環(huán)境中其實(shí)是不存在同步代碼的。
首先在使用時(shí)每個(gè)await后面理論上都需要緊跟一個(gè)promise,例如:
async function asyncChain () {
try {
const stage1 = await new Promise(resolve => setTimeout(() => resolve(1), 1000));
console.log("第一階段結(jié)果:", stage1);
} catch (error) {
console.error("Async鏈錯(cuò)誤:", error);
}
}
如果跟的是一個(gè)非 Promise 值時(shí)(如字符串、數(shù)字等),引擎會通過 Promise.resolve() 將其隱式轉(zhuǎn)換為已解決的 Promise 對象,此時(shí)代碼會立即繼續(xù)執(zhí)行,也就是跟了一個(gè)立即resolve那個(gè)字符串或者數(shù)字的promise。
例如:
async function demo() {
const result = await 42; // 等同于 await Promise.resolve(42)
console.log(result); // 直接輸出42
}
而await后面的代碼,比如上方代碼塊中的console.log(result),實(shí)際上相當(dāng)于最近的前一個(gè)await后面緊跟的Promise的then回調(diào)里的內(nèi)容。
我:也就是await會阻塞async函數(shù)中在await之后的代碼,那是不是相當(dāng)于await之后的代碼其實(shí)是當(dāng)前await后面的這個(gè)promise實(shí)例的then回調(diào)?
deepseek:是的,await 之后的代碼可以視為當(dāng)前 await 后面的 Promise 實(shí)例的 then 回調(diào),但兩者在實(shí)現(xiàn)機(jī)制和細(xì)節(jié)上存在差異。
async function demo() {
const result = await somePromise; // 阻塞后續(xù)代碼
console.log(result); // 相當(dāng)于 somePromise.then(res => console.log(res))
}
六、Generator/yield
這個(gè)我寫了五年前端代碼沒有用到過。當(dāng)然我是菜鳥哈哈。面試遇到就放棄好了。
到此這篇關(guān)于JavaScript中異步編程的實(shí)現(xiàn)方式的文章就介紹到這了,更多相關(guān)js異步編程實(shí)現(xiàn)方式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
JavaScript實(shí)現(xiàn)將xml轉(zhuǎn)換成html table表格的方法
這篇文章主要介紹了JavaScript實(shí)現(xiàn)將xml轉(zhuǎn)換成html table表格的方法,實(shí)例分析了javascript操作XML文件與table表格的技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04
webpack中splitChunks分包策略的實(shí)現(xiàn)
splitChunks是 webpack 中用于分包的配置選項(xiàng)之一,本文主要介紹了webpack中splitChunks分包策略的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2024-06-06
談?wù)凧avaScript數(shù)組常用方法總結(jié)
本篇文章主要介紹了談?wù)凧avaScript數(shù)組常用方法總結(jié),在JavaScript中,我們需要時(shí)常對數(shù)組進(jìn)行操作。一起跟隨小編過來看看吧2017-01-01

