如何实现 Promise 的取消?
Promise 的取消是一个常见但复杂的问题。Promise 本身不支持取消,但我们可以通过一些技巧来实现类似的功能。为什么 Promise 不能被取消?Promise 的设计遵循"不可逆"原则:一旦 Promise 状态改变(pending → fulfilled 或 pending → rejected),就不能再改变这种设计保证了 Promise 的可靠性和可预测性取消操作会引入额外的复杂性和不确定性实现取消的几种方法1. 使用 AbortControllerAbortController 是现代浏览器提供的取消异步操作的标准 API。基本用法const controller = new AbortController();const signal = controller.signal;fetch('/api/data', { signal }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => { if (error.name === 'AbortError') { console.log('请求被取消'); } else { console.error('请求失败:', error); } });// 取消请求controller.abort();封装可取消的 fetchfunction fetchWithTimeout(url, options = {}, timeout = 5000) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); return fetch(url, { ...options, signal: controller.signal }) .then(response => { clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .catch(error => { clearTimeout(timeoutId); if (error.name === 'AbortError') { throw new Error('请求超时'); } throw error; });}// 使用示例fetchWithTimeout('/api/data', {}, 3000) .then(data => console.log(data)) .catch(error => console.error(error));2. 使用包装函数通过包装函数来实现取消功能。基本实现function makeCancellable(promise) { let hasCancelled = false; const wrappedPromise = new Promise((resolve, reject) => { promise.then( value => { if (!hasCancelled) resolve(value); }, error => { if (!hasCancelled) reject(error); } ); }); return { promise: wrappedPromise, cancel: () => { hasCancelled = true; } };}// 使用示例const { promise, cancel } = makeCancellable( fetch('/api/data').then(r => r.json()));promise .then(data => console.log(data)) .catch(error => { if (error.name === 'CancellationError') { console.log('操作被取消'); } else { console.error('操作失败:', error); } });// 取消操作cancel();完整实现class CancellablePromise { constructor(executor) { this.isCancelled = false; this.rejectors = []; this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; executor( value => { if (!this.isCancelled) { resolve(value); } }, error => { if (!this.isCancelled) { reject(error); } } ); }); } cancel() { this.isCancelled = true; this.rejectors.forEach(rejector => { rejector(new Error('Promise cancelled')); }); } then(onFulfilled, onRejected) { const promise = this.promise.then(onFulfilled, onRejected); return new CancellablePromise((resolve, reject) => { this.rejectors.push(reject); promise.then(resolve, reject); }); } catch(onRejected) { return this.then(null, onRejected); } finally(onFinally) { return this.then( value => { onFinally(); return value; }, error => { onFinally(); throw error; } ); }}// 使用示例const cancellablePromise = new CancellablePromise((resolve) => { setTimeout(() => { resolve('完成'); }, 2000);});cancellablePromise .then(result => console.log(result)) .catch(error => console.error(error));// 取消setTimeout(() => { cancellablePromise.cancel();}, 1000);3. 使用令牌(Token)模式通过传递令牌来检查是否应该继续执行。class CancellationToken { constructor() { this.isCancelled = false; } cancel() { this.isCancelled = true; } throwIfCancelled() { if (this.isCancelled) { throw new Error('Operation cancelled'); } }}function fetchWithToken(url, token) { return fetch(url) .then(response => { token.throwIfCancelled(); return response.json(); }) .then(data => { token.throwIfCancelled(); return data; });}// 使用示例const token = new CancellationToken();fetchWithToken('/api/data', token) .then(data => console.log(data)) .catch(error => { if (error.message === 'Operation cancelled') { console.log('操作被取消'); } else { console.error('操作失败:', error); } });// 取消操作setTimeout(() => { token.cancel();}, 1000);4. 使用 Promise.race 实现超时function promiseWithTimeout(promise, timeout = 5000) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error('Timeout')); }, timeout); }); return Promise.race([promise, timeoutPromise]);}// 使用示例promiseWithTimeout( fetch('/api/data').then(r => r.json()), 3000) .then(data => console.log(data)) .catch(error => { if (error.message === 'Timeout') { console.log('请求超时'); } else { console.error('请求失败:', error); } });实际应用场景1. 取消重复的搜索请求class SearchService { constructor() { this.currentController = null; } async search(query) { // 取消之前的请求 if (this.currentController) { this.currentController.abort(); } this.currentController = new AbortController(); try { const response = await fetch(`/api/search?q=${query}`, { signal: this.currentController.signal }); return await response.json(); } catch (error) { if (error.name === 'AbortError') { console.log('搜索请求被取消'); return null; } throw error; } }}// 使用示例const searchService = new SearchService();// 用户快速输入,只保留最后一次搜索searchService.search('hello');searchService.search('hello world');searchService.search('hello world example');2. 取消长时间运行的任务class TaskManager { constructor() { this.tasks = new Map(); } async runTask(taskId, task) { const controller = new AbortController(); this.tasks.set(taskId, controller); try { const result = await task(controller.signal); return result; } catch (error) { if (error.name === 'AbortError') { console.log(`任务 ${taskId} 被取消`); return null; } throw error; } finally { this.tasks.delete(taskId); } } cancelTask(taskId) { const controller = this.tasks.get(taskId); if (controller) { controller.abort(); } }}// 使用示例const taskManager = new TaskManager();// 运行任务taskManager.runTask('task1', async (signal) => { for (let i = 0; i < 10; i++) { signal.throwIfAborted(); await new Promise(resolve => setTimeout(resolve, 1000)); console.log(`步骤 ${i + 1} 完成`); } return '任务完成';});// 取消任务setTimeout(() => { taskManager.cancelTask('task1');}, 3000);3. 组件卸载时取消请求class Component { constructor() { this.controller = new AbortController(); } async fetchData() { try { const response = await fetch('/api/data', { signal: this.controller.signal }); return await response.json(); } catch (error) { if (error.name === 'AbortError') { console.log('组件卸载,请求被取消'); return null; } throw error; } } destroy() { this.controller.abort(); }}// 使用示例const component = new Component();component.fetchData().then(data => { if (data) { console.log('数据加载成功:', data); }});// 组件卸载时setTimeout(() => { component.destroy();}, 1000);最佳实践1. 总是清理资源async function fetchData() { const controller = new AbortController(); try { const response = await fetch('/api/data', { signal: controller.signal }); return await response.json(); } finally { controller.abort(); }}2. 提供取消回调function fetchWithCancel(url, onCancel) { const controller = new AbortController(); const promise = fetch(url, { signal: controller.signal }) .then(response => response.json()); promise.cancel = () => { controller.abort(); if (onCancel) { onCancel(); } }; return promise;}// 使用示例const promise = fetchWithCancel('/api/data', () => { console.log('请求被取消');});promise.then(data => console.log(data));// 取消请求promise.cancel();3. 处理取消错误async function fetchWithCancellation(url) { const controller = new AbortController(); try { const response = await fetch(url, { signal: controller.signal }); return await response.json(); } catch (error) { if (error.name === 'AbortError') { console.log('请求被取消'); return null; } throw error; }}总结Promise 本身不支持取消:需要通过其他方式实现AbortController 是标准方案:现代浏览器推荐使用包装函数提供灵活性:可以根据需求定制取消逻辑令牌模式适合复杂场景:可以精细控制取消时机总是清理资源:避免内存泄漏处理取消错误:区分取消错误和其他错误提供取消回调:让调用者知道操作被取消考虑用户体验:取消操作应该快速响应