🔥

[Vue]`$emit`に`click`のようなネイティブに存在するイベントキーを組み合わせたときのVue2->3における挙動差異について

2022/08/30に公開

はじめに

MS社自身によるIE11サポートが(大体)終了となり、フロントエンド関連ライブラリのバージョンアップ対応に奔走している方が多いのではと思います。
この度vueアプリのメジャーバージョンの2->3対応を行っている中で個人的に気になった「$emitclickのようなDOMネイティブに存在するイベントキーを組み合わせた」ときの挙動の差異について調べてまとめてみました。

結果的に他の破壊的変更の理由や背景について知れたよい調査になりました。

この記事で伝えたいこと

  1. DOMネイティブに存在するイベントキー(clickなど)と同名のキーがemit()で指定された時の破壊的変更について
    1. vue2における挙動と.native修飾子について
    2. vue3における挙動
  2. つまるところvue3においてemitsオプションの指定はほぼ必須になった
  3. 何故このような変更が加えられたかRFCを見てみよう
  4. 私の感想

1. DOMネイティブに存在するイベントキーと同名のキーがemit()で指定された時の破壊的変更について

DOMネイティブに存在するイベントキー、例えばclick,change,focusなどがあります。
子コンポーネント側でconst onClick = () => emit('click');のようなイベントハンドラをバインドし、親側では<Comp @click="callAPI">のようにして使うコードはごくごく一般的だと思われますが、この挙動に2->3間で破壊的な変更があります。

1.1. vue2における挙動と.native修飾子について

vue2ではDOMネイティブイベントキーをemitで使っていようが関係ありませんでした。
どんなキー名でも子コンポーネントがemitしたキーに対応する親コンポーネントのイベントハンドラが発火していました。

ただし.native修飾子(vue native modifier)という機能とDOMネイティブイベントキーを組み合わせると、DOMネイティブなイベントにバインドすることができました。
以下native修飾子の有無の違いを知るサンプル実装です。

Preview上の2つのボタンの違いは@click.nativeがついているかどうかだけですが、イベントハンドラに渡される引数に違いがあることがわかります。

  • native修飾子なし: 「emit value」子コンポーネントがemitした時の第2引数が渡されている
  • native修飾子あり: 「[object PointerEvent]」DOMネイティブなEventオブジェクトが渡されている

native修飾子は子コンポーネントのルート要素のDOMネイティブなイベントにハンドラをバインドさせる機能だということが分かります。
これは子コンポーネントがemitの第2引数に何を渡そうが関係なく、またそもそも子コンポーネントがemitしたかどうかさえ関係ありません。

1.2. vue3における挙動

上記のnative修飾子はvue3では削除されました。

実を言うと私はこのドキュメントを見て初めてnative修飾子を知ることになるのですが、

At the same time, the new emits option allows the child to define which events it does indeed emit.

vue3では新しいemitsオプションが追加されたことによって、子のDOMネイティブイベントを拾うかどうかが選べるようになったようです。
vue3でのemitsオプションとは、簡単に言うとコンポーネント自身がどんなキー名をemitするか自己文書化する機能です。

RFCを見るとこの機能を追加した目的がわかります。自己文書化・実行時バリデーション・型推論・IDEサポート、そしてListener Fallthrough Controlが挙げられています。
Fallthroughについてはまた別のRFCも関係していて、vue3の破壊的変更のもとになっているようです。後述で掘り下げます。

以下emitsオプションの有無の違いを知るサンプル実装です。

Preview上の2つのボタンコンポーネントの違いは、emits: ['click']があるかないかだけです。

  • emitsオプションなし: ハンドラが2回実行されます。without emits, [object PointerEvent]
  • emitsオプションあり: vue2のnative修飾子なしと同じようにwith emitsだけが実行されます。

ここで分かることとしてはemitsオプションがないと ヤバいわよ! ということです。
イベントハンドラが2回実行される…APIが2回も呼ばれる?、トグルな処理が2回呼ばれて結局もとに戻る?…想像するだけで恐ろしいです😆

そしてvue2と同じ動作を目指したい場合emitsオプションを指定することで達成出来るわけですね。

2. つまるところvue3においてemitsオプションの指定はほぼ必須になった

emitsオプションの有無で動作が変わることがわかりました。
しかしあくまでこの挙動の違いはclickなどのDOMネイティブイベントに対応するキー名でないと起こりません。
closeとかでは発生しないんですね。
ならemitsオプションでそのコンポーネントのemitするキー名を列挙するのは必須なのでしょうか?
必要な時だけclickなどだけを列挙すればいいのでは?

...

個人的には全てのemitのキーを列挙する必要があると考えました。
clickemitsオプションに含めるけどcloseは含めない、というのは中途半端ですし、clickなどの名前を使わないということも難しいです。
よってDOMネイティブなイベントを拾いたい時以外は全てのemitのキーはemitsに列挙するべきと考えました。

RFCでも紹介されていたように、そのコンポーネントが何をemitするのか把握しやすく、型やIDEの恩恵を受けることもできます。
Vue3イチオシの機能という訳ですね。マイグレーション時の作業は必須ですが💦

3. 何故このような変更が加えられたかRFCを見てみよう💪

結論から申しますと、コンポーネントの属性の受け渡しをシンプルにしたいというところが関係しています。
これは前述のListener Fallthrough Controlも関係しています。

↓のRFCに詳しく説明が書いてあります。

https://github.com/vuejs/rfcs/blob/master/active-rfcs/0031-attr-fallthrough.md#v-on-listener-fallthrough

こちらの動機を簡単にまとめますと、

  • vueではpropsv-on(@)を指定する以外にも属性を受け渡す機能がある
  • しかしvue2では属性の暗黙的受け渡しにはルールがある
  • 暗黙的受け渡しを回避するオプションinheritAttrs: falseもある
  • これらの挙動には以下の矛盾がある
    • inheritAttrs: falseclass,styleには影響しない
    • 暗黙受け渡しはv-on(@)を含んでいない。しかしnative修飾子を使うことで受け渡しできる
    • class,style,v-on(@)$attrsに含まれていない。ので高次コンポーネント作る時不便
    • Functionalコンポーネントには暗黙的受け渡し機能はない

(ここから意訳)...これらのルールは複雑なので全属性受け渡そう。そして暗黙的受け渡しが有効かどうかはinheritAttrsオプションで制御しよう。
となりました。

全属性つまりv-on(@)も受け渡されるためnative修飾子も必要なくなります。
emitsオプションは指定されたイベントキーのv-on(@)のみを受け渡される属性から外す機能だったんですね。
そしてvue2までは$listenerに全てのv-on(@)が含まれていましたが、v-on(@)$attrsに含まれるようになったため、$listenerも削除されました。

暗黙的受け渡しルールをシンプルにするための変更だということが分かりました👍

4. 私の感想

最初vue3でemitsオプションなしで2回イベントが発火したのを見たときは 「ぶっこんでくれたねぇ😇」 と思いました。

vue2と同じ書き方のままだと動作時深刻な不具合を引き起こす恐れがあることは、(Vueコミュニティが標榜しているわけではないので難癖になってしまいますが)Don’t break the web安全側に倒すという考えではないようですね。
逆を言えばそれだけの破壊的変更がありながらもVueコミュニティが強く改善したい点であったとも言えるかなと思いました。

自分自身記事を書くにあたって、動作を確認したりRFCを確認することでシンプルに倒したことによって使い勝手が良くなったと感じました。感謝🙏

以上

Discussion