Chrome DevTools を使用して非同期 JavaScript をデバッグする

はじめに

JavaScript を独自たらしめている強力な機能の 1 つが、コールバック関数を介して非同期で動作する機能です。非同期コールバックを割り当てると、イベントドリブン コードを記述できますが、JavaScript がリニアに実行されないため、バグの追跡が困難になります。

幸い、Chrome DevTools では、非同期 JavaScript コールバックの完全なコールスタックを確認できます。

非同期コールスタックの概要を簡単に説明します。
非同期呼び出しスタックの概要を簡単に説明します。 (このデモのフローについては、後ほど説明します)。

DevTools で非同期呼び出しスタック機能を有効にすると、さまざまな時点でウェブアプリの状態をドリルダウンできます。一部のイベント リスナー(setIntervalsetTimeoutXMLHttpRequest、Promise、requestAnimationFrameMutationObservers など)の完全なスタック トレースを確認します。

スタック トレースを確認する際に、ランタイム実行の特定の時点での変数の値を分析することもできます。ウォッチフェイスのタイムマシンのようなものです。

この機能を有効にして、いくつかのシナリオを見てみましょう。

Chrome で非同期デバッグを有効にする

この新機能をお試しいただくには、Chrome で有効にしてください。Chrome Canary DevTools の [Sources] パネルに移動します。

右側の [Call Stack] パネルの横に、[Async] の新しいチェックボックスがあります。チェックボックスをオンまたはオフにして、非同期デバッグを有効または無効にします(一度オンにすると、オフにすることはほとんどありません)。

非同期機能をオンまたはオフに切り替えます。

遅延したタイマー イベントと XHR レスポンスをキャプチャする

Gmail で以下のようなメッセージが表示されたことがあると思います。

Gmail がメールの送信を再試行している。

リクエストの送信で問題が発生した場合(サーバーに問題があるか、クライアント側のネットワーク接続に問題がある場合)、Gmail は短いタイムアウト後にメッセージを自動的に再送信しようとします。

非同期呼び出しスタックが遅延したタイマー イベントと XHR レスポンスを分析するうえでどのように役立つかを確認するため、Gmail のモック例でそのフローを再現しました。JavaScript コード全体は上記のリンクから確認できますが、フローとしては次のようになります。

架空の Gmail の例のフローチャート。
上の図で青色でハイライト表示されているメソッドは、非同期で動作するため、この新しい DevTool 機能が最も効果を発揮する場所です。

以前のバージョンの DevTools の [Call Stack] パネルのみを見ると、postOnFail() 内のブレークポイントから postOnFail() が呼び出された場所に関する情報がほとんど得られませんでした。ただし、非同期スタックをオンにしたときの違いを確認してください。

非同期呼び出しスタックのない Gmail のモック例で設定されたブレークポイント。
非同期が有効になっていないコールスタック パネル。

postOnFail() が AJAX コールバックから開始されたことがわかりますが、それ以上の情報はありません。

非同期呼び出しスタックを含む Gmail のモック例で設定されたブレークポイント。
非同期が有効になっている [Call Stack] パネル

XHR が submitHandler() から開始されたことがわかります。すばらしいですね!

非同期呼び出しスタックをオンにすると、呼び出しスタック全体を表示して、リクエストが submitHandler()(送信ボタンのクリック後に発生)から開始されたのか、retrySubmit()setTimeout() 遅延後に発生)から開始されたのかを簡単に確認できます。

submitHandler()
非同期呼び出しスタックを含む Gmail のモック例で設定されたブレークポイント
retrySubmit()
非同期呼び出しスタックを含む Gmail のモック例で設定された別のブレークポイント

Watch 式を非同期で評価する

完全なコールスタックを走査すると、ウォッチ対象の式も更新され、その時点での状態が反映されます。

非同期呼び出しスタックとウォッチ式を使用する例

過去のスコープのコードを評価する

式を単に監視するだけでなく、DevTools の JavaScript コンソール パネルで、前のスコープのコードと直接やり取りできます。

ドクター・フーだと想像してみてください。ターディスに乗る前と「今」の時刻を比較する際に、少し助けが必要だとします。DevTools コンソールでは、さまざまな実行ポイントの値を簡単に評価、保存、計算できます。

非同期コールスタックと JavaScript コンソールの使用例。
JavaScript コンソールを非同期コールスタックと組み合わせて使用して、コードをデバッグします。上記のデモはこちらでご覧いただけます。

DevTools 内で式を操作すると、ソースコードに戻って編集し、ブラウザを更新する手間が省けます。

チェーンされた Promise の解決を解きほぐす

非同期呼び出しスタック機能を有効にしないと、前述の Gmail のモックフローを解明するのは難しいと思われたでしょうか。チェーンされたプロミスなどの複雑な非同期フローで、どれほど解明が難しくなるか想像できますか?Jake Archibald の JavaScript Promises に関するチュートリアルの最後の例を見てみましょう。

以下は、Jake の async-best-example.html の例で呼び出しスタックをウォークするアニメーションです。

非同期呼び出しスタックのない promise で設定されたブレークポイントの例
非同期が有効になっていないコールスタック パネル。

プロミスをデバッグしようとすると、コールスタック パネルに表示される情報が非常に少ないことに注目してください。

非同期コールスタックを含む Promise の例で設定されたブレークポイント。
非同期が有効になっている [Call Stack] パネル

結果を確認しましょう。このような約束。コールバックが多い。

ウェブ アニメーションに関する分析情報を取得する

HTML5Rocks のアーカイブを詳しく見てみましょう。Paul Lewis 氏の requestAnimationFrame による軽量で高速なアニメーションを覚えていますか?

requestAnimationFrame デモを開き、post.html の update() メソッドの先頭(874 行目付近)にブレークポイントを追加します。非同期コールスタックを使用すると、requestAnimationFrame に関する多くの分析情報を取得できます。たとえば、開始したスクロール イベントのコールバックまで遡ることができます。

非同期コールスタックのない requestAnimationFrame の例で設定されたブレークポイント。
非同期が有効になっていないコールスタック パネル。
変更後
非同期コールスタックを含む requestAnimationFrame の例で設定されたブレークポイント
非同期が有効になっている場合

MutationObserver を使用した DOM の更新をトラッキングする

MutationObserver を使用すると、DOM の変更を監視できます。この簡単な例では、ボタンをクリックすると、新しい DOM ノードが <div class="rows"></div> に追加されます。

demo.html の nodeAdded()(31 行目)にブレークポイントを追加します。非同期コールスタックを有効にすると、コールスタックを addNode() から最初のクリック イベントまで遡ることができます。

非同期呼び出しスタックのない mutationObserver の例で設定されたブレークポイント。
非同期が有効になっていないコールスタック パネル。
変更後
非同期コールスタックを含む mutationObserver の例で設定されたブレークポイント。
非同期が有効になっている場合

非同期コールスタックで JavaScript をデバッグするためのヒント

関数に名前を付ける

すべてのコールバックを匿名関数として割り当てる場合は、コールスタックの表示を容易にするために、代わりに名前を付けることをおすすめします。

たとえば、次のような匿名関数を考えてみましょう。

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

windowLoaded() などの名前を付けます。

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

読み込みイベントが発生すると、DevTools のスタック トレースには、わかりにくい「(匿名関数)」ではなく、関数名が表示されます。これにより、スタック トレース内で何が起こっているかを一目で確認できるようになります。

変更前
匿名関数。
変更後
名前付き関数

もっと見る

要約すると、DevTools でコールスタック全体が表示される非同期コールバックは次のとおりです。

  • タイマー: setTimeout() または setInterval() が初期化された場所まで戻ります。
  • XHR: xhr.send() が呼び出された場所まで戻ります。
  • アニメーション フレーム: requestAnimationFrame が呼び出された場所まで戻ります。
  • Promise: Promise が解決された場所まで戻ります。
  • Object.observe: オブザーバー コールバックが最初にバインドされた場所に戻ります。
  • MutationObservers: ミューテーション オブザーバー イベントがトリガーされた場所まで戻ります。
  • 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 のバグトラッカーまたは Chrome DevTools グループでお問い合わせください。