?在前端開發(fā)中,異步請求和Promise是繞不開的核心知識點。無論是獲取數(shù)據(jù)、提交表單,還是處理復(fù)雜的業(yè)務(wù)邏輯,我們都需要與異步操作打交道。本文將通過手寫 Ajax 請求和解析 Promise 的底層原理,結(jié)合生活中的實際案例,帶你深入理解這些技術(shù)的本質(zhì)。
一、Ajax 的本質(zhì):異步通信的基石
1.1 什么是 Ajax?
Ajax(Asynchronous JavaScript and XML)是一種通過 JavaScript 與服務(wù)器進行異步通信的技術(shù)。它允許頁面在不刷新的情況下動態(tài)更新數(shù)據(jù),極大提升了用戶體驗。
生活類比:
想象你在點外賣。你下單后,不需要一直盯著手機等外賣送到,而是繼續(xù)做其他事情。外賣送達時,系統(tǒng)會通知你。這個“異步等待”的過程,就是 Ajax 的核心思想。
1.2 手寫 Ajax 請求
步驟一:創(chuàng)建 XMLHttpRequest 對象
function createAjaxRequest() {
if (window.XMLHttpRequest) {
return new XMLHttpRequest(); // 現(xiàn)代瀏覽器
} else {
return new ActiveXObject("Microsoft.XMLHTTP"); // 兼容 IE6-IE11
}
}
步驟二:發(fā)送 GET 請求
function getWeather(city) {
const xhr = createAjaxRequest();
xhr.open("GET", `https://api.example.com/weather?city=${city}`, true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log("成功獲取天氣數(shù)據(jù):", JSON.parse(xhr.responseText));
} else if (xhr.readyState === 4) {
console.error("請求失敗,狀態(tài)碼:", xhr.status);
}
};
xhr.send();
}
// 調(diào)用示例
getWeather("北京");
代碼解析:
open()
:初始化請求,指定方法(GET/POST)、URL 和是否異步。onreadystatechange
:監(jiān)聽請求狀態(tài)變化。send()
:發(fā)送請求。
1.3 手寫 Ajax 的痛點
上述代碼雖然能工作,但存在以下問題:
- 重復(fù)代碼:每次請求都要手動處理狀態(tài)判斷和錯誤處理。
- 回調(diào)地獄:多個異步操作嵌套會導(dǎo)致代碼難以維護。
- 缺乏統(tǒng)一接口:不同瀏覽器的兼容性處理復(fù)雜。
生活類比:
這就像每次點外賣都要自己跑廚房、打包、配送。效率低且容易出錯。
二、Promise 的底層原理:優(yōu)雅處理異步的“樂高積木”
2.1 Promise 的核心思想
Promise 是一種異步編程的容器,它將異步操作的結(jié)果(成功或失?。┓庋b成一個對象,通過鏈式調(diào)用和統(tǒng)一的接口管理異步流程。
生活類比:
Promise 像是一張“承諾書”。你告訴服務(wù)器:“我需要數(shù)據(jù)”,服務(wù)器承諾在某個時間點給你結(jié)果。無論成功還是失敗,你都可以通過.then()
或.catch()
處理。
2.2 手寫 Promise 的核心邏輯
步驟一:定義 Promise 的狀態(tài)
class MyPromise {
constructor(executor) {
this.status = "pending"; // 初始狀態(tài)
this.value = undefined; // 成功值
this.reason = undefined; // 失敗原因
this.onFulfilledCallbacks = []; // 成功回調(diào)隊列
this.onRejectedCallbacks = []; // 失敗回調(diào)隊列
const resolve = (value) => {
if (this.status === "pending") {
this.status = "fulfilled";
this.value = value;
// 觸發(fā)所有成功回調(diào)
this.onFulfilledCallbacks.forEach((fn) => fn());
}
};
const reject = (reason) => {
if (this.status === "pending") {
this.status = "rejected";
this.reason = reason;
// 觸發(fā)所有失敗回調(diào)
this.onRejectedCallbacks.forEach((fn) => fn());
}
};
try {
executor(resolve, reject); // 執(zhí)行用戶傳入的函數(shù)
} catch (error) {
reject(error); // 捕獲同步錯誤
}
}
then(onFulfilled, onRejected) {
if (this.status === "fulfilled") {
onFulfilled(this.value);
} else if (this.status === "rejected") {
onRejected(this.reason);
} else {
// 異步情況下,先將回調(diào)存入隊列
this.onFulfilledCallbacks.push(() => onFulfilled(this.value));
this.onRejectedCallbacks.push(() => onRejected(this.reason));
}
}
}
代碼解析:
- 狀態(tài)管理:Promise 有三種狀態(tài)(
pending
、fulfilled
、rejected
),狀態(tài)一旦改變不可逆。 - 回調(diào)隊列:當(dāng) Promise 處于
pending
狀態(tài)時,先將回調(diào)函數(shù)暫存,等到狀態(tài)改變后再執(zhí)行。 - 錯誤處理:通過
try-catch
捕獲同步錯誤,避免程序崩潰。
2.3 手寫 Promise 的優(yōu)化:鏈式調(diào)用
class MyPromise {
// ... 上述代碼省略 ...
then(onFulfilled, onRejected) {
const promise2 = new MyPromise((resolve, reject) => {
if (this.status === "fulfilled") {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x); // 返回新 Promise
} catch (e) {
reject(e);
}
}, 0);
} else if (this.status === "rejected") {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (e) {
reject(e);
}
}, 0);
} else {
// 異步情況下,先將回調(diào)存入隊列
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const x = onFulfilled(this.value);
resolve(x);
} catch (e) {
reject(e);
}
}, 0);
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const x = onRejected(this.reason);
resolve(x);
} catch (e) {
reject(e);
}
}, 0);
});
}
});
return promise2;
}
}
代碼解析:
- 鏈式調(diào)用:每個
.then()
返回一個新的 Promise 實例,實現(xiàn)鏈式調(diào)用。 - 微任務(wù)隊列:使用
setTimeout(fn, 0)
將回調(diào)放入微任務(wù)隊列,確保異步執(zhí)行順序正確。
三、結(jié)合 Ajax 的 Promise 封裝
3.1 用 Promise 封裝 Ajax 請求
function getWeather(city) {
return new MyPromise((resolve, reject) => {
const xhr = createAjaxRequest();
xhr.open("GET", `https://api.example.com/weather?city=${city}`, true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(`請求失敗,狀態(tài)碼: ${xhr.status}`);
}
}
};
xhr.send();
});
}
// 使用示例
getWeather("北京")
.then((data) => {
console.log("成功獲取天氣數(shù)據(jù):", data);
return getWeather("上海"); // 鏈式調(diào)用
})
.then((data) => {
console.log("成功獲取上海天氣數(shù)據(jù):", data);
})
.catch((error) => {
console.error("請求失敗:", error);
});
代碼解析:
- 封裝邏輯:將 Ajax 請求封裝為 Promise,統(tǒng)一處理成功和失敗。
- 鏈式調(diào)用:通過
.then()
實現(xiàn)多個異步操作的串聯(lián)。 - 錯誤捕獲:通過
.catch()
統(tǒng)一處理所有錯誤,避免嵌套回調(diào)。
3.2 生活類比:從“點外賣”到“智能配送”
- 未使用 Promise:每次點外賣都要手動查看配送狀態(tài),代碼嵌套復(fù)雜。
- 使用 Promise:系統(tǒng)自動通知你配送結(jié)果,你可以專注于其他事情,無需頻繁檢查。
四、Promise 的底層原理詳解
4.1 Promise 的狀態(tài)管理
狀態(tài) | 描述 |
---|---|
pending | 初始狀態(tài),既不是成功也不是失敗 |
fulfilled | 操作成功完成 |
rejected | 操作失敗 |
關(guān)鍵點:狀態(tài)一旦改變,不可逆;Promise 只能有一個最終結(jié)果。
4.2 微任務(wù)隊列與事件循環(huán)
Promise 的 .then()
和 .catch()
方法會在微任務(wù)隊列中執(zhí)行,優(yōu)先級高于宏任務(wù)(如 setTimeout
)。這是 Promise 實現(xiàn)異步流程控制的關(guān)鍵。
console.log("Start"); // 同步代碼
new MyPromise((resolve) => {
console.log("Promise executor"); // 同步代碼
resolve();
}).then(() => {
console.log("Promise then"); // 微任務(wù)
});
setTimeout(() => {
console.log("Timeout"); // 宏任務(wù)
}, 0);
console.log("End"); // 同步代碼
// 輸出順序:
// Start
// Promise executor
// End
// Promise then
// Timeout
生活類比:
- 同步代碼:像排隊點餐,必須等前面的人處理完才能輪到你。
- 微任務(wù):像餐廳的快速通道,優(yōu)先處理。
- 宏任務(wù):像普通排隊,需等待其他任務(wù)完成。
4.3 鏈式調(diào)用的實現(xiàn)原理
每個 .then()
返回一個新的 Promise 實例,形成鏈式結(jié)構(gòu)。通過遞歸調(diào)用 then
,可以實現(xiàn)異步操作的串聯(lián)。
new MyPromise((resolve) => {
resolve(1);
})
.then((value) => {
console.log(value); // 1
return value + 1;
})
.then((value) => {
console.log(value); // 2
return value + 1;
})
.then((value) => {
console.log(value); // 3
});
五、總結(jié):從“手寫”到“理解”
5.1 核心收獲
- Ajax 的本質(zhì):通過
XMLHttpRequest
實現(xiàn)異步通信,但存在代碼冗余和回調(diào)地獄的問題。 - Promise 的優(yōu)勢:通過狀態(tài)管理和鏈式調(diào)用,簡化異步代碼,提升可維護性。
- 底層原理:Promise 通過微任務(wù)隊列和回調(diào)隊列管理異步流程,狀態(tài)不可逆且只能有一個結(jié)果。
5.2 實際應(yīng)用建議
- 封裝 Ajax:將重復(fù)邏輯抽離為 Promise,提高代碼復(fù)用性。
- 避免回調(diào)地獄:使用
.then()
和.catch()
替代嵌套回調(diào)。 - 理解事件循環(huán):掌握微任務(wù)和宏任務(wù)的執(zhí)行順序,避免異步邏輯錯誤。
六、結(jié)語
手寫 Ajax 和 Promise 不僅是面試高頻考點,更是深入理解前端異步編程的關(guān)鍵。通過本文的實踐和解析,希望你能真正掌握這些技術(shù)的核心思想,并在實際項目中靈活運用。如果覺得內(nèi)容對你有幫助,歡迎點贊、收藏,或分享給更多開發(fā)者!