もし Vue に触れたことがあるなら、おそらく TransitionGroup コンポーネントについて何かしらの知識を持っているでしょう。この記事では、TransitionGroup 内の「移動アニメーション」の使用方法について説明します。
TransitionGroup の「移動アニメーション」についてまだよく理解していないかもしれませんので、ここで簡単に説明します。通常、.[name]-move
クラスに遷移のスタイルを指定する必要があります。例えば、transition: all 0.5s ease;
のように指定します。これにより、TransitionGroup 内の要素の位置が変更された場合、Vue は位置が変更された要素を古い位置から新しい位置にスムーズに移動させようとします。もちろん、Vue は要素の追加や削除にも遷移効果をサポートしています。[name]-enter-from
と[name]-leave-to
クラスにスタイルを指定するだけで、要素の追加や削除の遷移効果を実現できます。ただし、これはこの記事の重点ではないため、詳細な説明は省略します。
私も以前は、スタイルを書いて問題なく終わらせていました。
しかし、グループチャットで「要素に常に!important
を含む優先度の高いtransition
スタイルを追加すると、遷移が機能しなくなる」という情報を聞いたとき、私は固まってしまいました😧。当時の私にとっては理解が難しいことでした。遷移時に Vue が要素に[name]-move
を追加するため、なぜ要素に優先度の高いtransition
スタイルを事前に追加すると、遷移が機能しなくなるのでしょうか?
ソースコードを見てみる#
TransitionGroup.tsには、TransitionGroup に関連するコードが記述されています。
初期化フェーズ#
Vue はレンダリング関数内で、子要素の配列children
をsetup
関数のスコープ内の変数prevChildren
に代入しています (L113)。同時に、prevChildren
を for ループで走査し、各子要素の位置情報をpositionMap
に保存しています(古い位置)(L135)。
この初期化フェーズで私たちが議論している内容に関連するのは、これら 2 つの箇所です。prevChildren
の代入により、Vue は更新フェーズで子要素の参照を取得し、関連する操作を行うことができます。また、positionMap
により、Vue は後続の操作で要素の元の位置を取得することができます。
ここでのpositionMap
は WeakMap です。Vue は要素オブジェクトをキーとして使用し、要素が破棄された後にpositionMap
内の対応する要素の位置情報が適時に自動的に解放されるようにしています。
更新フェーズ#
TransitionGroup 内の子要素が変更されると、登録されたonUpdated
コールバックが呼び出されます。同時に、Vue は遷移に必要なほとんどの操作を完了します。
まず、Vue は forEach を使用して再度各要素の位置情報を取得し、newPositionMap
に保存します (L72)。この時点で、要素の新しい位置と古い位置がnewPositionMap
とpositionMap
にそれぞれ保存されます。私たちが行う必要があるのは、要素を古い位置から新しい位置にスムーズに移動させることです。
次に、重要な部分です:更新フェーズである以上、要素は既に新しい位置に存在しているはずです。なぜなら、要素が新しい位置に移動している間に遷移が行われるはずがないからです。したがって、私たちがするべきことは、要素を単に古い位置から新しい位置に移動させるだけではありません。代わりに、既に新しい位置にある要素を再び古い位置に戻し、その後新しい位置にスムーズに戻すことです。では、Vue はどのようにこれを実現しているのでしょうか?
L73のコードは次のようになっています。
const movedChildren = prevChildren.filter(applyTranslation)
Vue はapplyTranslation
関数を使用して移動が必要な子要素の配列movedChildren
をフィルタリングしています。関数と変数の命名を見ると、フィルタリングの際に Vue が要素に対していくつかの操作を行っていることがほぼ確実です。
function applyTranslation(c: VNode): VNode | undefined {
const oldPos = positionMap.get(c)!
const newPos = newPositionMap.get(c)!
const dx = oldPos.left - newPos.left
const dy = oldPos.top - newPos.top
if (dx || dy) {
const s = (c.el as HTMLElement).style
s.transform = s.webkitTransform = `translate(${dx}px,${dy}px)`
s.transitionDuration = '0s'
return c
}
}
移動が必要な要素に対して、Vue は新しい位置と古い位置の差を計算し、Transform
プロパティを使用して要素を古い位置に戻します。特に注意すべきは、s.transitionDuration = '0s'
です。~ 後で考える必要がある~。
しかし、実際には、この時点で要素はまだ古い位置に戻っていません。ブラウザはスタイルの変更をレンダリングキューに追加し、即座にレンダリングされません。ここでブラウザのリフロー (reflow) に関連する知識が関係してきますので、関連する記事を検索して読むことをお勧めします。
要素を即座に古い位置に配置するためには、L73 でmovedPosition
を取得した後、Vue はforceReflow
関数を実行してリフローを強制的にトリガーします (L76)。forceReflow
関数の内容も非常にシンプルです (Transition.ts#L461)。
function forceReflow() {
return document.body.offsetHeight
}
ここでもフロントエンドの小ネタです🤯。ドキュメントのoffsetHeight
やoffsetWidth
を読み取ることで、ドキュメントのリフローがトリガーされます。これを理解するためには、レンダリングキューに変更があるのにリフローせずに直接ドキュメントの幅や高さを取得すると、取得する要素の幅や高さが古い情報になる可能性があるため、ブラウザは読み取り前にドキュメントをリフローする必要があるということです。
その後の作業は非常に簡単です。要素に[name]-move
クラスを追加し、以前に追加したtransitionDuration
とTransform
プロパティを削除するだけです。要素は自然に新しい位置にスムーズに戻るでしょう〜transitionend
イベントを監視して (L83-L93)、後処理を行います(遷移に関連するクラス名の削除など)。
ここまで読んだら、最初の問題を解決することができるようになりました。遷移効果を実現するには、要素が古い位置に正しく配置されていることを確認する必要があります。Vue では、要素がTransform
を使用して古い位置に配置されることを保証するために、遷移時間を 0 秒に設定し、強制的なリフローを行っています。しかし、人為的に追加された高優先度のtransition
プロパティにより、リフロー時に要素がすぐに古い位置に戻らないため、遷移効果が失われてしまいます。
私は TransitionGroup の関係ないコードを簡略化したデモも作成しましたので、興味があればご覧ください:https://codepen.io/DaiDR/pen/VwdMRxa