AbortController 深度实战:从 fetch 取消到复杂异步流程控制

全面解析 JavaScript AbortController 与 AbortSignal 的高级用法,涵盖 fetch 取消、竞态条件处理、超时控制、信号组合、React 清理集成等生产级模式,附完整代码示例与性能对比。

前端开发 2026-05-31 18 分钟

你写过多少次 fetch 却从来没处理过取消逻辑?根据 HTTP Archive 2025 年的统计,超过 87% 的前端 fetch 调用没有关联 AbortSignal,这意味着用户快速切换页面时,旧请求仍在后台运行,轻则浪费带宽和服务器资源,重则导致状态竞态(Race Condition)——用户看到的数据来自过期请求,界面展示的内容与预期完全不一致。AbortController 是浏览器原生提供的异步取消基础设施,从 ES2017 引入到如今已经经历了近十年的演进,新增了 AbortSignal.timeout()、AbortSignal.any() 等强大方法。但大多数开发者只知道 signal 传给 fetch 就完事了,完全没有意识到它在整个异步编程体系中的战略地位。本文将从核心机制到生产级模式,系统性地拆解 AbortController 的全部能力,让你的异步代码从此具备优雅的取消和清理能力。

🔌 一、AbortController 核心机制

AbortController 与 AbortSignal 的关系

AbortController 是一个极其精简的设计模式——Controller 负责触发取消,Signal 负责传播取消。这种发布-订阅(Publish-Subscribe)模式的核心优势在于解耦:触发取消的一方不需要知道有多少个消费者在监听,而消费者也不需要了解取消是如何产生的。这与 Go 语言的 context.Context 的设计理念一脉相承,都是为了让取消信号能够在复杂的调用链中自然传播。

从实现层面看,AbortController 内部只持有一个 AbortSignal 实例的引用。当你调用 controller.abort() 时,它会将 signal 的 aborted 标志设为 true,保存 reason,然后触发 signal 上所有注册的 abort 事件监听器。这个过程是同步的——一旦调用 abort(),所有监听器会在同一个微任务中依次执行。

// AbortController 基础结构与信号传播机制
const controller = new AbortController();
const signal = controller.signal;

// signal 是一个标准的 EventTarget,可以监听 'abort' 事件
// 多个监听器可以同时注册,它们会按照注册顺序依次执行
signal.addEventListener('abort', () => {
  console.log('第一个监听器 - 已取消:', signal.reason);
});

signal.addEventListener('abort', () => {
  console.log('第二个监听器 - aborted 状态:', signal.aborted);
});

// controller.abort() 触发取消,参数作为 reason 存储
controller.abort('用户主动取消');

// abort 之后,signal 永久处于已取消状态
console.log(signal.aborted);  // true
console.log(signal.reason);   // '用户主动取消'

// 再次调用 abort() 不会报错,但也不会再次触发事件
controller.abort('第二次调用无效');

以下是 AbortController 和 AbortSignal 的完整 API 一览:

属性/方法所属类型说明
controller.abort(reason?)Controller方法触发取消,可传入任意类型的 reason
controller.signalController属性返回关联的 AbortSignal 实例
signal.abortedSignal只读属性布尔值,表示是否已取消
signal.reasonSignal只读属性取消原因,未取消时为 undefined
signal.throwIfAborted()Signal方法若已取消则抛出 reason(默认 AbortError)
signal.onabortSignal事件处理器取消事件的回调属性
AbortSignal.timeout(ms)Signal 静态方法创建超时自动取消的信号
AbortSignal.any(signals)Signal 静态方法组合多个信号,任一触发即触发
AbortSignal.abort(reason?)Signal 静态方法创建一个立即已取消的信号

📌 记住: AbortController 是一次性消费品——一旦调用 abort(),信号永远处于 aborted 状态,无法重置或复用。如果你需要”可重置”的取消逻辑(比如一个可反复启动和停止的定时器),每次操作都必须创建全新的 Controller。不要试图缓存或复用已经 abort 过的 controller。

fetch 的取消语义与错误处理

当 fetch 请求关联的 AbortSignal 被触发时,fetch 的行为取决于请求当时所处的阶段。理解这些阶段对正确处理错误至关重要:

阶段一:请求尚未发出——如果在 fetch 调用之前 signal 已经处于 aborted 状态,fetch 会立即 reject 一个 AbortError,网络层不会发出任何请求。这个行为对于那些在组件卸载后才到达的延迟操作特别有用。

阶段二:请求已发出,等待响应中——浏览器会中断底层的 HTTP 连接(发送 RST 或关闭 TCP 连接),然后 reject 一个 AbortError。此时服务器可能已经收到了请求并在处理中,但客户端不会再接收响应。

阶段三:响应已到达,正在读取 body——如果在 response.json()、response.text() 或通过 ReadableStream 读取响应体的过程中 signal 被触发,读取操作会被中断并 reject。

// fetch 取消的完整错误处理模式
async function fetchWithProperAbort(url, signal) {
  try {
    const response = await fetch(url, { signal });

    // 注意:response.json() 也可能被取消
    // 如果 signal 在 json() 执行期间被触发
    const data = await response.json();
    return data;
  } catch (err) {
    // 统一用 err.name 判断,不要匹配 err.message
    // 因为不同浏览器的错误消息完全不同:
    // Chrome:  "The user aborted a request."
    // Firefox: "The operation was aborted."
    // Safari:  "The operation couldn't be completed. (kCFErrorDomainCFNetwork error -999.)"
    if (err.name === 'AbortError') {
      console.log('请求被取消:', signal.reason);
      return null;
    }

    // AbortSignal.timeout() 产生的错误类型是 TimeoutError
    // 注意这不是 AbortError,需要单独处理
    if (err.name === 'TimeoutError') {
      console.log('请求超时,服务端可能繁忙或网络不稳定');
      return null;
    }

    // 其他网络错误正常抛出
    throw err;
  }
}

⚠️ 警告: AbortError 的 err.message 在不同浏览器中完全不同。Chrome 是 “The user aborted a request.”,Firefox 是 “The operation was aborted.”,Safari 的措辞又不一样。永远用 err.name === 'AbortError' 来判断取消,绝对不要匹配 err.message。同理,超时错误用 err.name === 'TimeoutError' 判断。

🏎️ 二、六大生产级模式

模式一:搜索框防抖 + 竞态消除

这是 AbortController 最经典也最实用的场景。想象一个电商网站的搜索框:用户快速输入 “mackbook” 时,每个字符都会触发搜索请求。如果没有取消机制,可能出现以下竞态——用户输入 “mack” 时发出请求 A,继续输入到 “mackbook” 后发出请求 B。如果请求 A 因为网络延迟反而比请求 B 更晚返回,那么最终渲染的是 “mack” 的搜索结果,而非用户期望的 “mackbook” 结果。防抖(Debounce)减少请求次数,取消(Abort)消除请求竞态,两者配合使用才能保证搜索体验既高效又正确。

// 搜索框竞态消除 —— 生产环境中最常用的 AbortController 模式
let searchController = null;

async function searchProducts(query) {
  // 取消上一次未完成的请求
  // 如果上一次请求已经完成,abort() 是无害的空操作
  if (searchController) {
    searchController.abort('新搜索取代旧搜索');
  }

  // 每次搜索创建新的 controller
  // 旧 controller 会被 GC 自动回收
  searchController = new AbortController();

  try {
    const response = await fetch(
      `/api/search?q=${encodeURIComponent(query)}&limit=20`,
      { signal: searchController.signal }
    );

    if (!response.ok) throw new Error(`HTTP ${response.status}`);

    const results = await response.json();

    // 渲染搜索结果
    renderSearchResults(results);
    updateResultCount(results.total);
  } catch (err) {
    // AbortError 表示被新请求取代,静默忽略
    if (err.name === 'AbortError') {
      return;
    }
    // 真正的错误才提示用户
    showError('搜索失败,请重试');
  }
}

// 配合防抖使用:用户停止输入 300ms 后才发起搜索
const debouncedSearch = debounce((query) => {
  if (query.length >= 2) {
    searchProducts(query);
  } else {
    clearSearchResults();
    hideSuggestions();
  }
}, 300);

document.getElementById('search-input')
  .addEventListener('input', (e) => debouncedSearch(e.target.value));

// 简单但可靠的防抖实现
function debounce(fn, delay) {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => fn(...args), delay);
  };
}

模式二:AbortSignal.timeout() 超时控制

在 AbortSignal.timeout() 出现之前,实现请求超时需要手动创建 controller 和 setTimeout,还要在 finally 中清理 timer,代码繁琐且容易出错。ES2023 引入的这个静态方法彻底改变了局面:一行代码就能创建超时信号,无需手动清理,底层由浏览器自动管理定时器的生命周期。

// ✅ 现代写法:AbortSignal.timeout() 一行搞定
// 浏览器自动管理定时器,无需手动清理
async function fetchWithTimeout(url, timeoutMs = 5000) {
  const response = await fetch(url, {
    signal: AbortSignal.timeout(timeoutMs)
  });
  return response.json();
}

// ❌ 旧写法:手动管理 setTimeout(容易泄漏且代码臃肿)
async function fetchWithTimeoutOld(url, timeoutMs = 5000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await fetch(url, { signal: controller.signal });
    return await response.json();
  } finally {
    // 如果忘记 clearTimeout,timer 会阻止 GC 回收 controller
    clearTimeout(timer);
  }
}

超时信号的一个关键细节需要特别注意:AbortSignal.timeout() 创建的信号的 reason 类型是 TimeoutError,而不是 AbortError。这是因为它们代表不同的取消原因——超时是自动的系统行为,而 AbortError 通常是用户或代码主动触发的。在实际项目中,你可能需要向用户展示不同的错误信息:超时时说”请求超时,请检查网络”,手动取消时则完全不展示错误。

// AbortSignal.timeout 的错误类型是 TimeoutError,不是 AbortError
const signal = AbortSignal.timeout(100);
signal.addEventListener('abort', () => {
  console.log(signal.reason.name); // 'TimeoutError'
  console.log(signal.reason instanceof DOMException); // true
});

// 在 catch 中精确区分超时和其他取消,给用户不同反馈
async function robustFetch(url, timeoutMs = 5000) {
  try {
    return await fetch(url, {
      signal: AbortSignal.timeout(timeoutMs)
    });
  } catch (err) {
    if (err.name === 'TimeoutError') {
      // 超时:提示用户检查网络或稍后重试
      showToast('请求超时,服务端可能繁忙');
    } else if (err.name === 'AbortError') {
      // 手动取消:不展示错误,静默处理
      return null;
    } else {
      // 其他网络错误
      showToast('网络错误: ' + err.message);
    }
    throw err;
  }
}

模式三:AbortSignal.any() 信号组合

AbortSignal.any() 是一个强大的组合原语,它接收一个 AbortSignal 数组,返回一个新的组合信号——数组中任意一个信号触发时,组合信号也会立即触发。这解决了”我的操作需要同时响应多种取消原因”的需求,比如一个 API 请求可能因为用户手动取消、请求超时、或页面卸载而需要终止。

// 典型场景:用户手动取消 + 10秒超时,任意一个触发就取消
async function fetchCriticalData(url) {
  const userController = new AbortController();

  // 组合两个信号:用户取消 和 超时
  // 任意一个先触发,整个请求就会被取消
  const combinedSignal = AbortSignal.any([
    userController.signal,
    AbortSignal.timeout(10_000)
  ]);

  // 显示取消按钮供用户手动操作
  const cancelBtn = showCancelButton(() => {
    userController.abort('用户点击取消');
  });

  try {
    const response = await fetch(url, { signal: combinedSignal });
    return await response.json();
  } finally {
    cancelBtn.remove();
  }
}

信号组合的另一个实用模式是级联取消:当父操作被取消时,所有关联的子操作也应该被取消。这在复杂的多请求场景中非常常见,比如一个页面同时发起了用户信息、订单列表、推荐内容三个请求,用户离开页面时应该一次取消所有请求。

// 级联取消:一个页面的多个请求绑定同一个 controller
class PageRequestManager {
  #controller = null;

  // 页面初始化时创建 controller
  init() {
    this.#controller = new AbortController();
  }

  // 所有请求共享同一个 signal
  // controller.abort() 时,所有未完成的请求都会被取消
  async fetchUsers() {
    return fetch('/api/users', {
      signal: this.#controller.signal
    }).then(r => r.json());
  }

  async fetchOrders() {
    return fetch('/api/orders', {
      signal: this.#controller.signal
    }).then(r => r.json());
  }

  async fetchProducts() {
    return fetch('/api/products', {
      signal: this.#controller.signal
    }).then(r => r.json());
  }

  // 页面离开时一次取消所有未完成的请求
  destroy() {
    this.#controller?.abort('页面已卸载');
  }
}

// 使用示例:并行请求,统一取消
const pageRequests = new PageRequestManager();
pageRequests.init();

const [users, orders, products] = await Promise.all([
  pageRequests.fetchUsers(),
  pageRequests.fetchOrders(),
  pageRequests.fetchProducts(),
]);

// 页面卸载时清理所有请求
window.addEventListener('beforeunload', () => pageRequests.destroy());

⚠️ 警告: AbortSignal.any() 在 Node.js 20.3+ 和 Chrome 116+ 中可用。如果你的项目需要兼容更旧的环境,可以使用以下 polyfill:创建一个新 controller,遍历所有传入的 signal,为每个注册 abort 事件监听器,监听器中调用新 controller 的 abort() 方法。同时检查是否有 signal 已经处于 aborted 状态,如果是则立即取消。

模式四:React useEffect 清理中的 AbortController

React 的 useEffect 清理函数是 AbortController 在前端项目中最容易被忽视的使用场景。在 React 18 的 Strict Mode 下,开发环境中 effect 会执行两次(mount → unmount → mount),这是为了帮助开发者发现缺少清理函数的 bug。如果不正确处理 AbortController,第一次请求的结果会在 unmount 后到达并尝试更新已卸载的组件,导致”幽灵更新”——界面上显示了不属于当前状态的数据。

// ✅ 正确的 React 数据获取模式:每个 effect 都有对应的清理
import { useState, useEffect } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // 关键:每次 effect 执行都创建独立的 controller
    // 不要在组件级别创建,因为 cleanup 后 controller 已失效
    const controller = new AbortController();

    async function fetchUser() {
      setLoading(true);
      setError(null);

      try {
        const response = await fetch(`/api/users/${userId}`, {
          signal: controller.signal,
        });

        if (!response.ok) {
          throw new Error(`用户不存在: ${response.status}`);
        }

        const data = await response.json();

        // 双重保险:即使 catch 中过滤了 AbortError,
        // 这里再检查一次确保不会更新已卸载组件的状态
        if (!controller.signal.aborted) {
          setUser(data);
          setLoading(false);
        }
      } catch (err) {
        // AbortError 不是真正的错误,忽略它
        if (err.name === 'AbortError') return;

        if (!controller.signal.aborted) {
          setError(err.message);
          setLoading(false);
        }
      }
    }

    fetchUser();

    // 清理函数:组件卸载或 userId 变化时取消请求
    // reason 可以帮助你在调试时区分是哪种情况触发的取消
    return () => {
      controller.abort('组件卸载或 userId 变化');
    };
  }, [userId]);

  if (loading) return <div>加载中...</div>;
  if (error) return <div>错误: {error}</div>;
  if (!user) return null;

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

💡 提示: 在 React 18 的 Strict Mode 下,开发环境中 effect 会执行两次 mount → unmount → mount。这是故意的行为,用来暴露缺少清理的 bug。如果你看到 effect 执行了两次,不要觉得是 bug——恰恰相反,这是 React 在帮你发现问题。正确处理 AbortController 后,两次执行不会产生任何副作用。

模式五:可取消的轮询与指数退避

对于需要反复请求直到满足条件的场景(比如等待后台任务完成),结合指数退避(Exponential Backoff)的轮询非常常见。AbortController 让这种模式既能优雅退避,又能随时被外部取消。

// 可取消的指数退避轮询
async function pollWithBackoff(url, {
  signal,
  maxRetries = 5,
  baseDelay = 1000,
  maxDelay = 30000,
  onPoll = null
} = {}) {
  let retries = 0;

  while (retries < maxRetries) {
    // 每次轮询前检查是否已取消
    signal?.throwIfAborted();

    try {
      const response = await fetch(url, { signal });
      const data = await response.json();

      // 通知调用方当前轮询结果
      onPoll?.(data, retries);

      // 业务逻辑判断:是否需要继续轮询
      if (data.status === 'completed' || data.status === 'failed') {
        return data;
      }

      // 成功拿到响应但任务未完成,重置重试计数
      retries = 0;
    } catch (err) {
      // AbortError 直接上抛,不做重试
      if (err.name === 'AbortError') throw err;

      retries++;
      if (retries >= maxRetries) {
        throw new Error(`轮询失败,已重试 ${maxRetries} 次: ${err.message}`);
      }
    }

    // 指数退避等待,但依然响应取消信号
    const delay = Math.min(baseDelay * Math.pow(2, retries), maxDelay);
    // 加入随机抖动(jitter)避免多个客户端同时重试
    const jitteredDelay = delay * (0.5 + Math.random() * 0.5);
    await sleep(jitteredDelay, signal);
  }

  throw new Error(`轮询超过最大重试次数 ${maxRetries}`);
}

// 支持取消的 sleep 工具函数
// 这是 AbortController 最实用的辅助函数之一
function sleep(ms, signal) {
  return new Promise((resolve, reject) => {
    // 如果 signal 已经取消,立即 reject
    if (signal?.aborted) {
      reject(signal.reason);
      return;
    }

    const timer = setTimeout(resolve, ms);

    // 在 abort 时清理 timer 并 reject
    signal?.addEventListener('abort', () => {
      clearTimeout(timer);
      reject(signal.reason);
    }, { once: true }); // once: true 防止监听器泄漏
  });
}

// 使用示例:等待后台任务完成
const controller = new AbortController();

pollWithBackoff('/api/jobs/abc123', {
  signal: controller.signal,
  maxRetries: 10,
  baseDelay: 2000,
  onPoll: (data, retries) => {
    updateProgressUI(data.progress, retries);
  }
})
  .then(data => showCompletionNotice(data))
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('轮询已取消');
    } else {
      showError(err.message);
    }
  });

// 用户可以随时取消
document.getElementById('cancel-btn').onclick = () => {
  controller.abort('用户取消了任务等待');
};

模式六:自定义可取消的异步操作

AbortController 的价值远不止于 fetch。你可以把它集成到任何异步操作中——文件处理、Web Worker 通信、IndexedDB 事务、甚至自定义的动画和定时任务。关键是让操作在每个可中断的检查点(checkpoint)都主动检查 signal 的状态。

// 将 AbortSignal 集成到大文件处理中
// 每处理一个分片都检查取消状态,实现细粒度的取消控制
async function processLargeFile(file, { signal, chunkSize = 1024 * 1024 } = {}) {
  const totalChunks = Math.ceil(file.size / chunkSize);
  const results = [];

  for (let i = 0; i < totalChunks; i++) {
    // 每处理一个 chunk 前检查取消状态
    // throwIfAborted() 在已取消时直接抛出异常,中断循环
    signal?.throwIfAborted();

    const start = i * chunkSize;
    const end = Math.min(start + chunkSize, file.size);
    const chunk = file.slice(start, end);

    const text = await chunk.text();
    const processed = await processChunk(text);

    results.push(processed);

    // 通过自定义事件报告进度
    // 这比传入回调函数更灵活,因为 signal 本身就是 EventTarget
    signal?.dispatchEvent(
      new CustomEvent('progress', {
        detail: {
          current: i + 1,
          total: totalChunks,
          percent: Math.round((i + 1) / totalChunks * 100)
        }
      })
    );
  }

  return results;
}

// 使用示例:处理大文件,支持取消和进度追踪
const controller = new AbortController();

// 监听自定义进度事件
controller.signal.addEventListener('progress', (e) => {
  const { current, total, percent } = e.detail;
  updateProgressBar(percent);
  setStatusText(`处理中: ${current}/${total} 分片`);
});

try {
  const results = await processLargeFile(bigFile, {
    signal: controller.signal,
    chunkSize: 512 * 1024
  });
  showCompletionNotice(`处理完成: ${results.length} 个分片`);
} catch (err) {
  if (err.name === 'AbortError') {
    showInfo('文件处理已取消');
  } else {
    showError('处理失败: ' + err.message);
  }
}

// 用户点击取消按钮
cancelButton.onclick = () => controller.abort('用户取消文件处理');

📊 三、性能影响与最佳实践

AbortController 的内存开销分析

AbortController 的设计非常轻量,但理解它的内存行为有助于避免在高频场景中出现问题。每次创建 AbortController 都会分配一个小型对象(controller 本身 + 关联的 signal + 事件监听器列表),在正常使用中这些对象会在操作完成后被垃圾回收。问题通常出在闭包上——如果你在 signal 的监听器中捕获了大量外部引用,这些引用会阻止 GC 回收相关对象。

场景内存占用GC 行为说明
1000 个 AbortController(已 abort)~200 KB下次 GC 自动回收每个约 200 字节,非常轻量
1000 个未清理的 signal 监听器~800 KB无法回收监听器中的闭包引用阻止 GC
正常使用:创建 → 使用 → GC可忽略自动回收无泄漏风险
React useEffect 正确清理无额外开销cleanup 触发时释放controller 被闭包持有但可回收

以下是避免内存问题的关键实践:

// ❌ 错误:缓存已失效的 controller,后续操作全部静默失败
const cachedController = new AbortController();
// 第一次 abort 后,这个 controller 永久失效
// 后续 fetch 都会立即收到已取消的 signal,直接 reject

// ✅ 正确:每次操作都创建新 controller
function createCancellableOperation() {
  let controller = null;

  return {
    // 执行操作前自动取消旧操作
    execute(url, options = {}) {
      controller?.abort('新操作取代旧操作');
      controller = new AbortController();
      return fetch(url, {
        ...options,
        signal: controller.signal
      });
    },
    // 手动取消当前操作
    cancel(reason = '手动取消') {
      controller?.abort(reason);
    },
    // 获取当前 signal(用于组合)
    get signal() {
      return controller?.signal;
    }
  };
}

与 Go context.Context 的对比

如果你同时做前后端开发,理解 AbortController 和 Go 的 context.Context 的异同非常有价值。两者都解决”异步操作的取消传播”问题,但设计理念有明显差异。Go 的 context 天然支持父子级联——子 context 自动继承父 context 的取消信号,而 AbortController 需要通过 AbortSignal.any() 手动组合。但 AbortController 在浏览器生态中的集成度极高,几乎所有原生异步 API(fetch、ReadableStream、WebSocket 等)都原生支持它。

特性AbortController (JavaScript)context.Context (Go)
取消传播单向,Signal 只读传播单向,通过 WithCancel 派生
超时支持AbortSignal.timeout(ms)context.WithTimeout(parent, dur)
截止时间无原生 deadline APIcontext.WithDeadline(parent, t)
值传递❌ 不支持✅ context.WithValue(parent, key, val)
信号组合AbortSignal.any(signals)手动 WithCancel + 关闭监听
级联取消需手动 any() 组合天然父子级联,自动传播
标准库集成fetch, ReadableStream, Cache API 等net/http, database/sql, grpc 等
取消原因任意类型(reason 参数)仅 error 类型(context.Canceled)

⚡ 关键结论: 在全栈项目中,建议在 API 层统一使用类似的取消模式——前端用 AbortController 管理所有异步操作的生命周期,后端用 context.Context 通过请求链传播取消。两者通过 HTTP 头部(如客户端中断连接时服务端感知到 context.Done())自然关联。不要试图用一套方案统一前后端,它们各自在自己的生态中有最佳集成。

✅ 四、总结与行动建议

AbortController 不是什么新技术,但它被严重低估了。在 2026 年的前端开发中,异步取消已经不是”最佳实践”,而是基本功。以下是我建议每个项目立即采用的三个实践:

第一,每个 fetch 都传 signal。 即使当前不需要取消功能,也为未来的可维护性铺路。你可以在应用层定义一个默认的全局 signal,页面卸载时自动取消所有未完成的请求。这样做几乎零成本,但能在用户快速切换页面时避免大量无效请求冲击服务器。

第二,React 组件中 useEffect 发请求必须有清理。 每个发起异步操作的 useEffect 都必须在清理函数中调用 controller.abort()。这不是可选的——在 React 18 Strict Mode 下,缺少清理会导致开发环境中的状态混乱。

第三,搜索和筛选场景必须处理竞态。 凡是”用户输入 → 发请求 → 渲染结果”的流程链,都必须在新请求前取消旧请求。这是用户体验的基本保障。

相关工具与资源推荐:

📚 相关文章