知

模块四 · 协作与表达

第 12 / 14 节18 分钟阅读型

测试与调试:把问题缩小到一行

调试能力决定了修复速度。这一节讲清断点与网络面板的正确用法、错误监控的三层捕获、最小单测的写法,以及"先复现再二分"的排查顺序。

读完这一节,你能做到

  1. 用条件断点与日志点替代满屏 console.log。
  2. 从 Network 与 Performance 面板判断问题属于哪一层。
  3. 用 try/catch、onerror、unhandledrejection 三层捕获线上错误。
  4. 用 Vitest 给纯函数写可维护的单测。

一、调试的第一原则:先复现

没有稳定复现步骤的 bug,修好了也不知道是不是真的修好。复现要记录四件事:

要素例子
环境Chrome 120 / iOS Safari / 特定账号
步骤打开列表 → 切到第 3 页 → 删除最后一条
现象列表变空,但总数没变
期望回退到第 2 页并刷新

能稳定复现后,再按"由外到内"缩小范围:

页面整体 → 哪个组件/模块 → 哪个函数 → 哪一行

二分法是缩小范围最有效的手段:注释掉一半逻辑、在中间打日志、用 git bisect 找引入问题的提交。

二、断点比 console 强在哪

手段优点局限
console.log随手可用要改代码、会刷屏、异步时序难看清
普通断点能看调用栈与作用域循环里会停很多次
条件断点只在满足条件时停条件表达式写错会一直不停
日志点(Logpoint)不暂停、不改代码、能打表达式不能交互修改变量
debugger 语句精确定位忘删会阻塞用户

三个实用技巧:

  1. 条件断点:在循环里写 item.id === "123",只在目标数据上停。
  2. 日志点:右键行号选 "Add logpoint",输入 JSON.stringify(state),页面不中断。
  3. 异步调用栈:Sources 面板勾选 Async,能看到 await 之前是谁调用的。

三、三类问题,三个面板

现象先看关键信息
接口 404/500Network请求 URL、状态码、响应体、请求头
页面白屏/报错Console第一条红色错误,不是最后一条
交互卡顿Performance长任务、强制同步布局、主线程占用
内存持续上涨Memory堆快照对比、Detached DOM
样式不对Elements → Computed最终生效的规则与覆盖来源

Network 面板要看四个字段:状态码、耗时(TTFB/内容下载)、尺寸(是否命中缓存)、响应体。很多"接口慢"其实是前端在等锁、重复请求或串行瀑布。

Console 的第一条错误往往才是根因,后面的错误多是它的连锁反应。先修第一条。

四、线上错误怎么捕获

// 1. 同步运行时错误
window.addEventListener("error", (e) => {
  report({
    type: "error",
    message: e.message,
    stack: e.error?.stack,
    file: e.filename,
    line: e.lineno,
  });
});

// 2. 未处理的 Promise 拒绝
window.addEventListener("unhandledrejection", (e) => {
  report({
    type: "unhandledrejection",
    reason: String(e.reason),
    stack: e.reason?.stack,
  });
});

// 3. 资源加载失败(img/script/link)
window.addEventListener(
  "error",
  (e) => {
    const el = e.target;
    if (el instanceof HTMLImageElement || el instanceof HTMLScriptElement) {
      report({ type: "resource", tag: el.tagName, src: el.src });
    }
  },
  true, // 资源错误不冒泡,必须用捕获
);

三个细节:

  • 资源加载错误不冒泡,必须用捕获阶段监听,这是最常见的遗漏。
  • 跨域脚本的错误信息会被浏览器脱敏成 "Script error.",需要脚本响应头带 crossorigin 且服务端允许 CORS。
  • 上报本身要有兜底:sendBeacon 在页面卸载时更可靠;上报失败不能影响业务。

线上堆栈是压缩后的,需要 Source Map 还原。Source Map 不要公开部署,上传到监控平台,或放在需要鉴权的路径。

五、最小单测:只测纯逻辑

import { describe, it, expect } from "vitest";
import { formatAmount } from "@/utils/format";

describe("formatAmount", () => {
  it("保留两位小数并加千分位", () => {
    expect(formatAmount(1234.5)).toBe("1,234.50");
  });

  it("处理 0 与负数", () => {
    expect(formatAmount(0)).toBe("0.00");
    expect(formatAmount(-12)).toBe("-12.00");
  });

  it("非法输入返回占位符", () => {
    expect(formatAmount(Number.NaN)).toBe("--");
  });
});

写测试的顺序建议:

  1. 先写正常路径。
  2. 再写边界:0、空、负数、超长。
  3. 最后写异常:NaN、null、接口缺字段。

三条原则:

  • 测行为不测实现:断言输出,不断言内部调用了几次。
  • 一个用例一个断言主题:失败时能直接看出问题。
  • 测试名描述业务规则:"保留两位小数并加千分位"比"test1"有用。

涉及定时器要用假时钟:

import { vi } from "vitest";
vi.useFakeTimers();
debounced("a");
vi.advanceTimersByTime(300);
expect(handler).toHaveBeenCalledWith("a");
vi.useRealTimers();

六、调试顺序:一张排查表

步骤做什么产出
1稳定复现步骤、环境、频率
2确认现象属于哪一层网络 / 渲染 / 逻辑 / 数据
3缩小范围二分、条件断点、日志点
4提出假设并验证一次只改一个变量
5修复并回归补一条测试或写清验证步骤

最常见的错误是跳过第 2 步,直接改代码。改了三处之后问题消失,却不知道是哪一处起的作用,下次还会复现。

七、常见坑

  1. 满屏 console.log,异步日志顺序看错,得出错误结论。
  2. 只看最后一条错误,忽略第一条根因。
  3. 资源 404 用冒泡监听,永远收不到。
  4. 线上只上报 message 不上报 stack,无法定位。
  5. Source Map 公开部署,源码泄露。
  6. 测试断言内部函数调用次数,重构就红。
  7. 用 setTimeout 等异步,CI 上随机失败。
  8. 修完不补测试,同样的问题下次换个入口再来一遍。

八、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 写一个循环 1000 次的函数,用条件断点只停在 id === "500" 那次,并打出当时的完整状态。
  2. 给页面加三层错误监听,故意触发资源 404 与未处理 Promise,确认都能上报。
  3. 给两个工具函数写单测,覆盖正常、边界、异常三类输入。
  4. 用 Performance 录制一次滚动,找出一个长任务并说明优化方向。

② 让 AI 写同一题

把第 2 题(定位一个偶发 bug)交给 AI,补一句 先给复现与缩小的步骤,再给修法。

③ 对比要盯的点

  • 它是不是直接给了“可能的修复”,跳过了复现?
  • 定位手段它是二分、条件断点,还是加一堆 console.log?
  • 它有没有区分“复现了”和“猜想是对的”?

④ 一句话取舍

它给的修法,你能设计一个用例证明 bug 真的没了吗?

九、自测

  1. 条件断点与日志点分别适合什么场景?
  2. 资源加载错误为什么必须用捕获阶段?
  3. 为什么只看最后一条控制台错误是错的?
  4. 跨域脚本错误为什么变成 "Script error."?
  5. 单测应该断言什么,不该断言什么?

参考答案要点:循环内精确暂停 / 不中断打日志;资源错误不冒泡;第一条往往是根因;浏览器脱敏;断言输出行为,不断言实现细节。

附:Performance 面板的三个信号

录制一次交互后,火焰图信息量很大,但只需盯三个信号就能判断问题类型:

信号表现说明优化方向
长任务超过 50ms 的黄色块主线程被占住,输入无法响应拆分任务、减少同步计算、用 Worker
强制同步布局紫色 Layout 块带警告三角JS 读布局后又写样式,反复触发先读后写,批量修改
大量重绘/合成绿色 Paint 块密集频繁改颜色、阴影、透明度用 transform/opacity,减少绘制面积

三者的排查顺序不同:

  • 输入卡顿先看长任务。如果长任务里是业务计算,考虑分片;如果是一次大循环渲染,考虑虚拟列表。
  • 滚动卡顿先看布局与绘制。列表项用了 box-shadow 或 filter,滚动时每帧都在重绘。
  • 动画卡顿先看是否动了布局属性。改 top、height 一定比 transform 慢。

两个实用技巧:

  1. Performance 面板的 “Bottom-Up” 视图按耗时排序,能直接看到哪个函数最贵,比在火焰图里找更省时间。
  2. 录制时勾选 “Screenshots”,能对应看到每一帧的画面,判断掉帧发生在哪个交互阶段。

最后一条经验:先确认瓶颈在渲染层还是网络层。包体从 2MB 降到 1MB 但首屏没变快,说明瓶颈在网络延迟或服务端响应,继续减包没有意义。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解