实际项目中的异步问题排查

通过真实案例分析 JavaScript 异步编程中的常见问题:竞态条件、内存泄漏、回调地狱,以及如何用正确的方式排查和解决。


案例一:React useEffect 中的竞态条件

问题

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  return <div>{user?.name}</div>;
}

userId 快速变化时,可能会出现"后发送的请求先返回"的问题。

原因

请求 A(userId=1)发送,然后请求 B(userId=2)发送。但 B 先返回,A 后返回。最终显示的是用户 1 的数据。

解决

使用 AbortController 取消旧请求,或使用 React Query / SWR 等库:

useEffect(() => {
  const controller = new AbortController();

  fetchUser(userId, { signal: controller.signal })
    .then(setUser)
    .catch(err => {
      if (err.name !== 'AbortError') throw err;
    });

  return () => controller.abort();
}, [userId]);

案例二:未清理的定时器

问题

组件卸载后定时器仍在运行,导致内存泄漏:

function ChatRoom() {
  useEffect(() => {
    const timer = setInterval(() => {
      fetchNewMessages();
    }, 5000);

    // 忘记清理
  }, []);

  return <div>Chat Room</div>;
}

解决

useEffect(() => {
  const timer = setInterval(() => {
    fetchNewMessages();
  }, 5000);

  return () => clearInterval(timer);
}, []);

案例三:async/await 错误处理

问题

async 函数中的错误被静默吞掉:

async function loadData() {
  await fetchData(); // 如果这里出错,什么都不会发生
  setDataLoaded(true);
}

解决

始终处理可能的错误:

async function loadData() {
  try {
    await fetchData();
    setDataLoaded(true);
  } catch (err) {
    setError(err.message);
    console.error('Failed to load data:', err);
  }
}

案例四:循环中的异步操作

问题

串行执行本应并行的操作:

async function processItems(items) {
  for (const item of items) {
    await processItem(item); // 串行:每个处理完才处理下一个
  }
}

解决

async function processItems(items) {
  await Promise.all(items.map(item => processItem(item)));
}

案例五:Vue 的 nextTick 陷阱

问题

在 Vue 中修改数据后立即读取 DOM,发现是旧值:

vm.message = 'new value';
console.log(vm.$el.textContent); // 仍然是旧值

解决

使用 Vue.nextTickawait vm.$nextTick()

vm.message = 'new value';

await vm.$nextTick();
console.log(vm.$el.textContent); // 'new value'

排查工具

Chrome DevTools

  1. Performance 面板:录制页面操作,分析任务分布和耗时
  2. Console:查看 Promise rejection 警告
  3. Network:分析请求时序

Node.js

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection:', reason);
});

这一章想说的

异步问题的根本原因通常只有一个:代码执行顺序和开发者预期不一致

常见的解决思路:

  1. 竞态条件:用 AbortController 或请求 ID
  2. 内存泄漏:在 cleanup 函数中清理定时器和订阅
  3. 错误处理:每个 async 操作都要处理 rejection
  4. 性能问题:没有依赖的异步操作要并行,不要串行

写好异步代码的关键是始终假设任何异步操作都可能失败或延迟,然后针对这些情况做处理。


延展阅读