はじめに
2020年4月にVue3.0ベータ版がリリースされ、そのパフォーマンスの向上、フレンドリーなTSサポート、ESエクスポートの書き込み方法の書き換え、パッケージのサイズを減らすためにTree shakingを使用し、Composition API、Custom Renderer APIとその新機能を拡張するRECsドキュメントの改善には嬉しい驚きでした。もちろん、まだいくつかのフォローアップ作業があり、現在は安定版ではなく、正式にプロジェクトで使用されていますが、安定版後の四半期にも。
Vue 3.0が登場するのは時間の問題ですので、雨の日に備えて新機能を試してみてください!
デザインの動機
ロジックの組み合わせと再利用
コンポーネントAPIの設計が直面する中核的な問題の1つは、ロジックをどのように整理するか、そして複数のコンポーネントにまたがるロジックをどのように抽出して再利用するかです。現在Vue 2.xで利用可能なものをベースに、APIでロジックを再利用するための一般的なパターンがいくつかありますが、いずれも何らかの問題があります。これらのパターンには次のようなものがあります:
- Mixins
- 高次コンポーネント
- レンダーレス・コンポーネント(スコープ付きスロット/スコープ・スロットに基づくロジックをカプセル化したコンポーネント)
これらのモデルには次のような問題があります:
- テンプレート内のデータのソースが明確ではありません。例えば、コンポーネントが複数のmixinを使用している場合、あるプロパティがどのmixinから来たものなのか、テンプレートを見ただけではわからないことがあります。
Renderless Componentsパフォーマンス:どちらもHOCで、ロジックをカプセル化するためにコンポーネントインスタンスの入れ子を追加する必要があるため、不必要なパフォーマンスオーバーヘッドが発生します。
React HooksにインスパイアされたComposition APIは、上記のような問題のない新しいロジックの再利用ソリューションを提供します。 "composition function"関数ベースのAPIを使用することで、関連するコードを関数に抽出することができます - 関数は関連するロジックをカプセル化し、レスポンシブなデータソースとしてコンポーネントに公開する必要がある状態を返します。マウスの位置をリスニングするロジックをカプセル化するために結合された関数を使用する例を示します:
function useMouse() {
const x = ref(0)
const y = ref(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
window.removeEventListener('mousemove', update)
})
return { x, y }
}
// コンポーネントで関数を使う
const Component = {
setup() {
const { x, y } = useMouse()
// 他の関数と連携する
const { z } = useOtherLogic()
return { x, y, z }
},
template: `<div>{{ x }} {{ y }} {{ z }}</div>`
}
上の例でわかるように
- テンプレートに公開されている属性の出所は明らかです;
- 戻り値の名前は任意に変更できるので、名前空間の衝突はありません;
- 追加のコンポーネント・インスタンスの作成に伴うパフォーマンスの低下はありません。
型の派生
3.0の主な設計目標は、TypeScriptのサポートを強化することでした。関数ベースのAPIは、関数の引数、戻り値、ジェネリクスのTSサポートがすでに非常によく開発されているため、型派生に対して自然に友好的です。
梱包サイズ
関数ベースのAPI 各関数は、名前付きESエクスポートとして個別に導入できるため、ツリーシェイキングに適しています。未使用のAPIに関連するコードは、最終的なパッケージングの際に削除することができます。また、関数ベースのAPI用に書かれたコードは、より効率的に圧縮されます。なぜなら、セットアップ関数本体内のすべての関数名と変数名は圧縮できますが、オブジェクトとクラスのプロパティ/メソッド名は圧縮できないからです。
Composition API
レンダーファンクションAPIとスコープスロットのシンタックス以外はすべてそのまま、または互換性ビルドによって2.xと互換性が保たれます。
Vue 3.0は、Angularのような非互換性をもたらすスーパースパンバージョンではなく、2.xの互換性を向上させています。
@vue/composition-apiVue 3.0の新機能は、.NET Frameworkを導入することで、2.xでも利用することができます。
初期化プロジェクト
1、vue-cli3をインストールします。
npm install -g @vue/cli
2、プロジェクトの作成
vue create vue3
3、composition-apiをプロジェクトにインストールします。
npm install @vue/composition-api --save
@vue/composition-api 4、Vue.use()によって提供される機能を使用する前に、まずそれらをインストールする必要があります。
import Vue from 'vue'
import VueCompositionApi from '@vue/composition-api'
Vue.use(VueCompositionApi)
setup()
Vue3では、新しいコンポーネントオプションsetup()が導入され、初期化されたpropsでコンポーネントインスタンスが作成されるときに呼び出されます。 引数として初期プロップを受け取ります:
export default {
props: {
name: String
},
setup(props) {
console.log(props.name)
}
}
入力されるプロップはレスポンシブで、後続のプロップが変更されると、フレームワーク内で同期的に更新されます。しかし、ユーザーコードでは変更できません。
また、setup() は、2.x ライフサイクルの beforeCreate と created の後に実行されます:
export default {
beforeCreate() {
console.log('beforeCreate')
},
setup() {
console.log('setup')
},
created() {
console.log('created')
}
}
// 結果を印刷する
// beforeCreate
// setup
// created
setup()のthisは、もはやvueインスタンスオブジェクトではなく、undefinedです。setup()の2番目のパラメータはコンテキストパラメータで、2.xのthisで有用なプロパティをいくつか提供します。
export default {
setup(props, context) {
console.log('this: ', this)
console.log('context: ', context)
}
}
// 結果を印刷する
// this: undefined
// context: {
// attrs: Object
// emit: f()
// isServer: false
// listeners: Object
// parent: VueComponent
// refs: Object
// root: Vue
// slots: {}
// ssrContext: undefined
// }
data() と同様に、 setup() はテンプレートのレンダリングコンテキストに公開されるプロパティを持つオブジェクトを返すことができます:
<template>
<div>{{ name }}</div>
</template>
<script>
export default {
setup() {
return {
name: 'zs'
}
}
}
</script>
reactive()
vue 2.xのVue.observable()関数に相当するreactive()関数は、vue 3.xでレスポンシブなデータオブジェクトを作成するために提供されています。
データが直接変更された場合、テンプレートは更新レンダリングに反応しません:
<template>
<div>count: {{state.count}}</div>
</template>
<script>
export default {
setup() {
const state = { count: 0 }
setTimeout(() => {
state.count++
})
return { state }
}
}
// 1秒経ってもページに変化がない
</script>
reactive は、オブジェクトのプロパティが変更されたときにテンプレートがレンダリングを更新するレスポンシブなデータオブジェクトを作成します:
<template>
<div>count: {{state.count}}</div>
</template>
<script>
import { reactive } from '@vue/composition-api'
export default {
setup() {
const state = reactive({ count: 0 })
setTimeout(() => {
state.count++
}, 1000)
return { state }
}
}
// ページ番号が1秒で0から1に変わる
</script>
ref()
Javascriptでは、プリミティブ型は値であり、参照ではありません。関数が文字列変数を返した場合、文字列を受け取ったコードは値のみを取得し、元の変数に対するその後の変更を追跡することはできません。
<template>
<div>count: {{state.count}}</div>
</template>
<script>
import { ref } from '@vue/composition-api'
export default {
setup() {
const count = 0
setTimeout(() => {
count++
}, 1000)
return { count }
}
}
// ページに変更はない
</script>
つまり、ラッパーオブジェクト ref() のポイントは、関数間であらゆる型の値を参照渡しするためのコンテナを提供することです。これはReact HooksのuseRefに少し似ていますが、Vueのラッパーオブジェクトがレスポンシブなデータソースでもあるという違いがあります。このようなコンテナを使用すると、ロジックをカプセル化した複合関数内で、参照によって状態をコンポーネントに戻すことができます。コンポーネントがプレゼンテーションを担当し、複合関数が状態の管理を担当します。
ref() は値参照を返します。ラップされたオブジェクトのプロパティは .value だけです。ラップされたオブジェクトの値は、直接変更することができます。
<script>
import { ref } from '@vue/composition-api'
export default {
setup() {
const count = ref(0)
console.log('count.value: ', count.value)
count.value++ // ラップされたオブジェクトの値を直接変更する
console.log('count.value: ', count.value)
}
}
// 結果を印刷する:
// count.value: 0
// count.value: 1
</script>
ラッパー・オブジェクトがテンプレート・レンダリング・コンテキストに公開されるとき、あるいは、別のレスポンシブ・オブジェクトの中に入れ子にされるとき、それは自動的に内部値に展開されます:
<template>
<div>ref count: {{count}}</div>
</template>
<script>
import { ref } from '@vue/composition-api'
export default {
setup() {
const count = ref(0)
console.log('count.value: ', count.value)
return {
count // Wrapperオブジェクトの値プロパティの自動展開
}
}
}
</script>
また、reactive()によって作成されたオブジェクトのプロパティ値として、ref()ラッパー・オブジェクトを使用することも可能で、この場合も、プロパティ値のref()ラッパー・オブジェクトは、テンプレート・コンテキストで展開されます:
<template>
<div>reactive ref count: {{state.count}}</div>
</template>
<script>
import { reactive, ref } from '@vue/composition-api'
export default {
setup() {
const count = ref(0)
const state = reactive({count})
return {
state // Wrapperオブジェクトの値プロパティの自動展開
}
}
}
</script>
Vue 2.xでは、インスタンスの$refs属性は、テンプレート要素のref属性でマークされたDOMやコンポーネントの情報を取得するために使用されます。ref()ラッパーオブジェクトは、ページ要素やコンポーネントを参照するためにも使用できます;
<template>
<div><p ref="text">Hello</p></div>
</template>
<script>
import { ref } from '@vue/composition-api'
export default {
setup() {
const text = ref(null)
setTimeout(() => {
console.log('text: ', text.value.innerHTML)
}, 1000)
return {
text
}
}
}
// 結果を印刷する:
// text: Hello
</script>
unref()
引数が ref の場合はその値を返し、そうでない場合は引数そのものを返します。 val = isRef(val) ? val.value : val これは、.
isref()
値が ref オブジェクトであるかどうかを調べます。
toRefs()
レスポンシブオブジェクトを通常のオブジェクトに変換します。通常のオブジェクトの各プロパティは、レスポンシブオブジェクトのプロパティに対応する ref です。また、組み合わせ論理関数からレスポンシブオブジェクトを返したい場合は、toRefs を使用するのが効率的です。toRefs は、コンシューマーコンポーネントがレスポンスを失うことなく、返されたオブジェクトを分解/拡張できる API です:
<template>
<div>
<p>count: {{count}}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
import { reactive, toRefs } from '@vue/composition-api'
export default {
setup() {
const state = reactive({
count: 0
})
const increment = () => {
state.count++
}
return {
...toRefs(state), // 応答性を失うことなく分解する
increment
}
}
}
</script>
computed()
computed() は、computed プロパティの作成に使用され、computed() 関数の返り値は ref のインスタンスです。この値スキーマは読み取り専用です:
import { ref, computed } from '@vue/composition-api'
export default {
setup() {
const count = ref(0)
const plusOne = computed(() => count.value + 1)
plusOne.value = 10
console.log('plusOne.value: ', plusOne.value)
console.log('count.value: ', count.value)
}
}
// 結果を印刷する:
// [Vue warn]: Computed property was assigned to but it has no setter.
// plusOne.value: 1
// count.value: 0
また、get関数とset関数を持つオブジェクトを渡して、手動で変更可能な計算状態を作成することもできます:
import { ref, computed } from '@vue/composition-api'
export default {
setup() {
const count = ref(0)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 10
console.log('plusOne.value: ', plusOne.value)
console.log('count.value: ', count.value)
}
}
// 結果を印刷する:
// plusOne.value: 10
// count.value: 9
watchEffect()
watchEffect() は、副作用関数を監視します。入力された関数を即座に実行し、その依存関係をレスポンスよく追跡し、依存関係が変更された場合に関数を再実行します:
<template>
<div>
<p>count: {{count}}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
import { ref, watchEffect } from '@vue/composition-api'
export default {
setup() {
// refデータソースを監視する
const count = ref(0)
// 依存関係の変更に注意し、すぐに実行する
watchEffect(() => {
console.log('count.value: ', count.value)
})
const increment = () => {
count.value++
}
return {
count,
increment
}
}
}
</script>
リスナーを停止します。watchEffect がコンポーネントの setup() 関数またはライフサイクルフックで呼び出されると、リスナーはコンポーネントのライフサイクルにリンクされ、コンポーネントがアンインストールされると自動的に停止します。
場合によっては、この戻り値を明示的に呼び出してリスニングを停止することもできます:
<template>
<div>
<p>count: {{state.count}}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
import { reactive, watchEffect } from '@vue/composition-api'
export default {
setup() {
// リアクティブなデータソースを監視する
const state = reactive({
count: 0
})
const stop = watchEffect(() => {
console.log('state.count: ', state.count)
})
setTimeout(() => {
stop()
}, 3000)
const increment = () => {
state.count++
}
return {
state,
increment
}
}
}
// 3数秒後+1ボタンが印刷されなくなった
</script>
副作用の消去。観測されたデータソースが変更されると、実行された操作の副作用を消去する必要が生じることがあります。例えば、非同期操作の完了と同時にデータに変更が生じた場合、まだ待機している前の操作を取り消したいことがあります。このような状況に対処するため、watchEffect コールバックは、クリーンアップ処理を登録する関数を引数として受け取ります。この関数を呼び出すと、クリーンアップ関数が登録されます。クリーンアップ関数は、下位のケースで呼び出されます:
- サイドエフェクトが再実行されようとしているとき
- リスナーが停止している場合、またはコンポーネントをアンインストールする場合、ライフサイクルフック関数で watchEffect が使用されている場合)。
失敗コールバックをコールバックから返すのではなく、関数を渡して登録するのが良いアイデアである理由は、戻り値が非同期のエラー処理にとって重要だからです。
const data = ref(null)
watchEffect(async (id) => {
data.value = await fetchData(id)
})
非同期関数は暗黙的に Promise を返します - この場合、すぐに登録する必要があるクリーンアップ関数を返すことはできません。これに加えて、コールバックによって返されたPromiseは、`Vue'内部での非同期エラー処理に使用されます。
実際には、ある周波数以上の動作がある場合、リソースを節約するために最初に動作をキャンセルすることができます:
<template>
<div>
<input type="text"
v-model="keyword">
</div>
</template>
<script>
import { ref, watchEffect } from '@vue/composition-api'
export default {
setup() {
const keyword = ref('')
const asyncPrint = val => {
return setTimeout(() => {
console.log('user input: ', val)
}, 1000)
}
watchEffect(
onInvalidate => {
const timer = asyncPrint(keyword.value)
onInvalidate(() => clearTimeout(timer))
console.log('keyword change: ', keyword.value)
},
{
flush: 'post' // 'post' 'sync' 'pre'コンポーネントの更新
}
)
return {
keyword
}
}
}
// ユーザー入力の "アンチシェイク "効果を実装する。
</script>
watch()
watch API は、2.x の this と完全に同等です。watch .watch は、特定のデータソースをリッスンし、コールバック関数で副作用を実行する必要があります。デフォルトは遅延実行で、リッスン対象のソースが変更されたときにのみコールバックが実行されます。
watch() が受け取る最初のパラメータは "データ・ソース" と呼ばれ、以下のようなものがあります:
- 任意の値を返す関数
- ラップされたオブジェクト
- これら両方のデータソースを含む配列
2番目のパラメータはコールバック関数です。コールバック関数は、データソースが変更されたときにのみトリガされます:
watch(
// getter
() => count.value + 1,
// callback
(value, oldValue) => {
console.log('count + 1 is: ', value)
}
)
// -> count + 1 is: 1
count.value++
// -> count + 1 is: 2
上述したように、最初のパラメーターの "データ・ソース "は関数やラッパーの配列にすることができ、同時に複数のデータ・ソースをリッスンすることが可能です。同時に、watchとwatchEffectは、リスニングの停止や副作用のクリアなど、同じように動作します。ここでは、上記の "アンチシェイク "の例を使ってwatchを書き換える方法を説明します:
<template>
<div>
<input type="text"
v-model="keyword">
</div>
</template>
<script>
import { ref, watch } from '@vue/composition-api'
export default {
setup() {
const keyword = ref('')
const asyncPrint = val => {
return setTimeout(() => {
console.log('user input: ', val)
})
}
watch(
keyword,
(newVal, oldVal, onCleanUp) => {
const timer = asyncPrint(keyword)
onCleanUp(() => clearTimeout(timer))
},
{
lazy: true // デフォルトはfalseではない、つまり、最初のリスナーのコールバック関数が
}
)
return {
keyword
}
}
}
</script>
2.xの$watchとは異なり、watch()コールバックは作成時に一度だけ実行されます。デフォルトでは、watch() コールバックは常に現在のレンダラーがフラッシュされた後に呼び出されます。DOM は常に更新済みの状態になります。 この挙動はオプションでカスタマイズ可能です。
2.xのコードでは、mountedとwatcherコールバックで同じロジックを実行する必要があるのが一般的で、3.0のwatch()のデフォルトの動作は、このニーズを直接表現しています。
ライフサイクルフック機能
ライフサイクルフックは、onXXXファミリーの関数を直接インポートすることで登録できます。
import { onMounted, onUpdated, onUnmounted } from '@vue/composition-api'
const MyComponent = {
setup() {
onMounted(() => {
console.log('mounted!')
})
onUpdated(() => {
console.log('updated!')
})
onUnmounted(() => {
console.log('unmounted!')
})
},
}
これらのライフサイクル・フック登録関数は、setup() 中にのみ同期的に使用することができます。なぜなら、これらの関数は、現在のコンポーネント・インスタンスを見つけるために内部グローバル状態に依存しており、現在のコンポーネントがない状態で呼び出すとエラーになるからです。
コンポーネントのインスタンスコンテキストもライフサイクルフックの同期実行中に設定されるため、コンポーネントがアンインストールされると、ライフサイクルフック内で同期的に作成されたリスナーや計算状態も自動的に削除されます。
2.xのライフサイクル関数と新バージョンのコンポジションAPIとのマッピング:
- -> setup() を使用
- -> setup() の使用
- beforeMount -> onBeforeMount
- mounted -> onMounted
- beforeUpdate -> onBeforeUpdate
- update -> onUpdated
- beforeDestroy -> onBeforeUnmount
- beforeDestroy -> onBeforeUnmount
- errorCaptured -> onErrorCaptured
注: Vue3では、beforeCreateとcreatedはsetupに置き換えられました。
refは、提供される値と注入される値の間の応答性を保証するために使用することができます。
import { ref, provide } from '@vue/composition-api'
import ComParent from './ComParent.vue'
export default {
components: {
ComParent
},
setup() {
let treasure = ref('家宝の玉璽')
provide('treasure', treasure)
setTimeout(() => {
treasure.value = 'Vue3.0ベータ'
}, 1000)
return {
treasure
}
}
}
import { inject } from '@vue/composition-api'
import ComChild from './ComChild.vue'
export default {
components: {
ComChild
},
setup() {
const treasure = inject('treasure')
return {
treasure
}
}
}
import { inject } from '@vue/composition-api'
export default {
setup() {
const treasure = inject('treasure')
console.log('treasure: ', treasure)
}
}
弱点/潜在的な問題
新しいAPIは、コンポーネントのオプションを動的にレビュー/変更することをより困難にします。
ユーザー・コード内でコンポーネントを動的に表示/変更することは、通常リスクの高い操作であり、ランタイムにとって多くの潜在的なエッジ・ケースを追加することになるからです。新しいAPIの柔軟性により、ほとんどの場合、より明示的なコードで同じ結果を達成することが可能になるはずです。
経験の浅いユーザーは "スパゲッティ・コード "を書くかもしれません。新しいAPIは古いAPIのようにオプションに基づいてコンポーネント・コードを切り刻むことを強制しないからです。
新しい関数ベースAPIと旧オプションベースAPIの大きな違いは、新しいAPIでは通常のコードで関数を抽出するのと同じように、非常に簡単にロジックを抽出できることです。つまり、ロジックを再利用する必要があるときだけ関数を抽出する必要はなく、コードを整理するためだけに関数を抽出することもできます。
オプションベースのコードは、見た目がすっきりしているだけです。複雑なコンポーネントは、同時に複数の異なる論理タスクを処理する必要があることが多く、各論理タスクに関係するコードは、options API配下の複数のオプションにまたがっています。極端な例ですが、アプリケーションのすべての論理タスクを1つのコンポーネントに入れると、必然的にコンポーネントが大きくなりすぎて保守が困難になります。
対照的に、関数ベースのAPIでは、各論理タスクのコードを対応する関数に整理することが可能です。コンポーネントが大きくなりすぎていることがわかったら、小さなコンポーネントにスライスします。同様に、コンポーネントのsetup()関数が複雑になってきたら、小さな関数にスライスすることができます。同様に、コンポーネントの setup() 関数が複雑になってきたら、それを小さな関数にスライスすることができます。一方、オプションベースの場合は、このようなスライスを行うことはできません。
まとめ
Vue 3.0のAPI調整はそれほど大きなものではなく、2.xに慣れ親しんだ子供たちは既視感を覚えるでしょう。むしろ、ソースレベルでのリファクタリングによって、より良く動作し、より良いパフォーマンスが得られるようになりました。
この記事のケースコードはご覧ください
今日は終わり。




