到这里,Sources 面板系列已经走完了十章。我们从最基础的面板概览开始,一路学到了断点、单步执行、debugger 命令、日志点、条件断点、代码段、Workspace、性能分析和黑盒脚本。

知识点很多,但你可能会有一种感觉:每个技能单独学的时候都懂,但真正遇到问题时,不知道该从哪里下手。

这很正常。调试不是“按顺序使用工具”,而是“根据问题选择工具”。就像一个木匠,工具箱里有锤子、锯子、刨子,但做一把椅子时,他不会把所有工具都用一遍——他只会用当下最合适的那一件。

这一章,我们用一个完整的实战案例,把前面十章的技能串起来。你会看到一个真实的调试场景:一个待办事项应用,有逻辑错误、有性能问题、有第三方库干扰。我们会一步步排查,每一步都用到不同的 Sources 面板技能。

小提示:这一章是“综合演练”,建议你打开代码编辑器,把下面的示例代码复制进去,跟着文章一步步操作。亲手走完整个流程,你才能真正把知识变成能力。

一、系列知识回顾

在开始实战之前,先用一张表快速回顾一下 Sources 面板的核心技能:

章节

技能

一句话说明

第 1 章

Sources 面板概览

三大区域:文件导航、代码编辑器、调试区

第 2 章

打开 Sources 面板

命令菜单 Ctrl/Cmd + Shift + P → sources

第 3 章

Page 标签页

树形浏览所有已加载资源

第 4 章

编辑 CSS 与 JS

CSS 改完即生效,JS 需保存并刷新

第 5 章

断点

行断点、条件断点、日志点、异常断点等

第 6 章

调试器控制

F8 继续、F10 跳过、F11 进入、Shift+F11 跳出

第 7 章

debugger 与高级断点

代码中埋点、日志点、条件断点深入用法

第 8 章

代码段(Snippets)

可复用的调试脚本,随时运行

第 9 章

Workspace

让 Sources 面板中的修改持久化到本地文件

第 10 章

性能分析与黑盒脚本

从 Performance 跳转到 Sources,忽略第三方库

现在,让我们把这些技能放进一个真实的场景里。

二、实战场景:一个有问题的待办应用

我们准备了一个简单的待办事项应用。它有三个问题:

  1. 计数错误:添加任务并切换完成状态后,底部的“总计”显示的是字符串拼接的结果,比如 总计: 1 + 1 = 11,而不是正确的→总计: 1 项,已完成: 1 项。

  2. 标记错误:当任务数量较多时,点击某个任务的复选框,有时会切换错误的那个任务。

  3. 性能卡顿:当任务数量较多时,页面操作明显卡顿。

示例目录结构
Demo:
└─app.js
└─index.html
└─style.css
└─vendor.js

下面是示例的具体代码:

index.html

<!DOCTYPE html>
<html lang="zh-CN">
    <head>
        <meta charset="UTF-8">
        <title>待办事项</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="./style.css">
    </head>
    <body>
        <h1>待办事项</h1>
        <label for="taskInput">
            <input id="taskInput" type="text" placeholder="输入新任务...">
        </label>
        <button id="addBtn">添加</button>
        <ul id="taskList"></ul>
        <p id="summary">总计: 0 项,已完成: 0 项</p>
        <script src="vendor.js"></script>
        <script src="./app.js"></script>
    </body>
</html>

style.css

body {font-family: sans-serif;max-width: 600px;margin: 40px auto;padding: 0 20px;}
#taskList {list-style: none;padding: 0;}
#taskList li {display: flex;align-items: center;padding: 8px 0;border-bottom: 1px solid #eee;}
#taskList li span {flex: 1;margin-left: 10px;}
#taskList li button {background: #1a73e8;color: white;border: none;padding: 4px 12px;border-radius: 4px;cursor: pointer;}
#summary {color: #666;font-size: 14px;}

vendor.js(模拟第三方库,故意包含低效代码)

// 模拟一个第三方工具库
window.VendorUtils = {
    // 这个函数故意写得低效,用于演示性能问题
    formatTaskText: function(task) {
        var result = '';
        // 不必要的循环和字符串拼接
        for (var i = 0; i < 100000; i++) {
            result += task.text.charAt(i % task.text.length);
        }
        // 再做一些无意义的操作
        var temp = result.split('').reverse().join('');
        return task.text + ' (' + temp.length + ')';
    }
};

app.js

const taskInput = document.getElementById('taskInput');
const addBtn = document.getElementById('addBtn');
const taskList = document.getElementById('taskList');
const summary = document.getElementById('summary');

let tasks = [];

function addTask() {
    const text = taskInput.value.trim();
    if (!text) return;

    const task = {
        id: Date.now(),
        text: text,
        completed: false
    };
    tasks.push(task);
    taskInput.value = '';
    render();
}

function toggleTask(id) {
    // const task = tasks.find(t => t.id === Number(id));
    const task = tasks.find(t => t.id === id);
    if (task) {
        task.completed = !task.completed;
        render();
    }
}

function render() {
    taskList.innerHTML = '';

    tasks.forEach(task => {
        // logpoint位置
        const li = document.createElement('li');

        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.checked = task.completed;
        // 埋坑:task.id + ''
        checkbox.addEventListener('change', () => toggleTask(task.id + ''));
        // checkbox.addEventListener('change', () => toggleTask(task.id));

        const span = document.createElement('span');
        // span.textContent = task.text;

        // 调用第三方库的低效函数,导致性能问题
        span.textContent = VendorUtils.formatTaskText(task);

        li.appendChild(checkbox);
        li.appendChild(span);
        taskList.appendChild(li);
    });

    const total = tasks.length;
    const completed = tasks.filter(t => t.completed).length;

    // BUG 1: 这里使用了字符串拼接,而不是数字相加
    summary.textContent = '总计: ' + total + ' + ' + completed + ' = ' + (String(total) + String(completed));

    // summary.textContent = '总计: ' + total + ' 项,已完成: ' + completed + ' 项';
}

addBtn.addEventListener('click', addTask);
taskInput.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') addTask();
});

交互说明:每个任务项前面有一个复选框,点击复选框即可切换该任务的完成状态——这就是 toggleTask 函数被触发的入口。

三、完整调试流程

在执行以下具体操作之前,请先在项目根目录(demo)下通过终端运行命令 npx serve . 启动本地测试服务器,以便于后续操作。选择 localhost 访问。

步骤 1:打开 Sources 面板,定位问题文件

在 Chrome 中访问网址:http://localhost:3000/,按 F12 打开 DevTools。

用命令菜单打开 Sources 面板:

  • 按 Ctrl/Cmd + Shift + P。

  • 输入 sources。

  • 选择 Show Sources panel,按 Enter。

在 Page 标签页中,展开文件树,找到 app.js,点击打开它。

步骤 2:用行断点定位计数错误

在 app.js 中,找到 summary.textContent 这一行,点击行号设置一个行断点。

回到页面,在输入框中输入“学习 DevTools”,点击“添加”按钮或按下”Enter”键。代码会在 render 函数的断点处暂停。

查看右侧的 Scope 面板:

  • total 的值是 1(数字)。

  • completed 的值是 0(数字)。

按 F8 继续执行。现在页面上显示了一条任务,但底部“总计”显示的是 总计: 1 + 0 = 10。问题已经暴露。

再次触发断点,观察完成状态:

点击任务前的复选框,将任务标记为已完成。代码会再次在断点处暂停。

查看 Scope:

  • total 的值是 1(数字)。

  • completed 的值是 1(数字)。

按 F8 继续执行。底部显示 总计: 1 + 1 = 11。

问题确认:String(total) + String(completed) 把两个数字转成了字符串拼接,而不是做加法。

步骤 3:用单步执行验证修复方案

再次点击任务前的复选框,在断点暂停的状态下,按 F10(单步跳过)执行 summary.textContent 这一行。

再次查看右侧的 Scope 面板:

  • total 的值是 1(数字)。

  • completed 的值是 0(数字)。

在控制台(按Esc键打开底部抽屉→ Console)中输入 Number(total) + Number(completed),结果是 1。确认了修复方向——应该用数字相加,而不是字符串拼接。

步骤 4:用条件断点排查标记错误

你发现当任务数量较多时,点击某个任务的复选框,有时会切换错误的那个任务。怀疑是 toggleTask 函数中的 find 逻辑有问题,或者事件监听器中的 task 引用出了问题。

在右侧 Breakpoints 面板中,找到 app.js 下的 summary.textContent 选项,取消勾选以临时禁用该断点。然后刷新页面,在 toggleTask 函数的第一行设置一个条件断点,条件是:

tasks.length > 2

这样只有当任务数量大于 2 时才会暂停。

添加三个任务(比如“任务 A”、“任务 B”、“任务 C”),然后点击“任务 B”的复选框。代码在断点处暂停。

检查 Scope:

  • id (Scope → Local → id)的值是 Date.now() 生成的时间戳。

  • task (Scope → Local → task)的值是<value unavailable>(留意这个值)。

  • tasks数组中的每个 id (Scope → Script → tasks)也是时间戳。

按 F10 单步执行 const task = tasks.find(t => t.id === id);,观察 task 的值。结果是 undefined,因为 === 比较时字符串和数字不相等。

问题找到了:类型不匹配。

修复方案:将 id 转换为数字,或者使用 == 比较。推荐使用 Number(id):

function toggleTask(id) {
    const task = tasks.find(t => t.id === Number(id));
    // ...
}

随后刷新页面,重复步骤 5中添加三个任务的操作。完成后,勾选"任务 B"对应的复选框,并按下 F10 键。此时观察右侧的 Scope 面板,可以看到 task 的值已从 <value unavailable> 变为一个包含 text: '任务 B' 属性的对象。再次按下 F10 键,程序将进入 task 的真值判断分支。至此,类型不匹配问题得到修复。

步骤 5:用日志点观察循环中的变量变化

在 render 函数的 tasks.forEach 循环中,设置一个日志点,输出:

`任务 ${task.id}: ${task.text},完成状态: ${task.completed}`

临时禁用条件断点后,刷新页面,添加几个任务,再点击复选框切换完成状态。观察控制台输出。每次 render 被调用时,控制台都会输出所有任务的状态。你可以快速确认哪些任务的 completed 状态不符合预期。

日志点的好处是:代码不会暂停,你可以在页面正常操作的同时,在控制台看到每一次渲染的详细数据。

步骤 6:用 debugger 命令快速埋点

在 toggleTask 函数中添加一行 debugger;:

function toggleTask(id) {
    debugger;  // 临时埋点
    const task = tasks.find(t => t.id === Number(id));
    if (task) {
        task.completed = !task.completed;
        render();
    }
}

临时禁用日志断点后,刷新页面,添加一个或多个任务,然后勾选任意任务前的复选框。代码执行将在 debugger; 语句处暂停。此时,检查 id 变量与 tasks 数组,确认该 id 是否已存在于数组中。

调试完成后,删除 debugger;。保存并刷新页面。

步骤 7:用代码段做辅助测试

按下 Ctrl/Cmd + Shift + P 打开命令面板,输入 snippet 并选择 Create New Snippet,创建一个代码片段,用于快速添加测试数据。

// 快速添加 10 个测试任务
for (let i = 0; i < 10; i++) {
    tasks.push({
        id: Date.now() + i,
        text: '测试任务 ' + (i + 1),
        completed: i % 2 === 0
    });
}
render();
console.log('已添加 10 个测试任务');

将其保存为 AddTestTasks。随后按下 Ctrl/Cmd + O,输入 !AddTestTasks 并按回车键,即可观察添加多任务场景下的运行表现。

步骤 8:用 Workspace 持久化修复

配置 Workspace,将项目文件夹映射到 DevTools。然后在 Sources 面板中直接修改 app.js:

把:

summary.textContent = '总计: ' + total + ' + ' + completed + ' = ' + (String(total) + String(completed));

改成:

summary.textContent = '总计: ' + total + ' 项,已完成: ' + completed + ' 项';

按 Ctrl/Cmd + S 保存。修改自动写入本地文件。

步骤 9:用性能分析和黑盒脚本优化卡顿

打开 Performance 面板,录制一段操作(添加多个任务、反复切换完成状态)。停止录制后,在火焰图中找到耗时最长的函数。

在 Chrome 152 以上的版本中,推荐使用 Bottom-Up 或 Call Tree 视图来定位:

  1. 切换到 Bottom-Up 标签页。

  2. 按 Self Time 列排序,排在第一位的很可能是 VendorUtils.formatTaskText——因为它在每次渲染时都被调用,并且内部有大量低效操作。

  3. 点击该函数名旁边的链接,DevTools 会自动跳转到 Sources 面板并高亮 vendor.js 中的对应代码。

你看到 formatTaskText 内部有一个 10000 次的循环和无意义的字符串操作,这就是性能瓶颈。

操作截图:Bottom-Up

点击 vendor.js跳转后

优化方案:这个第三方库函数实际上不需要做这些复杂操作。你可以在 app.js 中直接使用 task.text,而不调用 VendorUtils.formatTaskText。

但在修改之前,为了在调试时不被 vendor.js 的内部代码干扰,可以把它加入黑盒:

  • 在 Sources 面板中,右键点击 vendor.js 文件,选择 Add script to ignore list。

  • 之后单步执行时,调试器会自动跳过 vendor.js 的内部实现。

然后修改 app.js:

// 不再调用低效的第三方函数
span.textContent = task.text;

按 Ctrl/Cmd + S 保存。

步骤 10:验证修复

刷新页面,切换至 Performance 面板,录制一段操作(添加3个任务、勾选1个完成状态)。确认:

  • 计数显示正确:总计: 3 项,已完成: 1 项。

  • 任务标记正常,点击哪个就切换哪个。

  • 页面操作流畅,没有明显卡顿。

所有问题解决,修复前后对比:

四、调试流程总结

回顾整个实战过程,我们用到了以下技能:

步骤

用到的技能

章节

1

命令菜单打开 Sources 面板

第 2 章

2

Page 标签页定位文件、行断点

第 3、5 章

3

单步执行、Scope 检查

第 6 章

4

条件断点

第 5、7 章

5

日志点

第 5、7 章

6

debugger 命令

第 7 章

7

代码段

第 8 章

8

Workspace 持久化

第 9 章

9

性能分析、黑盒脚本

第 10 章

10

综合验证

全部

这个流程不是固定的——实际调试中,你可能会跳过某些步骤,或者反复使用某个技能。但核心思路是一样的:先定位问题,再分析原因,然后验证修复,最后持久化改动。

五、最佳实践清单

基于整个系列的内容,这里整理一份 Sources 面板调试的最佳实践清单,供你在日常开发中参考:

  1. 优先使用命令菜单:Ctrl/Cmd + Shift + P → sources,比鼠标点击快得多。

  2. 善用条件断点和日志点:不要在循环里设置普通断点,也不要在代码里塞 console.log。

  3. 用 debugger; 快速埋点:但记得提交前删除。

  4. 把常用脚本保存为代码段:一次编写,随时复用。

  5. 配置 Workspace:让浏览器中的修改持久化到本地文件。

  6. 把第三方库加入黑盒:让调试器专注在你的业务代码上。

  7. 性能问题先录 Performance:从 Bottom-Up 或 Call Tree 视图定位具体代码。

  8. 调试完记得清理:删除 debugger;、移除断点、取消黑盒(如果不再需要)。

总结

这一章是 Sources 面板系列的收官之作。我们用一个完整的待办应用案例,把前面十章的技能全部串了起来:

  • 从打开面板到定位文件。

  • 从行断点到条件断点和日志点。

  • 从单步执行到 Scope 分析。

  • 从debugger 命令到代码段。

  • 从Workspace 持久化到性能分析和黑盒脚本。

Sources 面板是 Chrome DevTools 中最核心、最强大的调试工具。它把“看代码、改代码、调代码”三件事整合在一个界面中。掌握了它,你就掌握了前端调试的“基本功”。

但工具只是工具,真正的能力在于根据问题选择合适的工具。希望通过这个系列的学习,你不仅记住了每个功能的位置,更建立了一套自己的调试思维框架——遇到问题时,知道先做什么、再做什么、用什么工具。

调试是一门实践的手艺。读一百篇文章,不如亲手调试一个真实的 bug。从今天开始,在你的项目中多用 Sources 面板,多设置断点,多单步执行。用得越多,它就越成为你身体的一部分。

系列完结,但调试之路才刚刚开始。祝你调试愉快。