“阻塞还是非阻塞?”——这是每个前端开发者必须面对的灵魂拷问。
在现代 Web 应用中,数据驱动已成常态,而异步网络请求正是连接前端与后端的核心桥梁。从古老的 XMLHttpRequest,到如今主流的 Fetch API 和 Promise,再到优雅的 async/await,前端异步通信经历了深刻的变革。
本文将带你: ✅ 深入理解异步的本质
✅ 系统对比 XHR、Fetch、Promise 的设计哲学与实现差异
✅ 掌握最佳实践与常见陷阱
✅ 构建健壮、可维护的请求层架构
无论你是刚入门的新手,还是想梳理知识体系的资深开发者,这篇文章都值得你完整阅读。
🧠 一、异步的本质:为什么我们需要它?
同步 vs 异步:一场效率革命
想象一下你在银行排队办理业务:
- 同步模式:前面一个人没办完,你就只能干等着——整个流程被“阻塞”。
- 异步模式:你点完餐就可以去工作,服务员叫号时再回来取餐——不耽误其他事。
在浏览器中,JavaScript 是单线程运行的。如果一个操作(如网络请求)长时间占用主线程,页面就会“卡死”,用户无法点击、滚动或输入。
📌 这就是我们为何必须使用异步编程——为了保持应用的响应性与用户体验。
阻塞请求有多可怕?
// ❌ 危险!假设存在同步请求函数
const data = syncFetch('https://api.example.com/data');
console.log(data); // 必须等待...
console.log('这行代码永远不会立刻执行!');
在这期间,整个 UI 被冻结,用户体验极差。
而异步方案则完全不同:
// ✅ 安全!异步请求不会阻塞主线程
asyncFetch('https://api.example.com/data', (data) => {
console.log('数据已到达:', data);
});
console.log('这条日志会立即打印!'); // 用户可继续交互
✅ 结论:异步 = 不阻塞主线程 + 提升响应速度 + 更好的用户体验。
⚙️ 二、元老登场:XMLHttpRequest(XHR)
作为前端异步请求的“开山鼻祖”,XMLHttpRequest 自 1999 年诞生以来,支撑了整整一代 Web 应用的发展。
尽管现在已被更现代的技术取代,但了解它的原理,有助于我们理解底层机制。
1. XHR 的基本使用流程
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://api.github.com/orgs/lemoncode/members', true);
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) {
const data = JSON.parse(xhr.responseText);
document.getElementById('members').innerHTML = data
.map(item => `<li>${item.login}</li>`)
.join('');
} else if (xhr.readyState === 4) {
console.error('请求失败:', xhr.status);
}
};
xhr.send();
核心步骤解析:
| 步骤 | 方法 | 说明 |
|---|---|---|
| 创建实例 | new XMLHttpRequest() | 初始化请求对象 |
| 配置请求 | xhr.open(method, url, async) | 设置方法、URL 和是否异步 |
| 设置头 | setRequestHeader() | 如 POST 需设置 Content-Type |
| 监听状态变化 | onreadystatechange | 响应状态改变时触发 |
| 发送请求 | send(body) | 可携带请求体 |
2. 关键属性详解
| 属性 | 含义 |
|---|---|
readyState | 请求生命周期状态(0~4) |
status | HTTP 状态码(如 200、404) |
responseText | 返回的文本内容 |
🔹 readyState 的五种状态:
0: UNSENT(未初始化)1: OPENED(已调用 open)2: HEADERS_RECEIVED(收到响应头)3: LOADING(正在接收响应体)4: DONE(请求完成)
⚠️ 注意:只有当 readyState === 4 且 status 在 2xx 范围内才算成功。
3. XHR 的优缺点总结
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 兼容性极好(支持 IE7+) | API 设计陈旧,基于事件监听 |
| 支持上传进度、超时控制 | 容易陷入“回调地狱” |
| 可手动 abort() 中断请求 | 错误处理分散,需重复判断 status |
| 功能全面,细节可控 | 默认不支持 Promise,难以配合 async/await |
💡 适用场景:维护老项目、需要精细控制请求过程(如监控上传进度)。
🚀 三、现代标准:Fetch API —— 更简洁、更强大
随着 ES6 的普及,Fetch API 成为新一代网络请求的标准,其核心理念是:让异步请求像写同步代码一样自然。
1. 基本语法:链式调用 + Promise
fetch('https://api.github.com/orgs/lemoncode/members')
.then(response => {
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json(); // 解析 JSON 数据
})
.then(data => {
const list = document.getElementById('members');
list.innerHTML = data.map(user => `<li>${user.login}</li>`).join('');
})
.catch(error => {
console.error('请求出错:', error);
});
参数说明:
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: 'Alice' }),
credentials: 'include' // 若需携带 cookie
})
2. Fetch 的关键特性
| 特性 | 说明 |
|---|---|
| 返回 Promise | 天然支持 .then().catch() |
| 分离网络错误与 HTTP 错误 | 网络断开 → reject;404/500 → resolve 但 !ok |
| 流式响应处理 | response.body 是 ReadableStream,适合大文件 |
| 支持 AbortController | 可主动取消请求 |
| 更语义化的 Response 方法 | .json(), .text(), .blob() 等 |
⚠️ 重要提醒:
fetch只在网络错误(如断网)时抛出异常,对于 404、500 等状态码并不会自动 reject!必须手动检查response.ok!
3. 结合 async/await 写法更优雅
async function fetchMembersAsync() {
try {
const response = await fetch('https://api.github.com/orgs/lemoncode/members');
if (!response.ok) throw new Error(`状态码: ${response.status}`);
const data = await response.json();
const list = document.getElementById('members');
list.innerHTML = data.map(user => `<li>${user.login}</li>`).join('');
} catch (error) {
console.error('加载失败:', error.message);
}
}
✨ 使用 async/await 后,异步代码几乎和同步代码一样直观!
4. Fetch 的优缺点对比
| ✅ 优点 | ❌ 缺点 |
|---|---|
| 基于 Promise,易于链式调用 | IE 不支持,需 Polyfill |
| API 简洁,配置集中 | 默认不带 Cookie(需设 credentials: 'include') |
| 与 async/await 完美融合 | HTTP 错误不会自动 reject |
| 支持流式处理大型响应 | 取消请求需搭配 AbortController |
| 更符合现代 JS 编程范式 | 对 FormData 等处理稍复杂 |
🎯 推荐指数:★★★★★
👉 新项目首选方案!
💡 四、异步基石:Promise 的深入理解
如果说 Fetch 是“工具”,那么 Promise 就是“思想” 。它是现代异步编程的根基。
1. Promise 是什么?
Promise 表示一个尚未完成但将来会结束的异步操作的结果容器。
它有三种状态:
pending→ 初始状态fulfilled→ 成功完成rejected→ 执行失败
一旦状态变更,就不会再变。
2. 创建一个 Promise
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
Math.random() > 0.5
? resolve('成功获取数据')
: reject(new Error('网络超时'));
}, 1000);
});
myPromise
.then(result => console.log('✅', result))
.catch(err => console.error('❌', err.message))
.finally(() => console.log('请求结束'));
3. Promise 解决了哪些痛点?
✅ 痛点一:回调地狱(Callback Hell)
// ❌ 回调嵌套,层层缩进,难以维护
getUser(userId, (user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
// ... 还可能更多嵌套
});
});
});
// ✅ 使用 Promise 扁平化处理
getUser(userId)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => { /* ... */ })
.catch(handleError);
✅ 痛点二:统一错误处理
以前每个回调都要单独处理错误,现在只需一个 .catch() 捕获所有异常。
4. 常用静态方法一览
| 方法 | 描述 | 使用场景 |
|---|---|---|
Promise.resolve(value) | 返回已成功的 Promise | 包装同步值为异步 |
Promise.reject(reason) | 返回已失败的 Promise | 主动抛出错误 |
Promise.all(promises) | 全部成功才成功,任一失败即失败 | 并行请求全部成功才继续 |
Promise.race(promises) | 第一个完成的 Promise 决定结果 | 实现超时控制 |
Promise.allSettled(promises) | 等待所有完成(不论成败) | 批量任务统计结果 |
Promise.any(promises) | 第一个成功的 Promise | 多源请求择优返回 |
示例:请求超时控制(race)
const fetchWithTimeout = (url, timeout = 5000) => {
const controller = new AbortController();
const fetchPromise = fetch(url, { signal: controller.signal });
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => controller.abort(), timeout)
);
return Promise.race([fetchPromise, timeoutPromise]);
};
🆚 五、终极对比:XHR vs Fetch vs Promise
| 特性 | XMLHttpRequest | Fetch API | Promise |
|---|---|---|---|
| 类型 | 浏览器原生对象 | 浏览器新 API | ES6 规范对象 |
| 异步模型 | 事件驱动 (onreadystatechange) | 基于 Promise | 基于状态机 |
| 返回值 | undefined(通过事件获取结果) | Promise<Response> | Promise<T> |
| 错误处理 | onerror + 手动判断 status | catch(仅网络错误)+ 手动判断 response.ok | catch 统一捕获 reject |
| 是否支持 async/await | ❌ 不直接支持 | ✅ 天然支持 | ✅ 天然支持 |
| 是否易产生回调地狱 | ✅ 容易 | ❌ 可避免 | ❌ 核心就是解决这个问题 |
| 浏览器兼容性 | ✅ 极佳(IE7+) | ❌ 不支持 IE | ✅ 支持现代浏览器 |
| 推荐度 | ⭐⭐☆(仅用于维护) | ⭐⭐⭐⭐⭐(推荐新项目) | ⭐⭐⭐⭐⭐(必备基础) |
📌 一句话总结:
- XHR 是历史的见证者,适合维护旧系统;
- Promise 是异步编程的思想基石;
- Fetch 是当前最优选择,代表未来方向。
🛠️ 六、实战场景与最佳实践
1. 简单数据获取 → 直接用 Fetch + async/await
async function getUser(id) {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error('用户不存在');
return res.json();
}
2. 并行请求 → 使用 Promise.all
async function loadUserProfile(userId) {
try {
const [user, posts, profile] = await Promise.all([
fetch(`/users/${userId}`).then(r => r.json()),
fetch(`/posts?uid=${userId}`).then(r => r.json()),
fetch(`/profile/${userId}`).then(r => r.json())
]);
return { user, posts, profile };
} catch (err) {
console.error('加载失败:', err);
}
}
3. 请求超时控制 → Promise.race + AbortController
function fetchWithTimeout(url, timeout = 8000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal })
.finally(() => clearTimeout(timeoutId));
}
4. 批量上传文件 → Promise.allSettled
const uploadTasks = files.map(file => uploadFile(file));
const results = await Promise.allSettled(uploadTasks);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`✅ 文件 ${index} 上传成功`);
} else {
console.warn(`❌ 文件 ${index} 失败:`, result.reason);
}
});
5. 封装通用请求库(推荐做法)
// utils/request.js
const BASE_URL = 'https://api.example.com';
export default async function request(url, options = {}) {
const config = {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
'Authorization': localStorage.getItem('token') // 自动注入 token
}
};
try {
const response = await fetch(BASE_URL + url, config);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.message || `HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') throw error;
console.error('[Request Error]', error.message);
throw error;
}
}
// 使用
const users = await request('/users', { method: 'GET' });
📝 七、总结:技术演进的背后是开发体验的升级
| 技术 | 定位 | 是否推荐 |
|---|---|---|
XMLHttpRequest | 异步请求奠基者 | ❌ 仅限维护 |
Promise | 异步编程范式革新 | ✅ 必须掌握 |
Fetch API | 现代浏览器标准 | ✅ 新项目首选 |
🔁 演进路线图:
XHR (事件驱动)
→ Promise (链式调用)
→ Fetch (基于 Promise)
→ async/await (同步风格书写异步)
这场演进不仅仅是 API 的更新换代,更是 编程思维的进化——从“如何发起请求”转向“如何优雅地组织异步逻辑”。
🎯 最后建议
- 新项目一律使用 Fetch + async/await
- 封装统一请求函数,提升复用性与一致性
- 善用 AbortController 控制请求生命周期
- 对 HTTP 错误做统一拦截处理
- 必要时引入 Axios 等第三方库(尤其需要 IE 支持时)
📚 延伸学习推荐:
- MDN 文档:Using Fetch
- 《You Don’t Know JS》系列 —— 深入理解 Promise
- Axios vs Fetch 深度对比(适合企业级项目选型)
🎉 结语
从前端的“刀耕火种”时代,到如今声明式、函数式的异步编程,每一次技术迭代都在让我们离“写出更好代码”的理想更近一步。
希望这篇文章能帮你打通异步请求的知识脉络,构建更加稳健高效的前端架构!
💬 欢迎留言讨论:你还在用 XHR 吗?你的项目是如何封装请求的?
✅ 如果你觉得这篇文章有帮助,请点赞、收藏、分享给更多开发者朋友!