Skip to main content

请求处理

前端请求的发展经历了 XHR -> Fetch -> Axios 等封装库。现代开发中最常被问到的是它们三者的区别、如何取消请求,以及 Fetch 的缺陷与局限性。

方案原理优点缺点/局限
AJAX (XHR)浏览器原生 XMLHttpRequest 对象兼容性极好,支持进度监听 (onprogress),支持取消 (abort)。基于事件的异步模型,写法繁琐,容易陷入回调地狱;且 API 设计老旧,不符合关注分离原则。
Fetch API浏览器原生 ES6 API,基于 Promise语法简洁,符合现代异步编程思想(结合 async/await);脱离了 XHR,是底层的网络请求接口。1. 默认不携带 cookie(需设置 credentials: 'include')。
2. 无法原生监听上传进度。
3. 对 400/500 等 HTTP 错误状态码不会 reject,只有网络断开才会 reject,需手动检查 res.ok
4. 不支持原生设置超时时间(需借助 AbortController)。
Axios第三方库 (基于 Promise)浏览器端基于 XHR,Node 端基于 http 模块。功能极强:支持拦截器、自动转换 JSON、防御 XSRF、并发请求等。需要引入第三方包,增加了一点点体积。

如何取消请求?

AbortController

AbortController 并不是专门为 Fetch 或 Axios 设计的,它是 DOM 原生提供的一个用于中止一个或多个 Web 请求(甚至事件监听器)的通用接口。 它的工作原理是:实例化一个控制器,拿到它身上的 signal 信号对象并传递给任何支持中止的 API。当你调用 controller.abort() 时,signal 对象会触发一个 abort 事件,底层的 API(如 fetch 或 setTimeout 封装)监听到这个信号后就会立即中断操作。

面试官常问:如果用户在搜索框疯狂输入,或者频繁切换 Tab,如何取消上一次还未完成的冗余请求?

Axios 取消请求

Axios 在 v0.22.0 之后,推荐使用现代浏览器的 AbortController API(原先的 CancelToken 已被废弃)。

// 1. 创建控制器
const controller = new AbortController();

// 2. 将 signal 传给 axios
axios
.get("/api/data", {
signal: controller.signal,
})
.catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log("请求被取消", thrown.message);
}
});

// 3. 在需要的时候取消请求
controller.abort("用户手动取消");

Fetch 取消请求

Fetch 本身没有 abort 方法,同样需要借助 AbortController

const controller = new AbortController();

fetch("/api/data", {
signal: controller.signal,
})
.then((res) => res.json())
.catch((err) => {
if (err.name === "AbortError") {
console.log("Fetch 请求被取消");
}
});

// 触发取消
controller.abort();

原生 XHR 取消请求

调用实例的 xhr.abort() 方法即可。

Fetch 如何实现超时控制?

因为 Fetch 默认没有 timeout 配置,面试官喜欢让你手写一个带有超时的 Fetch,考察你对 Promise 和底层 API 的理解:

function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);

return fetch(url, {
...options,
signal: controller.signal,
})
.then((response) => {
clearTimeout(id); // 请求成功,清除定时器
return response;
})
.catch((error) => {
clearTimeout(id);
// 拦截 abort 抛出的错误,将其转化为超时错误
if (error.name === "AbortError") {
throw new Error("Request timed out");
}
throw error;
});
}

接口重试机制

面试题:"接口请求偶尔会因为网络波动失败,如何实现一个自动重试机制?"

核心思路是:使用 for 循环(或递归),结合 try...catch 和延迟(Delay)来重新发起请求。通常还会要求支持配置最大重试次数重试间隔时间

/**
* 带自动重试机制的 Fetch
* @param {string} url 请求地址
* @param {object} options fetch 参数
* @param {number} maxRetries 最大重试次数
* @param {number} delay 每次重试的间隔延迟(ms)
*/
async function fetchWithRetry(url, options = {}, maxRetries = 3, delay = 1000) {
for (let i = 0; i <= maxRetries; i++) {
try {
const response = await fetch(url, options);
// HTTP 状态码 5xx 也可以视作失败,根据业务需求决定是否 throw
if (!response.ok && response.status >= 500) {
throw new Error(`Server Error: ${response.status}`);
}
return response; // 成功则直接跳出循环并返回
} catch (err) {
// 如果已经是最后一次重试,直接抛出错误
if (i === maxRetries) {
throw new Error(`Max retries reached. Last error: ${err.message}`);
}
// 否则等待一段时间后继续下一次循环
console.warn(`请求失败,${delay}ms 后进行第 ${i + 1} 次重试...`);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
}

// 使用方式:
fetchWithRetry("https://api.example.com/data", {}, 3, 2000)
.then((res) => res.json())
.catch((err) => console.error("彻底失败:", err));

进阶追问:如何用 Axios 实现全局的自动重试?

答:在 Axios 的响应拦截器(Response Interceptor)中处理。如果发现请求失败(或者拿到 5xx 状态码),可以从 error.config 中获取当前请求的配置,然后在拦截器里用一个 setTimeout 返回一个重新执行 axios.request(config) 的 Promise。业界也有成熟的库如 axios-retry 可以直接使用。

Promise 版本(不依赖 async/await)

面试追问:"如果不能用 async/await,纯 Promise 怎么实现重试?"

核心思路是:用 Promise 链 .then().catch() + 内部计数器来代替 for + await.catch 里返回一个新的 Promise(延迟后重试),自然就形成了"链式"重试。

function fetchWithRetry(url, options = {}, maxRetries = 3, delay = 1000) {
return new Promise((resolve, reject) => {
// 内部递归函数:每次失败后重新调用自己
function attempt(n) {
fetch(url, options)
.then((response) => {
if (!response.ok && response.status >= 500) {
throw new Error(`Server Error: ${response.status}`);
}
resolve(response); // ✅ 成功:外层 Promise resolve
})
.catch((err) => {
if (n === 0) {
// ❌ 重试次数用完:外层 Promise reject
return reject(
new Error(`Max retries reached. Last error: ${err.message}`),
);
}
console.warn(`${maxRetries - n + 1} 次失败,${delay}ms 后重试...`);
// ⏰ 延迟后递归调用,次数 -1
setTimeout(() => attempt(n - 1), delay);
});
}
attempt(maxRetries); // 从 maxRetries 开始倒数
});
}

// 使用方式(和 async 版本一样):
fetchWithRetry("https://api.example.com/data", {}, 3, 2000)
.then((res) => res.json())
.catch((err) => console.error("彻底失败:", err));

核心区别对比

维度async/await 版本Promise 链版本
控制流for 循环 + await内部递归函数 + setTimeout
暂停方式await new Promise(setTimeout)setTimeout 回调
计数器for 循环的 i函数参数 n(倒着减)
状态出口return / throw外层 resolve / reject
适用场景现代项目(ES2017+)老项目、需兼容低版本浏览器

记忆口诀:"await 是同步写法做异步事,Promise 链是用回调做异步事"——本质都是在微任务队列里串行排任务。