案例一: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.nextTick 或 await vm.$nextTick():
vm.message = 'new value';
await vm.$nextTick();
console.log(vm.$el.textContent); // 'new value'
排查工具
Chrome DevTools
- Performance 面板:录制页面操作,分析任务分布和耗时
- Console:查看
Promise rejection警告 - Network:分析请求时序
Node.js
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
});
这一章想说的
异步问题的根本原因通常只有一个:代码执行顺序和开发者预期不一致。
常见的解决思路:
- 竞态条件:用 AbortController 或请求 ID
- 内存泄漏:在 cleanup 函数中清理定时器和订阅
- 错误处理:每个 async 操作都要处理 rejection
- 性能问题:没有依赖的异步操作要并行,不要串行
写好异步代码的关键是始终假设任何异步操作都可能失败或延迟,然后针对这些情况做处理。