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