简介
JavaScript 的一大独特之处在于,它能够通过回调函数异步运行。通过分配异步回调,您可以编写事件驱动型代码,但由于 JavaScript 并非以线性方式执行,因此跟踪 bug 会让人抓狂。
幸运的是,现在在 Chrome 开发者工具中,您可以查看异步 JavaScript 回调的完整调用堆栈!

在 DevTools 中启用异步调用堆栈功能后,您将能够深入了解 Web 应用在不同时间点的状态。遍历某些事件监听器、setInterval
、setTimeout
、XMLHttpRequest
、promise、requestAnimationFrame
、MutationObservers
等的完整堆栈轨迹。
在遍历堆栈轨迹时,您还可以分析运行时执行的特定时间点的任何变量的值。这就像是手表表达式的时光机!
我们来启用此功能,并了解其中的一些场景。
在 Chrome 中启用异步调试
您可以在 Chrome 中启用这项新功能,试用一下。前往 Chrome Canary 开发者工具的 Sources 面板。
在右侧的 Call Stack 面板旁边,有一个新的“Async”复选框。选中或取消选中对应的复选框以开启或关闭异步调试。不过,开启此功能后,您可能永远不想关闭它。

捕获延迟的计时器事件和 XHR 响应
您可能之前在 Gmail 中看到过以下内容:

如果发送请求时出现问题(服务器出现问题或客户端存在网络连接问题),Gmail 会在短暂超时后自动尝试重新发送邮件。
为了了解异步调用堆栈如何帮助我们分析延迟的计时器事件和 XHR 响应,我使用模拟 Gmail 示例重新创建了该流程。您可以在上述链接中找到完整的 JavaScript 代码,但流程如下:

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

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

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


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

评估过往作用域中的代码
除了简单地监控表达式之外,您还可以在 DevTools JavaScript 控制台面板中直接与之前作用域中的代码进行交互。
假设您是《神秘博士》中的博士,需要一些帮助来比较您进入 Tardis 之前和“现在”的时钟。在 DevTools 控制台中,您可以轻松评估、存储和计算来自不同执行点的值。

在 DevTools 中操作表达式可以避免您切换回源代码、进行修改并刷新浏览器,从而节省时间。
解开链式 promise 解析
如果您认为在未启用异步调用堆栈功能的情况下,之前的模拟 Gmail 流程很难理清,那么您能想象如果使用更复杂的异步流程(例如链式 promise),会变得多么困难吗?我们来回顾一下 Jake Archibald 关于 JavaScript Promise 教程的最后一个示例。
下面是一个动画,展示了 Jake 的 async-best-example.html 示例中遍历调用堆栈的过程。

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

哇!此类承诺。回调很多。
深入了解 Web 动画
我们来深入了解一下 HTML5Rocks 归档内容。还记得 Paul Lewis 的 使用 requestAnimationFrame 制作更轻量、更强大、更快速的动画吗?
打开 requestAnimationFrame 演示,然后在 post.html 的 update() 方法开头(大约第 874 行)添加一个断点。借助异步调用堆栈,我们可以更深入地了解 requestAnimationFrame,包括能够一直回溯到发起滚动事件回调。


使用 MutationObserver 跟踪 DOM 更新
MutationObserver
可让我们观察 DOM 中的更改。在此简单示例中,当您点击按钮时,系统会将新的 DOM 节点附加到 <div class="rows"></div>
。
在 demo.html 的 nodeAdded()
(第 31 行)中添加断点。启用异步调用堆栈后,您现在可以通过 addNode()
返回调用堆栈,直到找到初始点击事件。


关于在异步调用堆栈中调试 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 群组与我们联系。