正式リリース前に総予習!! Vue3の変更点まとめ

[8月上旬に正式リリース予定](https://github.com/vuejs/rfcs/issues/183)とされているVue3の変更点をいち早く理解できるように概要をまとめてみました。それぞれの項目ごとに[vuejs/rfc](https://github.com/vuejs/rfcs)又は[Vue3 Document](https://v3.vuejs.org/)へのリンクを貼っているので索引的に使ってもらえると嬉しいです。

この記事は以下バージョン時点の情報です。

- [vue-next v3.0.0-rc.5](https://github.com/vuejs/vue-next/releases/tag/v3.0.0-rc.5)

# Composition APIの追加
- [rfcs/0013-composition-api.md at master · vuejs/rfcs](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md)
- [Composition API RFC | Vue Composition API](https://vue-composition-api-rfc.netlify.app/)

おそらく一番大きな目玉となる変更はこちら。Composition APIという新しいAPIが追加されます（[Plugin](https://github.com/vuejs/composition-api)でVue2系でも使用可能です）。
Composition APIはコンポーネントのロジックの柔軟な再構成を可能にする関数ベースのAPI群です。
なお、Vue2系のOptions APIのサポートも継続されるので、Vue2系からのVue3へのバージョンアップ時にComposition APIに書き換える必要はありません。


```vue
<!-- Vue3 Composition API -->
<template>
  <form>
    <input type="text" v-model="formState.name" />
    <input type="text" v-model="formState.value" />
    <p>{{ contentLength }}</p>
    <button @click="submit" type="submit">submit</button>
  </form>
</template>

<script lang="ts">
import { defineComponent, computed, ref, onMounted, reactive } from "vue";

export default defineComponent({
  props: {
    maxContentLength: {
      type: String,
      required: true
    }
  },
  setup(props, context) {
    // reactive data
    const formState = reactive({
      name: '',
      content: ''
    })

    // computed
    const contentLength = computed(() => `${formState.content.length} / ${props.maxContentLength}`)

    // emit
    const submit = () => context.emit('submit', formState)

    // lifecycle hook
    onMounted(() => {
      // ....
    })

    return {
      formState,
      contentLength,
      submit
    };
  }
});
</script>
```

Composition APIについてはこちらの記事でより詳細にまとめています。

[先取りVue 3.x !! Composition API を試してみる - Qiita](https://qiita.com/ryo2132/items/f055679e9974dbc3f977)
[Vue Composition APIで使えるリアクティブ関連のAPI一覧 - Qiita](https://qiita.com/ryo2132/items/6dc51ede8082dea75812)


# Teleportの追加
- [rfcs/0025-teleport.md at master · vuejs/rfcs](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0025-teleport.md)

Teleportは定義したコンポーネントが属するDOMツリーとは別の場所に、まるでテレポートしたかのようにコンポーネントを移動できる機能です。Vue2系でもサードパーティのプラグイン[LinusBorg/portal-vue](https://github.com/LinusBorg/portal-vue)で実現されていました。

以下のようにモーダル表示をハンドルするコンポーネントで`<teleport>`を使うと、`to`で指定したDOM要素に内部のコンポーネントを移動できます。

```vue
<template>
  <div class="container">
    <button @click="toggleModal">toggle modal</button>
    <teleport to="#teleport-target">
      <MyModal v-if="isVisible"/>
    </teleport>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "vue";

export default defineComponent({
  setup() {
    const isVisible = ref(false);

    const toggleModal = () => {
      isVisible.value = !isVisible.value; };
    return {
      isVisible,
      toggleModal
    };
  }
});
</script>
```

`to`で指定できるDOM要素は自身のVueインスタンスをマウントしたDOM要素以外も指定できます。

```html
<html lang="ja">
<body>
  <div id="app"></div> <!-- VueがマウントされるDOM -->
  <div id="teleport-target"></div> <!-- teleportで指定されているDOM。ここにMyModalが表示される-->
  <script src="/dist/main.js"></script>
</body>
</html>
```

`teleport`を利用すると、いままでCSSの`z-index`で調整していたDOMの重なり順制御を、宣言的に制御できるようになり`z-index`の指定に悩まされることはなくなります。
Vue3を入れたらすぐに使いたいと思える実用的な機能ですね。

こちらの記事でも詳細をまとめています。
[Vue.js 3.0 の新機能を試す。 〜 Teleport 編〜 - Qiita](https://qiita.com/ryo2132/items/a620755b04294ffabde6)

# Fragmentsの追加
- [Fragments | Vue.js](https://v3.vuejs.org/guide/migration/fragments.html)
[rfcs/0031-attr-fallthrough.md at master · vuejs/rfcs](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0031-attr-fallthrough.md)

FragmentsはVue2系では実現できなかったmulti-root nodeのコンポーネントを可能にするものです。
Vue2系ではコンポーネントのroot要素は必ず1つという制約がありました。

```vue
<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>
```

Vue3ではFragmentsのおかげでその制約がなくなり、root要素に複数の要素を記載できます。冗長なdivでのラップなどが不要になります。簡潔で良いですね。

```vue
<template>
  <header>...</header>
  <main>...</main>
  <footer>...</footer>
</template>
```

ただ、注意点としてコンポーネントに直接Class指定をしていると、今まで自動的にroot要素にClassを当てられたのですが、それがfragmentsの場合は明示的な指定が必要になるというのがあります。


# Suspenseの追加（実験的）
- [rfcs/0026-async-component-api.md at master · vuejs/rfcs](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0026-async-component-api.md)

Suspenseは非同期処理が解決されるまでフォールバックコンテンツ（例えばLoading中アイコン）を表示してくれる特別なコンポーネントです。いままで、`v-if="loading === true"` などの状態変数を使って制御していたものを、状態変数を使わずに簡潔に書くことができます。

Suspenseはまだ実験的な機能で、Vue3.0の段階では仕様が確定しておらず今後変更が入ることも考えられます。Vue 3.1での正式リリースを見越しているとのことです。

```vue
<template>
  <Suspense>
    <template #default>
      <AsyncComponents/> <!-- 非同期処理を実行するコンポーネント -->
    </template>
    <template #fallback>
      loading...
    </template>
  </Suspense>
</template>
```

Suspenseの詳細はこちらにまとめています。
[Vue.js 3.0 の新機能を試す。 〜 Suspense 編〜 - Qiita](https://qiita.com/ryo2132/items/7225d60768a15dabfb9f)

# v-modelの仕様変更
- [rfcs/0011-v-model-api-change.md at master · vuejs/rfcs](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0011-v-model-api-change.md)

v-modelの機能が拡張され、ひとつの要素に複数のv-modelの設定が可能になります。
Vue2系のv-bind.syncで実現していたようなことがv-modelで実現できるようになるイメージですね。この機能によってv-bind.syncは代替されるので利用できなくなります。

以下v-modelの複数バインドの例です。
`v-model:xxxx`のxxxのところで明示的にバインドするプロパティを指定しています。

```vue
<!-- 親コンポーネント -->
<template>
  <UserForm
   v-model:name="name"
   v-model:message="message"
  />
</template>
```

```vue
<!-- UserFormコンポーネント -->
<template>
  <form>
   <label>name</label>
   <input 
    type="text"
    @input="$emit('update:name', $event.target.value)"
   >
   <label>message</label>
   <input 
    type="text"
    @input="$emit('update:message', $event.target.value)"
   >
  </form>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  props: {
    name: {
      type: String,
      default: ''
    },
    message: {
      type: String,
      default: ''
    }
  },
  setup() { return {} }
})
</script>

```

また、modifiersの機能が拡張され、`.lazy`, `.number`, `trim`のデフォルトのmodifiers以外に独自に,modifiersを定義できるようになりました。
以下はcapitalizeというmodifiersを設定している例です。`v-model:name.capitalize=`とすることで入力値にcapitalizeを実行しています。

```vue
<!-- 親コンポーネント -->
<template>
  <form>
    <MyName  v-model:name.capitalize="form.name"/>
    <!-- ... -->
  </form>
</template>
```

```vue
<!-- MyNameコンポーネント -->
<template>
  <input
    :value="name"
    @input="updateName"
    type="text"
    name="name"
  />
</template>

<script>
import { defineComponent } from "vue";

export default defineComponent({
  props: {
    name: {
      type: String,
      default: ''
    },
    nameModifiers: {
      default: () => ({})
    }
  },
  setup (props, { emit }) {
    const updateName = event => {
      let val = event.target.value
      if (props.nameModifiers.capitalize) {
        val = val.charAt(0).toUpperCase() + val.slice(1)
      }

      emit('update:name', val)
    }
 
    return {
      updateName
    }
  }
}
</script>
```


# scoped CSSの仕様変更
- [rfcs/0023-scoped-styles-changes.md at master · vuejs/rfcs](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0023-scoped-styles-changes.md)

scoped CSSで使える擬似クラスの仕様変更・機能追加が行われます。
まず1つ目が`::v-deep()`でこれは、今まで`/deep/`として設定していた、子コンポーネントにスタイルを適用するための擬似クラスです。
`/deep/`の記法がDEPRECATEDとなり、新たに`::v-deep()`が追加されています。

```css
::v-deep(.foo) {}
```
```css
/* コンパイル後 */
[v-data-xxxxxxx] .foo {}
```

2つ目が、`::v-slotted()`で、これはslotで受け取った親コンポーネントの要素にスタイルを適用するためのものです。Vue3からデフォルトではslotで受け取った要素は、子コンポーネントのスタイルが適用されなくなるため、こちらが追加されました。

```css
::v-slotted(.foo) {}
```
```css
/* コンパイル後 */
.foo[v-data-xxxxxxx-s] {}
```

最後が、`::v-global()`で、これはscoped CSSの中でグローバルなスタイルの定義を可能とするものです。この擬似要素を設定したクラスには`[v-data-xxxx]`が付与されないため、Scopedではなくグローバルに適用されます。

```css
::v-global(.foo) {}
```
```css
.foo {}
```


# $attrsの仕様変更
- [rfcs/0031-attr-fallthrough.md at master · vuejs/rfcs](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0031-attr-fallthrough.md)

`$attrs`の機能が拡張され、今まで`$listeners`で参照していたネイティブイベントや別管理であったclass、styleも全て包括して`$attrs`から取得できるようになりました。

```vue
<MyButton 
  @click="handleClick" 
  @custom="handleCustom" 
  v-model="value"
  type="button"
  class="btn"
/>
```

```ts
// MyButton.vue
$attrs: {
  class: 'btn'
  type: 'button',
  onClick: handleClick,
  onCustom: handleCustom,
  'onUpdate:modelValue': () => { value = payload },
}
```

そのため、`$listeners`はDEPRECATEDとなります。（実行時に警告がでます）
`$listeners`を使って透過的なラッパーコンポーネントを作っていた場合は、`$listeners`を削除し`$attrs`への書き換えが必要です。

```vue
<!-- Vue2系 -->
<input v-bind="$attrs" v-on="$liseners" />
```

```vue
<!-- Vue3 -->
<input v-bind="$attrs" />
```

# Global APIの仕様変更
- [rfcs/0009-global-api-change.md at master · vuejs/rfcs](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0009-global-api-change.md)

Vueの初期化処理が変わります。`createApp`でインスタンスを生成し、そのインスタンススコープで`use`や`mixin`などが適用されるようになります。
これによってテストケースでのグローバル設定の汚染を防いだり、同一ファイル内で別設定のVueインスタンスの初期化が容易になります。

```ts
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App) // インスタンスを生成

app.config.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */) // インスタンススコープで設定される
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)

app.config.globalProperties.customProperty = () => {}

app.mount(App, '#app')
```


# filterの廃止
- [rfcs/0015-remove-filters.md at master · vuejs/rfcs](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0015-remove-filters.md)

filterは、パイプ演算子`|`を使って関数適用をlinuxの標準入出力のパイプラインするものですがJavaScriptにはない独自構文で学習コストが増加する、構文解析を複雑にするという理由で削除されます。

filterの処理は通常の関数適用で代替できます。

```html
<!-- filter形式 -->
<p>{{ msg | uppercase | reverse }}</p>

<!-- 通常の関数適用形式 -->
<p>{{ reverse(uppercase(msg)) }}</p>
```

他の代替方法については[rfcs/0015-remove-filters.md#alternatives](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0015-remove-filters.md#alternatives)をご覧ください。


# Event Emitter系のAPIの廃止
- [rfcs/0020-events-api-change.md at master · vuejs/rfcs](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0020-events-api-change.md)

Event Emitter系のAPI（`$on`, `$off`, `$once`）が削除されます。
グローバルなイベント管理としてEvent Emitterを仕様している場合は書き換えが必要です。
Event管理の代替には[Mitt](https://github.com/developit/mitt)などの別のEvent Emitterライブラリの利用が推奨されています。

# Functional Componentの廃止
- [rfcs/0007-functional-async-api-change.md at master · vuejs/rfcs](https://github.com/vuejs/rfcs/blob/master/active-rfcs/0007-functional-async-api-change.md)
- [Functional Components | Vue.js](https://v3.vuejs.org/guide/migration/functional-components.html#_2-x-syntax)

Functional Componentはdata等の状態を持たない代わりにレンダリングパフォーマンスの大幅な改善が行えるコンポーネントです。
Vue3ではFunctional Componentを作る際に使用する`functional: true`のオプションや、`<template functional>`が削除されます。

Vue3では、通常のコンポーネントとFunctional Componentの性能差は大幅に縮小され、ほとんどのユースケースでは取るに足らないものとなったそうです。

SFC上でfunctionalを使用している場合は、functionalの属性を削除し、attrs、listnersの参照方法の修正が必要です。

```vue
<!-- Vue2 functional component -->
<template functional>
  <component
    :is="`h${props.level}`"
    v-bind="attrs"
    v-on="listeners"
  />
</template>

<script>
export default {
  props: ['level']
}
</script>
```

```vue
<!-- Vue3 -->
<template> <!-- functionalを削除 -->
  <component
    v-bind:is="`h${props.level}`"
    v-bind="$attrs" <!-- attrs及びlistnersは$attrsにまとめられる  -->
  />
</template>

<script>
export default {
  props: ['level']
}
</script>
```


# 終わりに
以上「正式リリース前に総予習!! Vue3の変更点まとめ」でした。
色々ワクワクするような新機能が追加されていてVue3のリリースが待ち通しいですね。
他にも諸々細かい点が変わってるのでより詳細な変更は[vuejs/rfc](https://github.com/vuejs/rfcs)をご確認ください。

# 参考
- [Learn Which Exciting Features Vue 3 Brings to The Table - Vue School Vue.js Tutorials](https://vueschool.io/articles/vuejs-tutorials/exciting-new-features-in-vue-3/)
- [まもなくやってくる Vue.js 3 - Speaker Deck](https://speakerdeck.com/kazupon/mamonakuyatutekuru-vue-dot-js-3)
- [rfcs/active-rfcs at master · vuejs/rfcs](https://github.com/vuejs/rfcs/tree/master/active-rfcs)
- [Introduction | Vue.js](https://v3.vuejs.org/guide/migration/introduction.html#breaking)
- [Learn Vue.js with our Courses | Vue Mastery](https://www.vuemastery.com/courses)