三咲智子 Kevin Deng

三咲智子 Kevin Deng

Gen Z | Full Stack Developer 🏳️‍🌈
github
twitter
twitter

Vue 3.3 の主要な新機能の詳細解説

前言#

こんにちは、ここは三咲智子、Vue コアチームのメンバーです。Vue 3.3 のこのリリースは主に DX(開発者体験)を改善するためのもので、いくつかのシンタックスシュガーとマクロ、そして TypeScript の改善が追加されました。

  • ジェネリックコンポーネント
  • SFC(単一ファイルコンポーネント)で外部 TS タイプをインポート
  • defineSlots でスロットのタイプを定義
  • defineEmits より便利なシンタックス
  • defineOptions コンポーネントオプションの定義
  • (実験的)リアクティブな props デストラクチャリング
  • (実験的)defineModel シンタックスシュガー
  • リアクティビティトランスフォームの廃止

昨年、Vue に参加したばかりの頃、私は Vue 3 に PR を貢献していましたが、最近やっと Vue 3.3 に実装されました。Vue 3.3 は Vue Macros から合計五六個の機能を吸収しました。今日は私が貢献した部分についてお話しします。

defineOptions マクロ#

前情提要#

<script setup> がない頃を思い出してみてください。propsemits を定義するために、setup と同じレベルのプロパティを簡単に追加できました。しかし、<script setup> を使うようになってからは、そうすることができなくなりました —— setup プロパティはもう存在しないので、同じレベルのプロパティを追加することはできません。この問題を解決するために、definePropsdefineEmits という二つのマクロを導入しました。

しかし、これは propsemits の二つのプロパティだけを解決しました。コンポーネントの nameinheritAttrs または他のカスタムプロパティを定義する必要がある場合、最初の方法に戻らなければなりません —— 普通の <script> タグを追加することです。これでは二つの <script> タグが存在することになります。私にとっては、これは受け入れられません。

  • 二つの script タグは ESLint プラグインや Volar に予期しない問題を引き起こす可能性があります;
  • 両方の script タグにインポート文が存在する場合、Vue は奇妙な特別処理を行います;
  • DX(開発者体験)にとっては、面倒で理解しにくいです。

现状#

そこで、Vue 3.3 では新たに defineOptions マクロを導入しました。名前の通り、主に Options API のオプションを定義するために使用されます。defineOptions を使用して任意のオプションを定義できますが、propsemitsexposeslots は除外されます(これらは defineXXX を使用して実現できます)。<template> タグを使わずに、defineOptions 内で h 関数や JSX を使ってレンダリング関数 render を記述することもできます(もちろん、これは推奨されません)。

🌰 例子#

<script setup>
defineOptions({
  name: 'Foo',
  inheritAttrs: false,
  // ... その他のカスタムプロパティ
})
</script>

Vue SFC Playground

背后的故事#

この機能の起源は、Element Plus のコンポーネントを <script setup> にリファクタリングしているときに生まれました。コンポーネントライブラリとして、私たちはカスタムコンポーネントの名前(name プロパティ)をデフォルトのファイル名ではなく、指定したいと考えました。しかし、元の書き方に戻りたくはなかったので、プラグイン(夢の始まり 🤣)unplugin-vue-define-options を作成しました。何度も修正を重ねて、現在の Vue 3.3 にある defineOptions マクロが確定し、実装されました。

提升静态常量#

この機能は SFC(単一ファイルコンポーネント)コンパイラの最適化です。script 部分に hoistStatic オプションが新たに追加されました。

模板中的 hoistStatic#

template 下の hoistStatic は基本的に同じです。Vue コンパイラには最適化があり、静的な要素ノードをトップレベルスコープに昇格させることができます。これにより、コードが読み込まれるときに一度だけ実行され、render 関数が実行されるたびに繰り返し実行されることはありません(極端な場合には欠点もあります)。

例を見てみましょう 🌰。

<template>
  <div id="title">Hello World</div>
</template>

Vue SFC Playground

このコードは以下の JavaScript にコンパイルされます(重要でないコードは省略されています)。

const _hoisted_1 = { id: 'title' }
function render(_ctx, _cache) {
  return _openBlock(), _createElementBlock('div', _hoisted_1, 'Hello World')
}

ここで、_hoisted_1 変数はコンパイラによって意図的にトップレベルに昇格されたコードです。この機能を無効にすると、render 関数内で実行されます。

script タグの hoistStatic#

Vue 3.3 以前は、上記の機能だけでした。Vue 3.3 では、同様の最適化を行いました —— もし定数の値がプリミティブ値(基本型、すなわち string, number, boolean, bigint, symbol, null, undefined)であれば、その定数宣言はトップレベルに昇格されます。(注:symbol は現在実装されていません)

これらの型の定数は変更できないため、どこで宣言されても効果は同じです。

作用#

この機能は性能の最適化だけでなく、さらに有用な点があります。Vue 3.3 以前は、マクロを使用する際に <script setup> ブロック内で定義された変数を渡すことができませんでした。以下の例を見てみましょう。

<script setup>
const name = 'Foo'
defineOptions({
  name,
})
</script>

Vue SFC Playground

エラーが発生します。

[@vue/compiler-sfc] `defineOptions()` in <script setup> cannot reference locally declared variables because it will be hoisted outside of the setup() function. If your component options require initialization in the module scope, use a separate normal <script> to export the options instead.

これは、前述の理由によるもので、definePropssetup と同じレベルの props プロパティを追加し、name 定数は setup 関数内で宣言されているためです。関数外でその内部の変数を参照することはできません。以下のコードは間違いなくエラーです。

const __sfc__ = {
  props: [propName],
  setup(__props) {
    const propName = 'foo'
  },
}

Vue 3.3 以降、4 行目の propName は最初の行に昇格されるため、コードは完全に理解可能になります。この機能はデフォルトで有効になっています。通常、開発者はこの機能が存在することを意識する必要はありません。

背后的故事#

この機能を開発した理由も前のものと似ています。Element Plus では defineOptionsname を設定した後、特定の条件下で例外をスローする必要がありました。例外にはコンポーネントの名前が必要で、ユーザーがデバッグしやすくするためです。

<script setup>
const name = 'ElButton'
defineOptions({
  name,
})
// ...
if (condition) {
  throw new Error(`${name}: something went wrong.`)
}
</script>

そのため、重複したコードを避け、コンポーネント名を定数として抽出し、defineOptions と例外をスローする際にそれを参照したいと考えました。

defineModel マクロ#

动机#

このマクロは純粋なシンタックスシュガーです。Vue 3.3 以前は、双方向バインディングの prop を定義するのは非常に面倒なことでした。

<script setup lang="ts">
const props = defineProps<{
  modelValue: number
}>()

const emit = defineEmits<{
  (evt: 'update:modelValue', value: number): void
}>()

// 値を更新
emit('update:modelValue', props.modelValue + 1)
</script>

props を定義し、次に emits を定義する必要があります。その中には多くの重複したコードがあります。この値を変更する必要がある場合は、手動で emit 関数を呼び出す必要があります。

私は考えました:このステップを簡素化するために関数(マクロ)をラップしない理由は何ですか?それが defineModel マクロの誕生です。

🌰 例子#

<script setup>
const modelValue = defineModel()
modelValue.value++
</script>

上記の煩雑な 7 行のコードは、Vue 3.3 では 2 行に短縮できます!

useVModel 的区别#

VueUse には useVModel 関数もあり、類似の効果を実現できます。なぜこのマクロを導入する必要があるのでしょうか?

これは、VueUse が単に propsemit 関数を組み合わせて Ref にラップしただけであり、コンポーネントの propsemits を定義することはできないからです。つまり、開発者は依然として手動で definePropsdefineEmits を呼び出す必要があります。

背后的故事#

😛 この背後には特にストーリーはありません。単純に書くのが面倒だと感じたからです。最初は Vue MacrosdefineModel のマクロを作成しました(現在は defineModels に改名され、公式と区別されています)。使ってみたところ、なかなか良い結果が得られました。

在 SFC 导入外部类型#

背景#

Vue 3.2 のリリース以来、Vue には最も注目されている issue がありました ——defineProps で外部の型を使用する方法

この問題を解決するために、私たちの前には二つの道がありました。

  • Vue SFC コンパイラが TypeScript コンパイラを呼び出し、最終的な型を計算し、それがどの型を含むかを特定する(String, Number, Boolean, Function など)。
  • 簡易的な TypeScript 分析器を自分たちで実装し、大部分のシナリオを解決する。

ご存知の通り、TypeScript の型体操は非常に恐ろしいものです。この issue を完璧に解決するためには、Vue の SFC コンパイラが TypeScript コンパイラのようにすべての型を解析し、計算する必要があります。最初のアプローチは良いですが、巨大で肥大化した TypeScript コンパイラに依存することになり、ビルド時間が大幅に遅くなるという大きな欠点があります。

最終的に、Vue Macros では第二のアプローチを選択しました。そのため、現在はマクロ内で複雑な型を渡すことは適していません。しかし、これは今後徐々に改善される予定です。

Vue 3.3 与 Vue Macros#

Vue Macros がこの簡易分析器を実装した後、Vue コアも同様の実装を行いました。

しかし、現時点では依然として違いがあります。Vue Macros のイテレーション速度は Vue コア自体を遥かに上回っており、Vue Macros は現在、より多くの奇妙なシンタックスをサポートしていますが、Vue 3.3 では一部のシンタックスがまだサポートされていません。

したがって、将来的に Vue が解析できない型に遭遇した場合は、Vue Macros を試してみると良いでしょう。もし Vue Macros でもうまくいかない場合は、最小限の再現コードを添えて、Vue Macros リポジトリに issue を提起するか、よりシンプルで二次推論を避けるシンタックスを試してみてください。

defineSlots マクロ#

背景#

Vue 3.3 では defineSlots マクロが新たに追加されました。defineSlots を使用して、自分でスロットの型を定義できます。このマクロはシンプルなコンポーネントではほとんど使われませんが、複雑なコンポーネントには非常に役立ちます。特に、この機能はジェネリックコンポーネントと一緒に使用する場合や、Volar が型を正しく推測できない場合に手動で指定することができます。

🌰 例子#

<script setup lang="ts">
const slots = defineSlots<{
  default(props: { foo: string; bar: number }): any
}>()
</script>

私たちは default コンポーネントのスロットスコープの型を手動で定義しました。

🌰 真的例子#

例えば、ページネーターコンポーネントがある場合、スロットを通じて具体的な item のレンダリング方法を制御できます。

<script setup lang="ts" generic="T">
// 子コンポーネント Paginator
defineProps<{
  data: T[]
}>()

defineSlots<{
  default(props: { item: T }): any
}>()
</script>
<template>
  <!-- 親コンポーネント -->
  <Paginator :data="[1, 2, 3]">
    <template #default="{ item }">{{ item }}</template>
  </Paginator>
</template>

data パラメータを渡すと、それは number[] であるため、itemnumber として推測されます。item の型は data パラメータに渡された型に応じて変化します。

defineEmits 更便捷的语法#

この機能も純粋なシンタックスシュガーです。

例子#

<script setup lang="ts">
const emits = defineEmits<{
  (evt: 'update:modelValue', value: string): void
  (evt: 'change'): void
}>()

// ⬇️ Vue 3.3 以降
const emits = defineEmits<{
  'update:modelValue': [value: string],
  'change': []
}>()
</script>

Vue 3.3 以前は、もう少し文字を多く書く必要がありましたが、今は省ける部分は省けます。

废弃 Reactivity Transform 语法糖#

早い段階で、Vue チームは Reactivity Transform シンタックスを廃止することを発表しました。具体的には、Vue 3.3 で廃止(deprecated)され、使用時に警告が表示され、Vue 3.4 では完全に削除される予定です。

私個人としては、Reactivity Transform にはまだ使い道があると思っています。

公式に廃止されましたが、その機能は Vue Macros に移動されました。これは、古いシンタックスに急いで移行する必要がなく、プラグイン を使用することで、正常に使用し続け、今後のバグ修正も受けられることを意味します。

具体的に廃止された理由については、こちらのコメントを読むことができます。

后记#

実際のところ、Vue がコミュニティからの提案や提案を受け入れる姿勢を見られることに非常に嬉しく思っています。これが私が Vue 3.3 に貢献した内容です。その他の機能については、Vue ブログの記事を読むことができます。

P.S. もし誰かがこの記事を英語に翻訳してくれるなら、翻訳が完了したら sxzz/articles リポジトリに内容を提出していただけると大変感謝します!

关于 Vue Macros#

Vue Macros は現在、Vue 公式とは独立したプロジェクトです。Vue 公式とは異なり、その目的は異なる可能性を探求することです。

私は、たとえそのアイデアが成熟していなくても、より過激な考えを見たいと思っています。私たちは Vue Macros でさらに実験を行い、成熟したら公式リポジトリに統合することを試みることができます。

現在、Vue Macros は私一人でメンテナンスしており、コミュニティからの仲間の参加をもっと引き寄せたいと思っています! 💕

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。