使用 Chrome 开发者工具调试异步 JavaScript

Pearl Chen

简介

JavaScript 的一大独特之处在于,它能够通过回调函数异步运行。通过分配异步回调,您可以编写事件驱动型代码,但由于 JavaScript 并非以线性方式执行,因此跟踪 bug 会让人抓狂。

幸运的是,现在在 Chrome 开发者工具中,您可以查看异步 JavaScript 回调的完整调用堆栈!

异步调用堆栈的简要预览。
异步调用堆栈的简要预览。 (我们很快就会详细介绍此演示的流程。)

在 DevTools 中启用异步调用堆栈功能后,您将能够深入了解 Web 应用在不同时间点的状态。遍历某些事件监听器、setIntervalsetTimeoutXMLHttpRequest、promise、requestAnimationFrameMutationObservers 等的完整堆栈轨迹。

在遍历堆栈轨迹时,您还可以分析运行时执行的特定时间点的任何变量的值。这就像是手表表达式的时光机!

我们来启用此功能,并了解其中的一些场景。

在 Chrome 中启用异步调试

您可以在 Chrome 中启用这项新功能,试用一下。前往 Chrome Canary 开发者工具的 Sources 面板。

在右侧的 Call Stack 面板旁边,有一个新的“Async”复选框。选中或取消选中对应的复选框以开启或关闭异步调试。不过,开启此功能后,您可能永远不想关闭它。

开启或关闭异步功能。

捕获延迟的计时器事件和 XHR 响应

您可能之前在 Gmail 中看到过以下内容:

Gmail 正在重试发送电子邮件。

如果发送请求时出现问题(服务器出现问题或客户端存在网络连接问题),Gmail 会在短暂超时后自动尝试重新发送邮件。

为了了解异步调用堆栈如何帮助我们分析延迟的计时器事件和 XHR 响应,我使用模拟 Gmail 示例重新创建了该流程。您可以在上述链接中找到完整的 JavaScript 代码,但流程如下:

模拟 Gmail 示例的流程图。
在上图中,蓝色突出显示的方法是此新开发者工具功能最有益的使用场景,因为这些方法是异步工作的。

如果仅查看旧版 DevTools 中的“调用堆栈”面板,postOnFail() 中的断点将无法提供有关 postOnFail() 的调用来源的任何信息。但请看启用异步堆栈后的差异:

之前
在模拟 Gmail 示例中设置的断点,不含异步调用堆栈。
启用异步的调用堆栈面板。

您可以在此处看到 postOnFail() 是从 AJAX 回调启动的,但没有其他信息。

之后 在模拟 Gmail 示例中设置的断点,其中包含异步调用堆栈。
启用异步的调用堆栈面板。

您可以在此处看到 XHR 是从 submitHandler() 启动的。很好!

开启异步调用堆栈后,您可以查看整个调用堆栈,以便轻松了解请求是从 submitHandler()(点击“提交”按钮后发生)还是从 retrySubmit()setTimeout() 延迟后发生)发起的:

submitHandler()
在包含异步调用堆栈的模拟 Gmail 示例中设置的断点
retrySubmit()
在包含异步调用堆栈的模拟 Gmail 示例中设置的另一个断点

异步监视表达式

当您遍历完整的调用堆栈时,所监视的表达式也会更新,以反映其当时的状态!

将监视表达式与异步调用堆栈搭配使用的示例

评估过往作用域中的代码

除了简单地监控表达式之外,您还可以在 DevTools JavaScript 控制台面板中直接与之前作用域中的代码进行交互。

假设您是《神秘博士》中的博士,需要一些帮助来比较您进入 Tardis 之前和“现在”的时钟。在 DevTools 控制台中,您可以轻松评估、存储和计算来自不同执行点的值。

将 JavaScript 控制台与异步调用堆栈搭配使用的一个示例。
将 JavaScript 控制台与异步调用堆栈结合使用,以调试代码。如需查看上述演示,请点击此处

在 DevTools 中操作表达式可以避免您切换回源代码、进行修改并刷新浏览器,从而节省时间。

解开链式 promise 解析

如果您认为在未启用异步调用堆栈功能的情况下,之前的模拟 Gmail 流程很难理清,那么您能想象如果使用更复杂的异步流程(例如链式 promise),会变得多么困难吗?我们来回顾一下 Jake Archibald 关于 JavaScript Promise 教程的最后一个示例。

下面是一个动画,展示了 Jake 的 async-best-example.html 示例中遍历调用堆栈的过程。

之前
在没有异步调用堆栈的 promise 示例中设置的断点
启用异步的调用堆栈面板。

请注意,在尝试调试 Promise 时,调用堆栈面板中的信息非常少。

之后 在包含异步调用堆栈的 promises 示例中设置的断点。
启用异步的调用堆栈面板。

哇!此类承诺。回调很多。

深入了解 Web 动画

我们来深入了解一下 HTML5Rocks 归档内容。还记得 Paul Lewis 的 使用 requestAnimationFrame 制作更轻量、更强大、更快速的动画吗?

打开 requestAnimationFrame 演示,然后在 post.html 的 update() 方法开头(大约第 874 行)添加一个断点。借助异步调用堆栈,我们可以更深入地了解 requestAnimationFrame,包括能够一直回溯到发起滚动事件回调。

之前
在 requestAnimationFrame 示例中设置的断点,其中不包含异步调用堆栈。
启用异步的调用堆栈面板。
之后 在包含异步调用堆栈的 requestAnimationFrame 示例中设置的断点
启用 async 后。

使用 MutationObserver 跟踪 DOM 更新

MutationObserver 可让我们观察 DOM 中的更改。在此简单示例中,当您点击按钮时,系统会将新的 DOM 节点附加到 <div class="rows"></div>

在 demo.html 的 nodeAdded()(第 31 行)中添加断点。启用异步调用堆栈后,您现在可以通过 addNode() 返回调用堆栈,直到找到初始点击事件。

之前
在 mutationObserver 示例中设置的断点,其中不包含异步调用堆栈。
启用异步的调用堆栈面板。
之后 在 mutationObserver 示例中设置的断点,其中包含异步调用堆栈。
启用 async 后。

关于在异步调用堆栈中调试 JavaScript 的提示

为函数命名

如果您倾向于将所有回调都指定为匿名函数,不妨为它们指定名称,以便更轻松地查看调用堆栈。

例如,使用如下匿名函数:

window.addEventListener('load', function() {
  // do something
});

并为其命名,例如 windowLoaded()

window.addEventListener('load', function <strong>windowLoaded</strong>(){
  // do something
});

当 load 事件触发时,它将在 DevTools 堆栈轨迹中显示其函数名称,而不是含糊的“(anonymous function)”。这样,您就可以更轻松地一目了然地了解堆栈轨迹中发生的情况。

之前
匿名函数。
之后
命名函数

深入探索

总的来说,以下是 DevTools 会显示完整调用堆栈的所有异步回调:

  • 计时器:返回 setTimeout()setInterval() 的初始化位置。
  • XHR:返回调用 xhr.send() 的位置。
  • 动画帧:返回调用 requestAnimationFrame 的位置。
  • Promise:返回到 promise 已解析的位置。
  • Object.observe:返回到最初绑定观察器回调的位置。
  • MutationObservers:返回触发 MutationObserver 事件的位置。
  • window.postMessage():遍历进程内消息传递调用。
  • DataTransferItem.getAsString()
  • FileSystem API
  • IndexedDB
  • WebSQL
  • 通过 addEventListener() 的符合条件的 DOM 事件:返回到触发事件的位置。出于性能原因,并非所有 DOM 事件都适用于异步调用堆栈功能。目前可用的事件示例包括:'scroll'、'hashchange' 和 'selectionchange'。
  • 通过 addEventListener() 触发的多媒体事件:返回触发事件的位置。可用的多媒体事件包括:音频和视频事件(例如“play”“pause”“ratechange”)、WebRTC MediaStreamTrackList 事件(例如“addtrack”“removetrack”)和 MediaSource 事件(例如“sourceopen”)。

能够查看 JavaScript 回调的完整堆栈轨迹应该可以让您保持冷静。当多个异步事件相互关联地发生,或者从异步回调中抛出未捕获的异常时,DevTools 中的这项功能尤为有用。

请在 Chrome 中试用。 如果您对这项新功能有任何反馈,请通过 Chrome DevTools bug 跟踪器Chrome DevTools 群组与我们联系。