调试
调试的关键是缩小问题空间:把“看起来奇怪的结果”还原为“可验证的假设”。
当你感觉卡住时,通常是因为变量同时在变化。你的任务是冻结变量,直到 bug 变得可重复、可验证。
可复用的排查流程
- 稳定复现。
- 冻结输入,缩到最小失败单元。
- 一次只验证一个假设。
这听起来很慢,但其实是最快的“真修复”路径。调试本质是搜索问题:通过约束把搜索空间缩小。
常见 bug 来源
- 闭包过期:异步回调里读到旧的 `state`/`props`。
- `Effect` 依赖不正确:漏依赖,或把过多事情塞进一个 `Effect`。
- 竞态:多个请求同时更新同一份 `state`,写入顺序颠倒。
- 以为 `setState` 立刻生效:`state` 对当前渲染是“快照”。
React 严格模式提示
开发模式下,React 可能会让 `Effect` 执行两次(`mount` → `cleanup` → 再 `mount`),用于帮助你发现缺失的清理逻辑。如果 bug 只在 dev 发生,先检查清理函数与幂等性。
一个可靠的经验法则是:如果你的 effect “启动了某个东西”(定时器/订阅/请求),就必须在 cleanup 里“停止它”。
追踪状态与副作用
TypeScript
type Event =
| { type: 'input'; value: string }
| { type: 'request-start' }
| { type: 'request-done'; ok: boolean };
export function trace(e: Event) {
const time = new Date().toISOString();
console.log(time, e.type, e);
}让日志可行动
优先结构化日志,避免散落的 `console.log`。带上关联 id 与耗时,才能重建时间线。
TypeScript
type TraceMeta = { scope: string; id?: string };
export function createTracer(meta: TraceMeta) {
return {
info(event: string, data?: Record<string, unknown>) {
console.log(new Date().toISOString(), meta.scope, meta.id ?? '-', event, data ?? {});
},
time<T>(event: string, run: () => T) {
const start = performance.now();
const result = run();
const durationMs = Math.round(performance.now() - start);
console.log(new Date().toISOString(), meta.scope, meta.id ?? '-', event, { durationMs });
return result;
},
};
}隔离与二分定位
当你无法对全系统推理时,别硬扛:把未知替换成常量,按二分法删掉一半代码,直到行为可解释。
- 一次砍掉一半组件树或逻辑分支,看问题是否仍存在。
- 冻结输入:固定 props/state/网络响应,让行为可重复。
- 把“时序 bug”变成“数据 bug”:加入 `abort`、request id,并忽略过期结果。
上线检查清单
- 问题有最小复现与明确步骤。
- 输入/输出日志具备关联 id。
- `Effect` 幂等且清理正确。
- 异步流程已处理竞态(`abort`/忽略过期)。