乐闻世界logo
搜索文章和话题

如何实现 Promise 的取消?

2月22日 14:07

Promise 的取消是一个常见但复杂的问题。Promise 本身不支持取消,但我们可以通过一些技巧来实现类似的功能。

为什么 Promise 不能被取消?

Promise 的设计遵循"不可逆"原则:

  • 一旦 Promise 状态改变(pending → fulfilled 或 pending → rejected),就不能再改变
  • 这种设计保证了 Promise 的可靠性和可预测性
  • 取消操作会引入额外的复杂性和不确定性

实现取消的几种方法

1. 使用 AbortController

AbortController 是现代浏览器提供的取消异步操作的标准 API。

基本用法

javascript
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();

封装可取消的 fetch

javascript
function 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. 使用包装函数

通过包装函数来实现取消功能。

基本实现

javascript
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();

完整实现

javascript
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)模式

通过传递令牌来检查是否应该继续执行。

javascript
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 实现超时

javascript
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. 取消重复的搜索请求

javascript
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. 取消长时间运行的任务

javascript
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. 组件卸载时取消请求

javascript
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. 总是清理资源

javascript
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. 提供取消回调

javascript
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. 处理取消错误

javascript
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; } }

总结

  1. Promise 本身不支持取消:需要通过其他方式实现
  2. AbortController 是标准方案:现代浏览器推荐使用
  3. 包装函数提供灵活性:可以根据需求定制取消逻辑
  4. 令牌模式适合复杂场景:可以精细控制取消时机
  5. 总是清理资源:避免内存泄漏
  6. 处理取消错误:区分取消错误和其他错误
  7. 提供取消回调:让调用者知道操作被取消
  8. 考虑用户体验:取消操作应该快速响应
标签:Promise