[Vue]`$emit`に`click`のようなネイティブに存在するイベントキーを組み合わせたときのVue2->3における挙動差異について
はじめに
MS社自身によるIE11サポートが(大体)終了となり、フロントエンド関連ライブラリのバージョンアップ対応に奔走している方が多いのではと思います。
この度vueアプリのメジャーバージョンの2->3対応を行っている中で個人的に気になった「$emit
にclick
のようなDOMネイティブに存在するイベントキーを組み合わせた」ときの挙動の差異について調べてまとめてみました。
結果的に他の破壊的変更の理由や背景について知れたよい調査になりました。
この記事で伝えたいこと
- DOMネイティブに存在するイベントキー(
click
など)と同名のキーがemit()
で指定された時の破壊的変更について- vue2における挙動と
.native
修飾子について - vue3における挙動
- vue2における挙動と
- つまるところvue3において
emits
オプションの指定はほぼ必須になった - 何故このような変更が加えられたかRFCを見てみよう
- 私の感想
emit()
で指定された時の破壊的変更について
1. DOMネイティブに存在するイベントキーと同名のキーがDOMネイティブに存在するイベントキー、例えばclick
,change
,focus
などがあります。
子コンポーネント側でconst onClick = () => emit('click');
のようなイベントハンドラをバインドし、親側では<Comp @click="callAPI">
のようにして使うコードはごくごく一般的だと思われますが、この挙動に2->3間で破壊的な変更があります。
.native
修飾子について
1.1. vue2における挙動と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
オプションを指定することで達成出来るわけですね。
emits
オプションの指定はほぼ必須になった
2. つまるところvue3においてemits
オプションの有無で動作が変わることがわかりました。
しかしあくまでこの挙動の違いはclick
などのDOMネイティブイベントに対応するキー名でないと起こりません。
close
とかでは発生しないんですね。
ならemits
オプションでそのコンポーネントのemit
するキー名を列挙するのは必須なのでしょうか?
必要な時だけclick
などだけを列挙すればいいのでは?
...
個人的には全てのemit
のキーを列挙する必要があると考えました。
click
はemits
オプションに含めるけどclose
は含めない、というのは中途半端ですし、click
などの名前を使わないということも難しいです。
よってDOMネイティブなイベントを拾いたい時以外は全てのemit
のキーはemits
に列挙するべきと考えました。
RFCでも紹介されていたように、そのコンポーネントが何をemit
するのか把握しやすく、型やIDEの恩恵を受けることもできます。
Vue3イチオシの機能という訳ですね。マイグレーション時の作業は必須ですが💦
3. 何故このような変更が加えられたかRFCを見てみよう💪
結論から申しますと、コンポーネントの属性の受け渡しをシンプルにしたいというところが関係しています。
これは前述のListener Fallthrough Control
も関係しています。
↓のRFCに詳しく説明が書いてあります。
こちらの動機を簡単にまとめますと、
- vueでは
props
やv-on(@)
を指定する以外にも属性を受け渡す機能がある - しかしvue2では属性の暗黙的受け渡しにはルールがある
- 暗黙的受け渡しを回避するオプション
inheritAttrs: false
もある - これらの挙動には以下の矛盾がある
-
inheritAttrs: false
はclass
,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