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

Pearl Chen

简介

JavaScript 的一项强大功能是它能够通过回调函数异步运行。分配异步回调可让您编写事件驱动型代码,但这也会让跟踪 bug 变得非常麻烦,因为 JavaScript 不是以线性方式执行。

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

异步调用堆栈的简短宣传语概览。
异步调用堆栈的简短宣传语概览。 (我们稍后将详细介绍此演示的流程。)

在开发者工具中启用异步调用堆栈功能后,您将能够深入了解 Web 应用在不同时间点的状态。浏览部分事件监听器(setIntervalsetTimeoutXMLHttpRequest、promise、requestAnimationFrameMutationObservers 等)的完整堆栈轨迹。

在执行堆栈轨迹时,您还可以分析任何变量在该特定运行时执行点的值。它就像是手表表情的时光机!

让我们启用此功能,并尝试其中几种情况。

在 Chrome 中启用异步调试功能

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

在右侧 Call Stack 面板旁边,有一个新的“Async”复选框。切换复选框以开启或关闭异步调试。(尽管启用此功能后,您可能永远都想将其关闭)。

开启或关闭异步功能。

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

您以前可能在 Gmail 中遇到过类似情况:

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

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

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

模拟 Gmail 示例的流程图。
在上图中,以蓝色突出显示的方法是这项全新开发者工具功能最有用的主要位置,因为这些方法以异步方式运行。

通过只查看先前版本的开发者工具中的“Call Stack”面板,postOnFail() 中的断点几乎可以让您了解从何处调用 postOnFail()。不过,我们来看看开启异步堆栈时的差异:

之前
在没有异步调用堆栈的模拟 Gmail 示例中设置的断点。
未启用异步的“调用堆栈”面板。

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

之后
在具有异步调用堆栈的模拟 Gmail 示例中设置的断点。
启用异步的“调用堆栈”面板

您可以看到,XHR 是从 submitHandler() 发起的。很好!

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

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

异步监视表达式

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

将监视表达式与 aysnc 调用堆栈结合使用的示例

评估既往范围内的代码

除了观察表达式之外,您还可以直接在开发者工具的 JavaScript 控制台面板中与先前作用域内的代码进行交互。

想象一下,你是哪些人在开发者工具控制台中,您可以轻松评估、存储和计算不同执行点中的值。

将 JavaScript 控制台与 aysnc 调用堆栈结合使用的示例。
将 JavaScript 控制台与异步调用堆栈结合使用,以调试您的代码。您可以在此处找到上述演示。

使用开发者工具进行操作您的表达式可以为您节省时间,而无需切换回源代码、进行修改和刷新浏览器。

探索链式 promise 解决方案

如果您认为在未启用异步调用堆栈功能的情况下,很难展开之前的模拟 Gmail 流程,可以想象一下,如果使用链接 Promise 等更复杂的异步流程,会有多困难?我们来回顾一下 Jake Archibald 关于 JavaScript Promise 的教程的最后一个示例。

下面是 Jake 的 async-best-example.html 示例中遍历调用堆栈的小动画。

之前
无异步调用堆栈的 promise 示例中设置的断点
未启用异步的“调用堆栈”面板。

请注意,在尝试调试 promise 时,Call Stack 面板会非常缺少相关信息。

之后
采用异步调用堆栈的 promise 示例中设置的断点。
启用异步的“调用堆栈”面板

哇!这种 promise。很多回调。

深入了解网页动画

让我们更深入地了解 HTML5Rocks 归档。还记得 Paul Lewis 的 Leaner, Meaner, Faster Animations with requestAnimationFrame 吗?

打开 requestAnimationFrame 演示,并在 post.html 的 update() 方法的开头(大约第 874 行)添加一个断点。通过异步调用堆栈,我们可以更深入地了解 requestAnimationFrame,包括返回到启动滚动事件回调的功能。

之前
在没有异步调用堆栈的 requestAnimationFrame 示例中设置的断点。
未启用异步的“调用堆栈”面板。
之后
在采用异步调用堆栈的 requestAnimationFrame 示例中设置了断点
启用了异步。

使用 MutationObserver 时跟踪 DOM 更新

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

在 demo.html 的 nodeAdded()(第 31 行)内添加一个断点。启用异步调用堆栈后,您现在可以通过 addNode() 将调用堆栈遍历回初始点击事件。

之前
在无异步调用堆栈的 mutateObserver 示例中设置的断点。
未启用异步的“调用堆栈”面板。
之后
在具有异步调用堆栈的 mutateObserver 示例中设置的断点。
启用了异步。

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

为函数命名

如果您倾向于将所有回调分配为匿名函数,则可能需要为它们指定名称,以便更轻松地查看调用堆栈。

例如,假设有一个匿名函数,如下所示:

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

为其命名,例如 windowLoaded()

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

当加载事件触发时,该事件将显示在开发者工具堆栈轨迹中,并与其函数名称一起显示,而不是带有“(anonymous function)”含义的神秘函数。这样一来,您可以更轻松地一目了然地查看堆栈轨迹中发生的情况。

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

深入探索

简而言之,下面是开发者工具会显示完整调用堆栈的所有异步回调:

  • 计时器:返回到初始化 setTimeout()setInterval() 的位置。
  • XHR:返回到调用 xhr.send() 的位置。
  • 动画帧:返回到调用 requestAnimationFrame 的位置。
  • promise:返回已解析 promise 的位置。
  • Object.observe:返回到最初绑定观察器回调的位置。
  • MutationObservers:返回到触发 mutate 观察者事件的位置。
  • 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 回调的完整堆栈轨迹的功能应该让您高枕无忧。当多个相互关联的异步事件发生时,或者异步回调中抛出未捕获的异常时,开发者工具中的此功能特别有用。

不妨在 Chrome 中试试。 如果您对这项新功能有任何反馈,请在 Chrome 开发者工具 bug 跟踪器Chrome 开发者工具群组中给我们留言。