到这里,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 = 11,而不是正确的→总计: 1 项,已完成: 1 项。
标记错误:当任务数量较多时,点击某个任务的复选框,有时会切换错误的那个任务。
性能卡顿:当任务数量较多时,页面操作明显卡顿。
示例目录结构
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 视图来定位:
切换到 Bottom-Up 标签页。
按 Self Time 列排序,排在第一位的很可能是 VendorUtils.formatTaskText——因为它在每次渲染时都被调用,并且内部有大量低效操作。
点击该函数名旁边的链接,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 面板调试的最佳实践清单,供你在日常开发中参考:
优先使用命令菜单:Ctrl/Cmd + Shift + P → sources,比鼠标点击快得多。
善用条件断点和日志点:不要在循环里设置普通断点,也不要在代码里塞 console.log。
用 debugger; 快速埋点:但记得提交前删除。
把常用脚本保存为代码段:一次编写,随时复用。
配置 Workspace:让浏览器中的修改持久化到本地文件。
把第三方库加入黑盒:让调试器专注在你的业务代码上。
性能问题先录 Performance:从 Bottom-Up 或 Call Tree 视图定位具体代码。
调试完记得清理:删除 debugger;、移除断点、取消黑盒(如果不再需要)。
总结
这一章是 Sources 面板系列的收官之作。我们用一个完整的待办应用案例,把前面十章的技能全部串了起来:
从打开面板到定位文件。
从行断点到条件断点和日志点。
从单步执行到 Scope 分析。
从debugger 命令到代码段。
从Workspace 持久化到性能分析和黑盒脚本。
Sources 面板是 Chrome DevTools 中最核心、最强大的调试工具。它把“看代码、改代码、调代码”三件事整合在一个界面中。掌握了它,你就掌握了前端调试的“基本功”。
但工具只是工具,真正的能力在于根据问题选择合适的工具。希望通过这个系列的学习,你不仅记住了每个功能的位置,更建立了一套自己的调试思维框架——遇到问题时,知道先做什么、再做什么、用什么工具。
调试是一门实践的手艺。读一百篇文章,不如亲手调试一个真实的 bug。从今天开始,在你的项目中多用 Sources 面板,多设置断点,多单步执行。用得越多,它就越成为你身体的一部分。
系列完结,但调试之路才刚刚开始。祝你调试愉快。