Promise 的取消是一个常见但复杂的问题。Promise 本身不支持取消,但我们可以通过一些技巧来实现类似的功能。
为什么 Promise 不能被取消?
Promise 的设计遵循"不可逆"原则:
- 一旦 Promise 状态改变(pending → fulfilled 或 pending → rejected),就不能再改变
- 这种设计保证了 Promise 的可靠性和可预测性
- 取消操作会引入额外的复杂性和不确定性
实现取消的几种方法
1. 使用 AbortController
AbortController 是现代浏览器提供的取消异步操作的标准 API。
基本用法
javascriptconst 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();
封装可取消的 fetch
javascriptfunction 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. 使用包装函数
通过包装函数来实现取消功能。
基本实现
javascriptfunction 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();
完整实现
javascriptclass 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)模式
通过传递令牌来检查是否应该继续执行。
javascriptclass 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 实现超时
javascriptfunction 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. 取消重复的搜索请求
javascriptclass 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. 取消长时间运行的任务
javascriptclass 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. 组件卸载时取消请求
javascriptclass 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. 总是清理资源
javascriptasync function fetchData() { const controller = new AbortController(); try { const response = await fetch('/api/data', { signal: controller.signal }); return await response.json(); } finally { controller.abort(); } }
2. 提供取消回调
javascriptfunction 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. 处理取消错误
javascriptasync 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 是标准方案:现代浏览器推荐使用
- 包装函数提供灵活性:可以根据需求定制取消逻辑
- 令牌模式适合复杂场景:可以精细控制取消时机
- 总是清理资源:避免内存泄漏
- 处理取消错误:区分取消错误和其他错误
- 提供取消回调:让调用者知道操作被取消
- 考虑用户体验:取消操作应该快速响应