异步操作中止机制 AbortController() 详解

异步操作中止机制 AbortController是现代JavaScript中管理异步操作的重要工具,提供统一的中止机制。


它通过signal属性与fetch请求、事件监听器等Web API结合使用,能有效避免内存泄漏和竞态条件。


主要应用场景包括:

  • 取消用户操作(搜索/上传)
  • 组件生命周期管理
  • 超时控制和事件清理

使用时需注意:

  • 每次创建新控制器
  • 正确处理AbortError
  • 及时清理资源
  • 并考虑浏览器兼容性

最佳实践建议结合防抖/节流使用,为耗时操作提供中止选项。


AbortController() 详解

1. 基本概念

AbortController 是一个用于中止一个或多个异步操作的控制器对象。它常与以下 API 结合使用:

  • fetch() 请求

  • addEventListener() 事件监听器

  • 其他 Web API(如 ReadableStreamWebSocket 等)


2. 基本使用

创建控制器

复制代码
// 创建一个 AbortController 实例
const controller = new AbortController();

// 获取关联的 AbortSignal
const signal = controller.signal;

3. 与 fetch() 结合使用

3.1 中止单个 fetch 请求

复制代码
const controller = new AbortController();
const signal = controller.signal;

// 发起 fetch 请求,传入 signal
fetch('https://api.example.com/data', { signal })
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('请求被中止');
    } else {
      console.error('请求失败:', error);
    }
  });

// 在需要时中止请求
setTimeout(() => {
  controller.abort(); // 中止请求
}, 5000); // 5秒后中止

3.2 同时中止多个请求

复制代码
const controller = new AbortController();
const signal = controller.signal;

// 发起多个使用同一个 signal 的请求
const requests = [
  fetch('/api/user', { signal }),
  fetch('/api/posts', { signal }),
  fetch('/api/comments', { signal })
];

// 全部使用相同的 signal
Promise.all(requests)
  .then(responses => Promise.all(responses.map(r => r.json())))
  .catch(error => {
    if (error.name === 'AbortError') {
      console.log('所有请求都被中止');
    }
  });

// 中止所有请求
document.getElementById('cancel-btn').addEventListener('click', () => {
  controller.abort();
});

4. 与 addEventListener() 结合使用

4.1 管理事件监听器

复制代码
const controller = new AbortController();
const signal = controller.signal;

// 使用同一个 signal 注册多个事件
button.addEventListener('click', () => {
  console.log('按钮被点击');
}, { signal });

input.addEventListener('input', (e) => {
  console.log('输入:', e.target.value);
}, { signal });

// 移除所有通过该 signal 注册的事件监听器
controller.abort();

4.2 组件清理示例

复制代码
class MyComponent {
  constructor() {
    this.controller = new AbortController();
    this.signal = this.controller.signal;
    
    this.setupEventListeners();
  }
  
  setupEventListeners() {
    // 所有事件都使用同一个 signal
    document.addEventListener('click', this.handleClick.bind(this), { 
      signal: this.signal 
    });
    
    window.addEventListener('resize', this.handleResize.bind(this), { 
      signal: this.signal 
    });
    
    document.addEventListener('keydown', this.handleKeydown.bind(this), { 
      signal: this.signal 
    });
  }
  
  handleClick() { /* ... */ }
  handleResize() { /* ... */ }
  handleKeydown() { /* ... */ }
  
  // 组件销毁时清理所有事件监听器
  destroy() {
    this.controller.abort();
  }
}

// 使用
const component = new MyComponent();
// ... 稍后 ...
component.destroy(); // 清理所有事件监听器

5. AbortSignal 属性和方法

5.1 属性

复制代码
const controller = new AbortController();
const signal = controller.signal;

console.log(signal.aborted); // false,是否已中止
console.log(signal.reason);  // undefined,中止原因

controller.abort('用户取消了操作');
console.log(signal.aborted); // true
console.log(signal.reason);  // '用户取消了操作'

5.2 监听中止事件

复制代码
const controller = new AbortController();
const signal = controller.signal;

// 监听中止事件
signal.addEventListener('abort', () => {
  console.log('signal 被中止');
  console.log('原因:', signal.reason);
});

// 触发中止
controller.abort('手动中止');

6. 高级用法

6.1 超时自动中止

复制代码
function fetchWithTimeout(url, options = {}, timeout = 5000) {
  const controller = new AbortController();
  const signal = controller.signal;
  
  // 设置超时
  const timeoutId = setTimeout(() => {
    controller.abort(new Error(`请求超时 (${timeout}ms)`));
  }, timeout);
  
  // 发起请求
  return fetch(url, { ...options, signal })
    .finally(() => clearTimeout(timeoutId));
}

// 使用
fetchWithTimeout('https://api.example.com/data', {}, 3000)
  .then(response => response.json())
  .catch(error => {
    if (error.name === 'AbortError') {
      console.error('请求超时');
    }
  });

6.2 组合多个 signal

复制代码
function createCombinedSignal(signals) {
  const controller = new AbortController();
  
  signals.forEach(signal => {
    if (signal.aborted) {
      controller.abort(signal.reason);
    } else {
      signal.addEventListener('abort', () => {
        controller.abort(signal.reason);
      });
    }
  });
  
  return controller.signal;
}

// 使用
const controller1 = new AbortController();
const controller2 = new AbortController();

const combinedSignal = createCombinedSignal([
  controller1.signal,
  controller2.signal
]);

fetch('/api/data', { signal: combinedSignal });

// 任意一个 controller.abort() 都会中止 fetch
controller1.abort('controller1 中止');

6.3 与异步迭代器结合

复制代码
async function* asyncGenerator(signal) {
  let i = 0;
  
  while (!signal.aborted) {
    // 模拟异步操作
    await new Promise(resolve => setTimeout(resolve, 1000));
    yield i++;
  }
  
  console.log('生成器被中止');
}

const controller = new AbortController();

(async () => {
  for await (const value of asyncGenerator(controller.signal)) {
    console.log(value);
    if (value >= 5) {
      controller.abort();
    }
  }
})();

7. 实际应用场景

7.1 搜索框防抖 + 请求取消

复制代码
class SearchComponent {
  constructor() {
    this.controller = null;
    this.timeoutId = null;
    
    this.input = document.getElementById('search-input');
    this.results = document.getElementById('search-results');
    
    this.init();
  }
  
  init() {
    this.input.addEventListener('input', (e) => {
      this.handleSearch(e.target.value);
    });
  }
  
  handleSearch(query) {
    // 取消之前的请求
    if (this.controller) {
      this.controller.abort();
    }
    
    // 清除之前的防抖定时器
    clearTimeout(this.timeoutId);
    
    // 设置新的防抖
    this.timeoutId = setTimeout(() => {
      this.performSearch(query);
    }, 300);
  }
  
  performSearch(query) {
    // 创建新的控制器
    this.controller = new AbortController();
    
    fetch(`/api/search?q=${encodeURIComponent(query)}`, {
      signal: this.controller.signal
    })
      .then(response => response.json())
      .then(data => {
        this.displayResults(data);
      })
      .catch(error => {
        if (error.name !== 'AbortError') {
          console.error('搜索失败:', error);
        }
      });
  }
  
  displayResults(data) {
    // 显示结果...
  }
}

7.2 表单提交防重复

复制代码
class FormSubmitter {
  constructor(formId) {
    this.form = document.getElementById(formId);
    this.submitButton = this.form.querySelector('[type="submit"]');
    this.controller = null;
    this.init();
  }
  
  init() {
    this.form.addEventListener('submit', async (e) => {
      e.preventDefault();
      
      // 如果已有请求在进行,先中止
      if (this.controller) {
        this.controller.abort();
      }
      
      await this.submitForm();
    });
  }
  
  async submitForm() {
    this.controller = new AbortController();
    this.submitButton.disabled = true;
    
    const formData = new FormData(this.form);
    
    try {
      const response = await fetch('/api/submit', {
        method: 'POST',
        body: formData,
        signal: this.controller.signal
      });
      
      const result = await response.json();
      this.handleSuccess(result);
    } catch (error) {
      if (error.name !== 'AbortError') {
        this.handleError(error);
      }
    } finally {
      this.submitButton.disabled = false;
      this.controller = null;
    }
  }
  
  handleSuccess(result) { /* ... */ }
  handleError(error) { /* ... */ }
  
  // 允许外部中止
  abort() {
    if (this.controller) {
      this.controller.abort();
    }
  }
}

8. 注意事项和最佳实践

8.1 错误处理

复制代码
try {
  const response = await fetch(url, { signal });
  // 处理响应
} catch (error) {
  if (error.name === 'AbortError') {
    // 请求被中止,通常是用户操作
    console.log('操作被取消');
  } else {
    // 其他错误
    console.error('请求失败:', error);
  }
}

8.2 内存管理

复制代码
// 正确:使用 finally 清理资源
const controller = new AbortController();

try {
  // ... 异步操作
} finally {
  // 清理工作
  controller.abort();
}

// 或使用 try...catch...finally
try {
  // ...
} catch (error) {
  // ...
} finally {
  controller.abort();
}

8.3 浏览器兼容性

复制代码
// 检查是否支持 AbortController
if (typeof AbortController !== 'undefined') {
  // 使用 AbortController
  const controller = new AbortController();
} else {
  // 降级方案
  console.warn('AbortController 不支持,使用传统方式');
}

8.4 不要重复使用

复制代码
// ❌ 错误:重复使用已中止的 controller
const controller = new AbortController();
controller.abort();

// 这个 signal 已经是中止状态
fetch(url, { signal: controller.signal }); // 会立即被拒绝

// ✅ 正确:每次创建新的
function makeRequest() {
  const controller = new AbortController();
  return fetch(url, { signal: controller.signal });
}

总结

AbortController 是现代 JavaScript 中管理异步操作生命周期的重要工具:

  1. 核心优势

    • 统一的中止机制

    • 更好的内存管理

    • 避免竞态条件

    • 提高用户体验

  2. 适用场景

    • 取消用户触发的操作(搜索、上传、下载)

    • 组件生命周期管理

    • 超时控制

    • 清理事件监听器

  3. 最佳实践

    • 及时清理不再需要的控制器

    • 正确处理 AbortError

    • 为长时间运行的操作提供中止机制

    • 结合防抖/节流使用