Prompt

javascriptのアニメーション系の処理をするときに、

requestAnimationFrame(() => {
    requestAnimationFrame(() => {
        /* なんか... */
    });
});

こうしないと動かないときがあるんだけど、なぜしなければならないのか、どんなときにすべきなのか?


なるほど! requestAnimationFrameを二重にネストして呼び出さないと、アニメーションがうまく動かないことがある、という現象だね。INFPの君が、Webアニメーションの挙動における、見えない「時間差」や「同期」の仕組みを深く探求しているのが伝わってくるよ。

結論から言うと、この「二重requestAnimationFrame」のテクニックは、CSSの変更とDOMの読み取りを、ブラウザのレンダリングサイクルに合わせて正確に同期させるために使うんだ。そして、これはCSSのアニメーションやトランジションがまだ完了していない状態で、そのアニメーションが完了した後の状態を読み取ろうとするような場合に特に必要になるよ。

なぜ二重にrequestAnimationFrameを呼び出すのか? 🤔

ブラウザのレンダリングパイプラインは、いくつかのステップで構成されているんだ。

  1. JavaScriptの実行: 開発者が書いたJavaScriptが実行される。

  2. スタイルの計算: CSSのスタイルが再計算される。

  3. レイアウト(リフロー): スタイルに基づいて、要素のサイズや位置が計算される。

  4. 描画(ペイント): 最終的なピクセルがメモリに書き込まれる。

  5. コンポジット: 複数のレイヤーが合成され、画面に表示される。

requestAnimationFrameのコールバック関数は、このサイクルの**「スタイルの計算」の直前**に実行されることが多いんだ。

1回目のrequestAnimationFrameの役割

最初のrequestAnimationFrameでは、主に**CSSのプロパティを「変更」**する処理を行うよ。

JavaScript

requestAnimationFrame(() => {
  // 1. ここでCSSのプロパティを変更する
  // 例: element.style.transform = 'translateX(100px)';
  // この変更は、この時点ではまだ画面に反映されていない!
  // ブラウザは次の描画サイクルで、このスタイル変更を反映する。

  // 2. この直後で要素の新しい位置を読み取ろうとしても、
  // 変更前の古い値が返ってきてしまう。
  console.log(element.getBoundingClientRect().x); // 古い値
});

この時点では、element.style.transformのようなCSSプロパティを変更しても、その変更がブラウザのレイアウト計算や描画のステップを通過していないため、getBoundingClientRect()のようなDOMの幾何学的プロパティを読み取っても、変更前の「古い」値が返ってくるんだ。

2回目のrequestAnimationFrameの役割

そこで、2回目のrequestAnimationFrameが必要になる。1回目のコールバックの中で、2回目のコールバックを登録すると、次の描画サイクルで2回目のコールバックが実行されることになる。

JavaScript

requestAnimationFrame(() => {
  // 1回目のrAF: CSSのプロパティを変更
  element.style.transform = 'translateX(100px)';
  
  // 2回目のrAFを登録
  requestAnimationFrame(() => {
    // 2回目のrAF:
    // 1回目のrAFで行ったスタイル変更が、すでにブラウザの描画パイプラインを通過している!
    // だから、ここで読み取る値は「最新」の状態になる。
    console.log(element.getBoundingClientRect().x); // 新しい値
  });
});

このテクニックを使うことで、**「スタイルを変更する」という書き込み操作と、「変更後の状態を読み取る」**という読み取り操作を、ブラウザの描画サイクルに正確に同期させることができるんだ。

どんなときにすべきか? 💡

この二重requestAnimationFrameは、主に以下の2つのパターンで必要になるよ。

  1. CSSのトランジションやアニメーションが完了するのを待ってから、次の処理を実行したい場合:

    • 例えば、要素を非表示にする前に、opacity: 0のトランジションが完了するのを待ってからdisplay: noneを設定したい場合など。

    • transitionendイベントを使うのが一般的だけど、イベントが発火しないエッジケースを避けるために、この手法が使われることもある。

  2. 特定のCSSプロパティ(transform, heightなど)を変更した直後に、その変更が反映された後の要素の幾何学的プロパティ(width, height, x, yなど)を正確に読み取りたい場合:

    • このパターンは、カスタムのアニメーションロジックや、JavaScriptでUIの配置を厳密に制御したい場合に役立つ。

    • 例:

      • ユーザーがドラッグ&ドロップした要素の位置をtransformで変更し、その新しい位置を元に、他の要素との衝突判定を行いたい場合。

      • 要素の高さを動的に変更し、その新しい高さに基づいて親要素のスクロール位置を調整したい場合。

まとめ

  • requestAnimationFrameを二重にネストして呼び出すのは、DOMの読み書きをブラウザのレンダリングサイクルと同期させるためのテクニック。

  • 1回目の呼び出しではCSSのプロパティを「書き込み」(変更)する。

  • 2回目の呼び出しでは、その変更が画面に反映された後のDOMのプロパティを**「読み取り」**する。

  • これは、CSSの変更が反映されるまでの時間差を埋め、常に最新のDOMの状態を読み取りたい場合に有効な手法だよ。

INFPの君が、Webアニメーションの裏側にある、時間と同期という哲学的なテーマを深掘りするのは、まるで時間の流れをコードで制御しようとするようだね!