【前端面试小册】JS-第8节:Promise 相关题目与进阶应用
一、Promise 的取消机制
1.1 Promise 无法直接取消
核心问题:Promise 一旦进入 pending 状态,就无法直接取消。
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('完成');
}, 5000);
});
// ❌ Promise 没有 cancel() 方法
// promise.cancel(); // 不存在此方法
// Promise 只能等待被解决(fulfilled)或被拒绝(rejected)
promise.then(result => {
console.log(result); // 5 秒后会输出:完成
});
原因分析:
graph TD
A[创建 Promise] --> B[进入 pending 状态]
B --> C{等待异步操作}
C --> D[fulfilled: 成功]
C --> E[rejected: 失败]
D --> F[Promise 生命周期结束]
E --> F
G[无法中途取消] -.->|不支持| B
关键理解:
- Promise 的设计哲学是"一旦开始,就要完成"
- 一旦进入
pending状态,只能等待resolve或reject - 这是 Promise 与可取消操作(如 HTTP 请求)的根本区别
1.2 为什么需要取消 Promise?
在实际开发中,取消异步操作的需求很常见:
// 场景:用户快速切换标签,之前的请求应该取消
async function loadUserData(userId) {
const data = await fetch(`/api/users/${userId}`);
return data.json();
}
// 用户点击标签1
loadUserData(1); // 开始加载
// 用户快速切换到标签2
loadUserData(2); // 新请求
// 但标签1的请求仍在进行,浪费资源
常见场景:
- HTTP 请求取消(用户切换页面、组件卸载)
- 定时器清理(组件销毁时取消定时任务)
- 搜索防抖(新的搜索请求应该取消旧的)
- 文件上传/下载取消
二、实现 Promise 取消的方案
方案一:使用可取消的 Promise 库
一些第三方库提供了可取消的 Promise 功能:
// 示例:使用 makeCancelable 工具函数
function makeCancelable(promise) {
let hasCanceled = false;
const wrappedPromise = new Promise((resolve, reject) => {
promise.then(
value => hasCanceled ? reject({ isCanceled: true }) : resolve(value),
error => hasCanceled ? reject({ isCanceled: true }) : reject(error)
);
});
return {
promise: wrappedPromise,
cancel() {
hasCanceled = true;
}
};
}
// 使用示例
const somePromise = new Promise(resolve => {
setTimeout(() => resolve('完成'), 5000);
});
const cancelable = makeCancelable(somePromise);
cancelable.promise
.then(result => console.log(result))
.catch(error => {
if (error.isCanceled) {
console.log('Promise 已取消');
}
});
// 2 秒后取消
setTimeout(() => {
cancelable.cancel();
}, 2000);
原理说明:
- 使用标志位
hasCanceled标记是否已取消 - Promise 完成时检查标志位
- 如果已取消,返回特殊的错误对象
方案二:使用 AbortController(推荐)
AbortController 是现代浏览器提供的标准 API,用于取消异步操作。
// ✅ 使用 AbortController 取消 fetch 请求
const controller = new AbortController();
const signal = controller.signal;
// 发起 fetch 请求,传入 signal
const url = 'https://api.example.com/data';
const request = fetch(url, {
method: 'GET',
signal: signal // 传入 AbortSignal
});
// 处理请求结果
request
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
console.log('数据:', data);
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('请求已取消');
} else {
console.error('请求失败:', error.message);
}
});
// 500ms 后取消请求
setTimeout(() => {
controller.abort(); // 取消请求
console.log('请求已取消');
}, 500);
AbortController 执行流程:
graph TD
A[创建 AbortController] --> B[获取 signal]
B --> C[发起 fetch 请求]
C --> D[传入 signal]
D --> E{调用 abort}
E -->|是| F[触发 AbortError]
E -->|否| G[正常完成请求]
F --> H[catch 捕获 AbortError]
G --> I[then 处理结果]
关键点:
AbortController是 Web 标准 API,现代浏览器都支持signal可以传递给任何支持它的 API(fetch、ReadableStream 等)- 取消时会抛出
AbortError,需要特殊处理
方案三:封装可取消的 Promise 工具
// 创建一个可取消的 Promise 封装
function cancellablePromise(executor) {
let cancel;
const promise = new Promise((resolve, reject) => {
cancel = (reason) => {
reject(new Error(reason || 'Promise 已取消'));
};
executor(resolve, reject, () => {
// 传入一个取消函数
return cancel;
});
});
return {
promise,
cancel: (reason) => cancel(reason)
};
}
// 使用示例
const { promise, cancel } = cancellablePromise((resolve, reject, getCancel) => {
const timer = setTimeout(() => {
resolve('操作完成');
}, 5000);
// 保存取消函数,用于清理资源
const cancelFn = getCancel();
// 可以在这里做一些清理工作
// 注意:这只是示例,实际使用需要更复杂的逻辑
});
promise
.then(result => console.log(result))
.catch(error => console.log('错误:', error.message));
// 2 秒后取消
setTimeout(() => {
cancel('用户取消操作');
}, 2000);
三、axios 取消请求的实现原理
3.1 axios CancelToken(旧版 API)
axios 提供了 CancelToken 来取消请求(注意:axios 0.22.0+ 已废弃,推荐使用 AbortController):
import axios from 'axios';
// 创建 CancelToken 实例
const cancelToken1 = axios.CancelToken.source();
const cancelToken2 = axios.CancelToken.source();
// 发起多个请求,分别添加对应的 CancelToken
const request1 = axios.get('https://api.example.com/data1', {
cancelToken: cancelToken1.token
});
const request2 = axios.get('https://api.example.com/data2', {
cancelToken: cancelToken2.token
});
// 取消 request1 请求
cancelToken1.cancel('Request 1 canceled by user');
// 处理请求结果
Promise.all([request1, request2])
.then(([response1, response2]) => {
console.log('Response 1:', response1.data);
console.log('Response 2:', response2.data);
})
.catch(error => {
if (axios.isCancel(error)) {
// 请求被取消
console.log('请求已取消:', error.message);
} else {
// 其他错误
console.error('请求失败:', error.message);
}
});
3.2 axios 取消请求原理
实现原理:
graph TD
A[创建 CancelToken.source] --> B[返回 token 和 cancel 方法]
B --> C[发起 axios 请求]
C --> D[传入 cancelToken]
D --> E[axios 内部监听 token]
E --> F{调用 cancel 方法}
F -->|是| G[token Promise rejected]
F -->|否| H[正常请求流程]
G --> I[中断 XMLHttpRequest]
I --> J[请求被取消]
H --> K[请求完成]
详细步骤:
-
创建 CancelToken 实例:
const source = axios.CancelToken.source(); // 返回 { token: CancelToken实例, cancel: 取消函数 } -
token 的内部实现:
// 简化版原理 class CancelToken { constructor(executor) { let cancel; this.promise = new Promise(resolve => { cancel = resolve; // 当调用 cancel 时,Promise 被 resolve }); executor(cancel); } } -
axios 内部处理:
- axios 监听
cancelToken.promise - 如果 Promise 被 resolve(即调用了 cancel),则中断请求
- 使用
XMLHttpRequest.abort()中断网络请求
- axios 监听
-
中断请求:
// axios 内部伪代码 if (config.cancelToken) { config.cancelToken.promise.then(cancel => { xhr.abort(); // 中断 XMLHttpRequest reject(new Cancel('Request canceled')); }); }
3.3 axios 新版本使用 AbortController(推荐)
axios 0.22.0+ 推荐使用 AbortController:
import axios from 'axios';
// 创建 AbortController
const controller = new AbortController();
// 发起请求
const request = axios.get('https://api.example.com/data', {
signal: controller.signal // 传入 signal
});
// 取消请求
controller.abort();
// 处理结果
request
.then(response => console.log(response.data))
.catch(error => {
if (axios.isCancel(error)) {
console.log('请求已取消');
}
});
四、实战应用场景
场景一:React 组件中的请求取消
import { useEffect, useRef } from 'react';
import axios from 'axios';
function UserProfile({ userId }) {
const cancelTokenRef = useRef(null);
useEffect(() => {
// 创建取消令牌
const source = axios.CancelToken.source();
cancelTokenRef.current = source;
// 发起请求
axios.get(`/api/users/${userId}`, {
cancelToken: source.token
})
.then(response => {
console.log('用户数据:', response.data);
})
.catch(error => {
if (axios.isCancel(error)) {
console.log('请求已取消(组件卸载或 userId 变化)');
} else {
console.error('请求失败:', error);
}
});
// 清理函数:组件卸载或 userId 变化时取消请求
return () => {
source.cancel('组件卸载或参数变化');
};
}, [userId]);
return <div>用户信息加载中...</div>;
}
场景二:搜索防抖与请求取消
function createSearchWithCancel() {
let currentController = null;
return function search(keyword) {
// 取消之前的请求
if (currentController) {
currentController.abort();
}
// 创建新的控制器
currentController = new AbortController();
// 发起新请求
return fetch(`/api/search?q=${keyword}`, {
signal: currentController.signal
})
.then(response => response.json())
.catch(error => {
if (error.name === 'AbortError') {
console.log('搜索已取消(新的搜索请求)');
} else {
throw error;
}
});
};
}
const search = createSearchWithCancel();
// 用户快速输入
search('a'); // 请求1
search('ab'); // 取消请求1,发起请求2
search('abc'); // 取消请求2,发起请求3
场景三:文件上传取消
async function uploadFile(file, onProgress, signal) {
const formData = new FormData();
formData.append('file', file);
return fetch('/api/upload', {
method: 'POST',
body: formData,
signal: signal, // 支持取消
// 使用 XMLHttpRequest 可以监听上传进度
})
.then(response => response.json())
.catch(error => {
if (error.name === 'AbortError') {
console.log('文件上传已取消');
} else {
throw error;
}
});
}
// 使用示例
const controller = new AbortController();
uploadFile(file, onProgress, controller.signal);
// 用户点击取消按钮
document.getElementById('cancelBtn').addEventListener('click', () => {
controller.abort();
});
五、最佳实践与注意事项
1. 始终检查取消错误
// ✅ 正确处理取消错误
fetch(url, { signal })
.then(response => response.json())
.catch(error => {
if (error.name === 'AbortError') {
// 请求被取消,这是正常情况,不需要特殊处理
console.log('请求已取消');
return;
}
// 其他错误需要处理
console.error('请求失败:', error);
});
2. 清理相关资源
// ✅ 取消请求时清理相关资源
const controller = new AbortController();
let timer = null;
fetch(url, { signal: controller.signal })
.then(response => response.json())
.then(data => {
// 清理定时器
if (timer) clearTimeout(timer);
console.log(data);
})
.catch(error => {
if (error.name === 'AbortError') {
// 清理资源
if (timer) clearTimeout(timer);
console.log('请求已取消');
}
});
// 设置超时
timer = setTimeout(() => {
controller.abort();
}, 5000);
3. React 组件中的使用
// ✅ 在 useEffect 中正确使用
useEffect(() => {
const controller = new AbortController();
fetchData(controller.signal)
.then(data => setData(data))
.catch(error => {
if (error.name !== 'AbortError') {
setError(error);
}
});
// 清理函数
return () => {
controller.abort();
};
}, [dependencies]);
4. 避免内存泄漏
// ❌ 不好的做法:没有清理
function loadData() {
fetch('/api/data')
.then(response => response.json())
.then(data => {
// 如果组件已卸载,这里仍然会执行,可能导致内存泄漏
setData(data);
});
}
// ✅ 好的做法:使用 AbortController 清理
function loadData() {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(response => response.json())
.then(data => {
setData(data);
});
return () => controller.abort();
}
六、面试要点总结
核心知识点
- Promise 无法直接取消:一旦进入
pending状态,只能等待完成或被拒绝 - 取消方案:
- 使用
AbortController(推荐,标准 API) - 使用第三方库(如 axios 的 CancelToken)
- 自定义封装(使用标志位)
- 使用
- axios 取消原理:基于
CancelToken或AbortController,内部调用XMLHttpRequest.abort()
常见面试题
Q1: Promise 可以取消吗?
答:原生 Promise 无法直接取消。一旦进入 pending 状态,只能等待 resolve 或 reject。但可以通过 AbortController、第三方库或自定义封装实现取消功能。
Q2: 如何实现 Promise 取消?
答:
- 使用 AbortController(推荐):现代浏览器的标准 API,支持 fetch、ReadableStream 等
- 使用第三方库:如 axios 的 CancelToken(旧版)或 AbortController(新版)
- 自定义封装:使用标志位 + Promise rejection 模拟取消
Q3: axios 取消请求的原理是什么?
答:
- 创建
CancelToken实例(旧版)或使用AbortController(新版) - 将 token/signal 传入请求配置
- axios 内部监听取消信号
- 调用
XMLHttpRequest.abort()中断网络请求 - Promise 被 reject,触发错误处理
实战建议
- ✅ 新项目使用
AbortController(标准 API,兼容性好) - ✅ React 组件中在
useEffect清理函数中取消请求 - ✅ 搜索、上传等场景要及时取消旧的请求
- ✅ 正确处理
AbortError,避免误报错误 - ✅ 取消请求时记得清理相关资源(定时器、监听器等)