前言
不知道大家平时在前端开发中,是如何追踪数据流向的。console.log()/console.count()/console.table()肯定大家或多或少的使用过。 还有那debugger也是必不可少的方式。
针对,一些简单的数据查验,上面所说的其实已经够用。但是,在面对页面结构繁杂,数据流向紊乱的应用时,上面的措施就有点捉襟见肘。
所以,今天我们来深入研究一下,如何优雅的进行数据追踪。也就是如何高效的在浏览器中进行断点的跟踪。
好了,天不早了,干点正事哇。
我们能所学到的知识点
- 前置知识点
- 代码行断点
- DOM 变更断点
- XHR/fetch 断点
- 事件监听器断点
- 异常断点
- 奇技淫巧
1. 前置知识点
「前置知识点」,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。「如果大家对这些概念熟悉,可以直接忽略」同时,由于阅读我文章的群体有很多,所以有些知识点可能「我视之若珍宝,尔视只如草芥,弃之如敝履」。以下知识点,请「酌情使用」。
console
我们在了不起的Base64中介绍过RFC。
图片
它可以算是网络协议的「圣经」。
而,针对前端的部分技术,其实我们可以在WHATWG[1]找对对应的标准描述。换句话说,我们可以里面找到最权威的解释说明和使用方式。
WHATWG (Web Hypertext Application Technology Working Group) 是一个由一群来自不同公司的 web 开发者组成的组织,致力于推动 web 标准的发展。该组织成立于2004年。WHATWG 最知名的工作之一就是 HTML Living Standard(HTML5),该标准定义了现代 web 页面的结构和行为。除了HTML Living Standard,WHATWG 还参与了一些其他规范的制定,包括Web Workers、Web Storage、Fetch API 等。
下面是我们截取的部分技术的文档。
图片
在Console中,我们看到如下的结构。
图片
看到截图中,有一个namespace console 。我们可以从截图中得知。在内置console中包含四部分
- loging
- counting
- grouping
- timing
在之前我们讲浏览器内核时提到过。在chrome/chromium的内核中,其中有很多C/C++的代码。我们可以在chromium 在线仓库[2]进行查询。
图片
此图中展示了在Chromium内核中console实现
回到WHATWG中,我们就大家最熟悉的console.log来简单聊聊,如何优雅的进行日志的输出。
我们平时做log的输出是不是,用console.log(message)
console.log(`${变量名}_一堆硬编码的字符信息`)
其实哇,在message中可以内嵌下面的格式化说明符。用于占位并输出指定的信息。
下面是各种说明符的使用案例。
// %s - 字符串格式化
console.log("输出字符串: %s", "前端柒八九!");
// %d or %i - 整数格式化
console.log("输出整数格式: %d", 42);
console.log("输出整数格式: %i", 42);
// %f - 浮点数格式化
console.log("输出浮点数格式: %f", 3.14159);
// %o - 以可折叠的优化多行样式显示一个对象,适合复杂对象
const obj = { age: 18, b: 'string', c: { name: "前端柒八九" } };
console.log("输出对象格式: %o", obj);
// %O - 以不可折叠的 JavaScript 对象格式化
console.log("用于简单的对象表示: %O", obj);
// %c - 应用 CSS 样式到输出
console.log("%c对文本进行样式化输出.", "font-size:20px; color:blue;");
我们将其复制到Source-Snippet中进行验证。
图片
断点类型
最常见的断点类型是代码行断点(就是我们经常用到的方式)。但是设置代码行断点可能效率较低,特别是如果我们不确定要查找的确切位置,或者如果我们正在处理大型代码库。
断点类型 | 用途 |
代码行 | 在代码指定区域触发断点。 |
有条件的代码行 | 只在满足限定条件时,在指定地方触发断点 |
记录点 | 在不暂停代码运行的情况下向控制台输出日志 |
DOM | 在更改或删除特定 DOM 节点或其子节点时触发断点 |
XHR | 当 XHR URL 包含某个字符串模式时触发断点 |
事件监听器 | 在指定事件触发后触发断点 |
异常 | 在抛出已捕获或未捕获异常的代码时触发断点 |
函数 | 每当调用特定函数时触发断点 |
Monitor Events & monitor
monitorEvents 是一个在浏览器开发者工具中使用的 JavaScript 方法,用于「监控指定元素上特定类型的事件」。这个方法通常用于调试和分析事件的触发情况。
一旦使用 monitorEvents 监控了某个元素上的事件,当该元素上触发相应类型的事件时,浏览器会在控制台中打印相应的事件信息,包括事件类型、事件目标等。
用法
// 监控特定元素上的一个或多个事件类型
monitorEvents(element, eventTypes);
- element: 要监控的 HTML 元素。
- eventTypes: 要监控的事件类型,可以是单个事件类型的字符串,或者是事件类型组成的数组。
示例
// 监控窗口内的点击事件
monitorEvents(window, 'click');
// 监控文档body上的键盘按键事件
monitorEvents(document.body, ['keyup', 'keydown']);
然后,我们还可以在控制台的Element中直接选中元素,然后在Console中输入对应的指令
图片
在特定元素触发对应的事件后,在控制台就会打印除对应的Event的信息。
图片
上面,我们涉及到一个$0变量。其实这是chrome-devtool的一种内置变量。在Elements选中一个元素时,我们就可以在Console中查询对应的元素引用。
我们还可以通过getEventListeners($0)来获取该元素上绑定的事件信息。
然后,我们还可以通过$0.addEventListener来添加对应的事件。这样我们就不用通过class/id来现获取元素,然后再调用addEventListener了。
图片
monitor函数调用
monitor方法允许你监听特定函数的调用
// 定义一个示例函数
function myFn() { }
// 进行监控
monitor(myFn)
myFn()
// function myFn called
myFn(1)
// function myFn called with arguments: 1
有时候,我们想要代码中对函数想进行监控,我们还可以使用monitor。
下面代码中,我们用Vite启动一个React项目。
2. 代码行断点
当我们对即将要监控的代码胸有成竹时,也就是我们知道代码的确切位置,那么我们就可以「代码行断点」,DevTools 总是在执行此代码行之前暂停。
设置 DevTools 中的代码行断点:
- 点击Sources选项卡
- 打开想要设置断点的文件
我们可以在Sources的左侧文件目录中进行搜索
如果想调试的文件层级过于深,我们可以使用⌘ P的快捷键,通过文件名来搜索
- 找到指定的代码行
- 在代码行左侧是行号列,点击它,此时一个「蓝色图标」出现在行号列处。
直接左键选中
或者右键唤起弹窗中,选择Add breakpoint
图片
在代码中设置代码行断点
我们还可以采用「硬编码」的方式,通过debugger在代码中打断点。
console.log("front");
console.log("789");
debugger;
console.log("456");
这种方式,是我们平时经常用到的,这里就不在展开说明了。
有条件的代码行断点
想必上面的打断点的方式大家都比较熟悉,现在我们再说一个大家平时可能会遇到的问题。
这种方式,墙裂推荐。效果不好,你打我。
假设现在有个循环,但是我们很确定的是,在循环的前半部分数据是好的,而在后半部分数据有问题。在之前,我们可能会通过「代码行断点」,在指定地方进行断点处理。如果是这种操作的话,那我们就需要对前面的数据也需要跟踪。
如果,下次遇到这种操作,我们可以用「有条件的代码行断点」 - 这种断点在我们想要跳过与我们的不关心的数据时非常有用。
- 打开Sources选项卡
- 打开想要设置断点的文件
- 找到代码行
- 在代码行左侧是行号列,右键点击它。
- 选择Add conditional breakpoint。一个对话框显示在代码行的下方。
- 在对话框中输入我们的筛选条件。
- 按 Enter 激活断点。一个带有问号的「橙色图标」出现在行号列的顶部。
上面的代码中表示,当i>3时候,才会触发断点,此时我们可以通过Watch来查询我们想知道的的数据信息,并且还可以在Block和Local也会显示当前断点上下文中的数据信息。
例如,我们可以在触发断点后,使用console.table()来查验localStorage的信息。
打印函数调用堆栈
如果函数的调用层级比较多,我们还可以把筛选条件置换成console.trace()在断点触发时,来查验对应的函数调用层级。
图片
参数定制化
同时,我们还可以在用一种近乎癫狂的方式,用我们自己的参数来为所欲为的让代码按照我们既定的路线进行运行。
图片
我们通过对参数进行假定,然后在触发对应的函数时,按照我们给定的参数来运行函数
图片
在代码层面id值为1,但是我们可以通过「有条件的代码行断点」,将其替换成我们想要探查的数值。并且还不影响函数的运行顺序。
计算函数耗时
针对一个长list的循环,我们想通过一些方式来计算它的耗时,一般我们通过硬编码的方式使用console.time()/console.timeEnd()在循环的前后进行处理。
其实,我们可以在起始点设置一个带有条件console.time('label')的断点,在结束点设置一个带有条件console.timeEnd('label')的断点。
图片
根据参数个数暂停
只有当当前函数以 N 个参数调用时才暂停:arguments.callee.length === N
在函数「有可选参数」的情况下很有用。
根据函数参数个数不匹配暂停
只有当当前函数以错误的参数个数调用时才暂停:(arguments.callee.length) != arguments.length
图片
程序化切换
使用全局布尔值对一个或多个条件断点进行门控:
图片
通过全局变量控制一组断点
图片
上面的案例中,我们使用了setTimeout来控制enableBreakpoints,其实我们还可以手动触发window.enableBreakpoints = true;来控制是否对某些断点进行开启和关闭。
日志代码行断点
使用「日志代码行断点」(logpoints)可以在「不暂停执行且不用在代码中添加console.log()调用的情况下」,将消息输出到控制台。其实,这种情况和「有条件的代码行断点」中加入console.log()效果差不多。
设置日志点的步骤:
- 打开Sources选项卡。
- 打开想要设置断点的文件。
- 找到代码行。
- 在代码行左侧是行号列。右键点击它。
- 选择Add logpoint。一个对话框显示在代码行的下方。
- 在对话框中输入我们的日志消息。我们可以使用与 console.log(message) 调用相同的语法。
- 按 Enter 激活断点。一个带有「两个点的粉色图标」出现在行号列的顶部。
图片
这个示例展示了在第 9 行设置的「日志代码行断点」,将变量i的值输出到控制台。
编辑代码行断点
使用Breakpoints面板可以禁用、编辑或删除代码行断点。
编辑断点组
Breakpoints面板「按文件对断点进行分组,并按行和列号进行排序」。我们可以对组执行以下操作:
- 通过点击其名称折叠或展开一个组。
- 通过点击组或断点旁边的复选框单独启用或禁用组或断点。
- 将鼠标悬停在其上,然后点击关闭按钮可以要删除一个组。
当我们禁用断点时,Sources 面板会使其在行号旁边的标记「变为透明」。
组具有上下文菜单。在Breakpoints面板中,选中一个组然后右键,然后选择:
- 启用文件中的所有断点
- 禁用文件中的所有断点
- 删除文件中的所有断点(本组内)
- 删除其他断点(在其他组中)
- 删除所有断点(在所有文件中)
编辑断点
要编辑断点:
- 点击断点旁边的复选框以启用或禁用它。当我们禁用断点时,Sources 面板会使其在行号旁边的标记「变为透明」。
- 将鼠标悬停在断点上,然后点击编辑以编辑,点击关闭以删除它。
- 在编辑断点时,可以在内联编辑器的下拉列表中更改其类型。
图片
- 右键点击断点以查看其上下文菜单,并选择以下选项之一:
图片
- 编辑条件或日志点。
- 显示位置。
- 删除断点。
- 删除其他断点(在其他文件中)。
- 删除所有断点(在所有文件中)。
3. DOM 变更断点
假设我们有如下的代码
import { useRef } from "react";
/**
* DebuggerDemo组件
*/
export default function DebuggerDemo() {
const refSection = useRef<HTMLDivElement>(null);
return (
<div>
<h1>DOM Debugger Demo</h1>
<div ref={refSection}>
<div>原有的内容</div>
</div>
<button
onClick={() => {
if (refSection.current) {
const newDiv = document.createElement("div");
const newContent = document.createTextNode("前端柒八九");
newDiv.appendChild(newContent);
refSection.current.appendChild(newDiv);
}
}}
>
修改Section的子树
</button>
</div>
);
}
在button触发时,会在div中插入一个新的div。
当我们想要在更改 DOM 节点或其子节点的代码上暂停时,可以使用 「DOM 变更断点」。
设置 DOM 变更断点的步骤:
- 点击Elements选项卡。
- 找到我们想要设置断点的元素。
- 右键点击元素。
- 悬停在Break on上,然后选择Subtree modifications、Attribute modifications或Node removal。
图片
我们可以在以下位置找到 DOM 变更断点列表:
- Elements > DOM Breakpoints 面板。
图片
- Sources > DOM Breakpoints 侧面板。
图片
DOM 变更断点的类型
- 「Subtree modifications(子树修改)」:当当前选定节点的子节点被移除或添加,或子节点的内容发生更改时触发。不会在子节点属性更改时触发,也不会在对当前选定节点的任何更改上触发。
- 「Attributes modifications(属性修改)」:当当前选定节点上添加或删除属性,或属性值更改时触发。
- 「Node Removal(节点移除)」:当当前选定节点被移除时触发。
当我们触发上面button时候,也就是触发了,div的子树修改的断点,在动作触发的同时,我们就会跳转到指定的代码中。
图片
在此时,我们也可以通过Watch来查看指定数据的信息。和在Block和Local中查看上下文中的信息。
这里有一个点,额外提醒一下,上面的代码是用Hook写的,而我们之前写过,Hook其实就是一个闭包,在上面截图右侧部分是不是有一个Scope。有兴趣的同学,可以打开看看。这里就不展示说明了。
4. XHR/fetch 断点
假设,我们有如下的页面结构
import { useEffect, useState } from "react";
/**
* DebuggerDemo组件
*/
export default function DebuggerDemo() {
const [posts, setPosts] = useState<Array<{ id: string; title: string }>>([]);
const getPosts = async () => {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts"
// {
// method: "POST",
// }
);
const data = await response.json();
if (!data) return;
setPosts(data);
} catch (err) {
console.log(err);
} finally {
console.log("接口请求完成");
}
};
useEffect(() => {}, []);
return (
<div>
<h1>XHR Debugger Demo</h1>
{posts.map((item) => (
<div key={item?.id}>{item?.title}</div>
))}
<button onClick={() => getPosts()}>接口查询</button>
</div>
);
}
当我们想在 XMLHttpRequest(XHR)的「请求 URL 包含特定字符串时」暂停时,可以使用 「XHR/fetch 断点」。DevTools 会在 XHR 调用 send() 的代码行上暂停。
这种情况有助于快速找到导致页面请求错误 URL 的 AJAX 或 Fetch 源代码。
设置 XHR/fetch 断点的步骤:
- 点击Sources选项卡。
- 展开 XHR Breakpoints 面板。
- 点击Add(添加断点)。
- 输入要在其上中断的字符串。当这个字符串出现在任何 XHR 请求的 URL 中时,DevTools 会暂停。
- 按 Enter 确认。
在点击查询后,我们就可以在指定的接口查询中,进行断点处理。
图片
还有一点,我们需要额外的说明,我们用SPA搭建页面,此时针对异步接口处理时,Axios是一个王者级别的解决方案。
如果大家看过Axios源码的话,就会知道,它其实就是在XHR和Fetch上做了一层封装。
所以,按道理,我们也可以通过XHR/fetch 断点进行接口断点。但是呢,由于Devtool有一个lognore List。
图片
默认情况下,Know third-party scripts form source map这项是勾选的。如果我们想要在调试Axios中的接口,我们就需要把这项给取消掉。
在处理完后,别记得把这个关闭掉,要不然bundle中的debugger也会会触发。
针对XHR我们还可以在Event Listener Breakpoints中进行对应事件的监听。(这个我们在下面「事件监听器断点」中介绍)
图片
使用「XHR/fetch 断点」时,其实在工作中能帮助我们很大,比方说你接手了一个项目,然后发现在某个接口中出现了问题,按照我们以往的排查方式的话,是不是先在控制台找到对应的url,然后在代码中全局搜索这个url。通过对应的本地方法,再次向上搜索,如果嵌套层级过多,那找着找着,把原来向定位的问题都遗忘了呢。
而有了「XHR/fetch 断点」,我们可以通过url中特定的参数进行断点处理。并且这是一种「子上而下」的搜索方式。我们可以通过调用栈就能把调用路线很清晰的把握住。
5. 事件监听器断点
当我们希望在事件被触发后运行的事件监听器代码上暂停时,请使用事件监听器断点。我们可以选择特定的事件,比如 click,或事件的类别,比如所有鼠标事件。
图片
设置事件监听器断点的步骤:
- 点击Sources选项卡。
- 展开 Event Listener Breakpoints 面板。DevTools 显示了一系列事件类别,比如 Animation。
- 勾选其中一个类别,以便在该类别的任何事件触发时暂停,或展开该类别并选择特定的事件。
- 创建事件监听器断点。
假设我们存在如下的页面
import { useEffect, useState } from "react";
/**
* DebuggerDemo组件
*/
export default function DebuggerDemo() {
const [posts, setPosts] = useState<Array<{ id: string; title: string }>>([]);
useEffect(() => {
const fetchData = () => {
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
setPosts(data);
}
};
xhr.open("GET", "https://jsonplaceholder.typicode.com/posts", true);
xhr.send();
};
fetchData();
}, []);
const handleClick = (e: HTMLButtonElement) => {
console.log(e);
};
return (
<div>
<h1>Event Debugger Demo</h1>
<button
onClick={() => handleClick(event as unknown as HTMLButtonElement)}
>
按钮点击
</button>
{posts.map((item) => (
<div key={item?.id}>{item?.title}</div>
))}
</div>
);
}
然后,我们在选中我们想要监听的事件。
图片
XHR 的事件监听
图片
Mouse 事件监听
图片
当然,如果我们想看React内部的处理逻辑,我们可以在lgnore list中将Know third-party scripts form source map打开,这样的话我们在断点触发后,也能查看框架内部的处理逻辑。
图片
6. 异常断点
当我们想在错误时进行断点跟踪时,可以使用「异常断点」。
在Sources选项卡的Breakpoints面板中,启用以下选项中的一个或两个,然后执行代码:
图片
- 勾选Pause on uncaught exceptions
图片
在这个例子中,我们在代码的第九行特意写了一个front789的未定义的变量,并且没执行捕获操作。
- 勾选Pause on caught exceptions
图片
在这个例子中,执行在已捕获的异常上暂停。
墙裂建议,在我们开发阶段,将Pause on uncaught exceptions打开,这样可以让浏览器来帮我们找到我们代码不正确的地方。
7. 奇技淫巧
使用 copy()
大家有没有遇到过,在进行log时候,想复制某些数据,但是只能在log输出到控制台后,才能复制。并且这些数据只是单纯的展示,想选中也不好处理。
例如:
图片
其实,我们可以使用copy()API 将浏览器中的特定信息「直接复制到剪贴板,而不会有任何字符串截断」。
- 当前 DOM 的快照:copy(document.documentElement.outerHTML)
- 关于资源的元数据(例如图片):copy(performance.getEntriesByType("resource"))
- 一个大的 JSON blob,格式化:copy(JSON.parse(blob))
- localStorage 的转储:copy(localStorage)
- ....
检查一个难以捕捉的元素
我们想检查一个只有在条件满足时才出现的 DOM 元素。
图片
当我们在first input悬浮时候,想查看second input时候,鼠标移出first input后,后者立马就消失不见了。
我们可以利用如下代码:
setTimeout(function () {
debugger;
}, 5000);
这使我们有 5 秒的时间触发 UI,然后一旦 5 秒计时器结束,JS 执行将暂停,没有任何东西会让你的元素消失。我们可以自由移动鼠标到开发工具而不失去元素:
图片
当 JS 执行暂停时,我们就可以检查元素、编辑其 CSS、在 JS 控制台中执行命令等。
在检查依赖于特定光标位置、焦点等 DOM 时很有用。
监视焦点元素
(function () {
let last = document.activeElement;
setInterval(() => {
if (document.activeElement !== last) {
last = document.activeElement;
console.log("Focus changed to: ", last);
}
}, 100);
})();
图片
Reference
[1]WHATWG:https://spec.whatwg.org/
[2]chromium 在线仓库:https://source.chromium.org/chromium/chromium/src;l=21