Vue.js 3.4 で向上した Computed プロパティ
このバージョンでは、Computed プロパティのパフォーマンスが向上しています。
この記事では Vue 3.4 においてどのような改善が行われ、それによって今後のベストプラクティスがどのように変化するかを考えてみたいと思います。
ツイート Post
どんな改善があったか一目瞭然の
ms
の更新により sec
→ min
→ hour
の順に更新されていく Computed プロパティです。
動画を見ると分かるとおり Vue 3.4 の Computed プロパティ min
は sec
の値が変更されたときのみ再計算されています(hour
も同様)
この投稿をしたJohnson Chu氏によってアップデートされたのが Vue 3.4 の Computed プロパティです。
Vue 3.4 のコード(script部のみ)
let sec_counter = 0
let min_counter = 0
let hour_counter = 0
const ms = ref(0)
const sec = computed(() => { sec_counter++; return Math.floor(ms.value / 1000) })
const min = computed(() => { min_counter++; return Math.floor(sec.value / 60) })
const hour = computed(() => { hour_counter++; return Math.floor(min.value / 60) })
setInterval(() => {
ms.value += 10
}, 10)
そもそも Computed プロパティとは
Vue.js ではリアクティブな値をもとに(副作用を伴うことなく、その値をもって)別のリアクティブな値を取得する場合に、Computed プロパティを利用します。
もっとも単純な例はつぎのようなものです。
const counter = ref(0)
const doubled = computed(() => counter.value * 2)
const inc = () => counter.value++
(ボタンのクリックなどで inc() が呼ばれ)counter
の値が変更されると、doubled
の値も自動的に更新されます。
doubled
の値が(テンプレート内のHTMLや、その他の場所で)参照されるたびにcounter
の値を2倍にする関数が実行されるのではなく counter
の値が変更されたときのみ Computed プロパティに記述した関数が実行されます。
そのため、開発者はとくに意識することなく、パフォーマンスの良いコードを書くことができます。
もう少し凝った例
たとえばユーザー情報 User の配列があり、そのなかの特定のユーザーを選択するとします。
type User = {
id: number
name: string
age: number
companyId: string
companyName: string
}
const props = defineProps<{
userId: number
users: User[]
}>()
const userId = computed(() => props.userId) // toRef でも可
const users = computed(() => props.users) // toRef でも可
const userCompanyId = computed<string | null>(() => {
const user = users.value.find((user) => user.id === userId.value)
return user?.companyId ?? null
})
const coworkers = computed<User[]>(() => {
if (userCompanyId.value == null) return []
return users.value.filter((user) => user.companyId === userCompanyId.value)
})
ここでは props.users
はあまり変化しないことを想定したいと思います。
(一度 fetch したらその後は更新しないようなケースです)
Computed プロパティ userCompanyId
は userId
の値が変更されたときのみ算出されます。
また、同僚を意図する Computed プロパティ coworkers
は userCompanyId
の値が変更されたときに算出されますので、やはり userId
の値の変更に反応します。
Vue 3.3 までは userId
の値が変更されるたびに userCompanyId
と coworkers
の双方の関数が実行されていました。
しかし userId
の値が変更されたとしても userCompanyId
の値が同じ値を返す場合、coworkers
の関数は実行される必要がありません。
たとえば、同じ会社の別のユーザーが選択されたケースです。
Vue 3.4 での改善点
Vue 3.4 では、このようなケースにおいて userCompanyId
の値が変更されなかった場合、coworkers
の関数をトリガーしない最適化が加わりました。
(coworkers
を watchEffect している場合でも userCompanyId
に変化がなかった場合は呼ばれなくなりました)
そのため、もし coworkers
の算出に大きなコストがかかる場合、この最適化によってパフォーマンスが向上することが期待できます。
このパフォーマンス向上はアプリケーション側での変更なしに得られるものです。お得ですね。
この効果が期待できないケース
このような効果が望めるのは、依存する userCompanyId.value
の型が string | null
だからです。
内部的に Object.is
による比較が行われているため、同じような結果が返されたとしても、別物と解釈されるケースがあることに注意が必要です。
たとえば、空オブジェクト {}
も、空配列 []
も、Object.is
による比較では異なるものと判定されます。
===
による判定と近いですが、同一ではありません。
const obj = { foo: 123 }
const arr = [1, 2, 3]
;[
['1', 1],
[NaN, NaN],
[undefined, undefined],
[undefined, null],
[null, null],
[-0, 0],
[true, true],
[{}, {}],
[[], []],
[obj, obj],
[arr, arr],
].map(([a, b]) => console.log(JSON.stringify(a), Object.is(a, b), a === b))
実行するとつぎのような結果が得られます。
(左右の値の同一性を判定。ひとつめの true/false
が Object.is
による比較結果、2つめが ===
による比較結果)
> '"1"' false false
> "null" true false // NaN
> undefined true true
> undefined false false // undefined と null
> "null" true true // null
> "0" false true
> "true" true true
> "{}" false false
> "[]" false false
> '{"foo":123}' true true
> "[1,2,3]" true true
そのため、つぎのようなケースにおいては、注意が必要です。
type User = {
id: number
name: string
age: number
companyId: string
companyName: string
}
type Company = {
id: string
name: string
}
const props = defineProps<{
userId: number
users: User[]
}>()
const userId = computed(() => props.userId) // toRef でも可
const users = computed(() => props.users) // toRef でも可
const userCompany = computed<Company | null>(() => {
const user = users.value.find((user) => user.id === userId.value)
if (user === undefined) return null
// オブジェクトリテラルを返すと同じ内容でも別の値として扱われる
return {
id: user.companyId,
name: user.companyName,
}
})
const coworkers = computed<User[]>(() => {
if (userCompany.value == null) return []
return users.value.filter((user) => user.companyId === userCompany.value.id)
})
さきの例との違いは Computed プロパティ userCompany
が Company | null
型を オブジェクトリテラルを使って 返す点です。
この関数で return されるオブジェクトは常に異なるものと判定されるため、userCompany
の値が同じでも coworkers
の関数は実行されてしまいます。
これを解決するために、Vue 3.4 で導入されたもうひとつの変更を知る必要があります。
Vue 3.4 では Computed プロパティは最後の結果を利用できるようになった
Vue 3.4 以降、Computed プロパティの getter 関数は、第1引数として最後の結果が渡されるようになりました。
これにより、つぎのように書くことができます。
type User = {
id: number
name: string
age: number
companyId: string
companyName: string
}
type Company = {
id: string
name: string
}
const props = defineProps<{
userId: number
users: User[]
}>()
const userId = computed(() => props.userId) // toRef でも可
const users = computed(() => props.users) // toRef でも可
const userCompany = computed<Company | null>((lastResult) => {
const user = users.value.find((user) => user.id === userId.value)
if (user === undefined) return null
if (lastResult != null && user.companyId === lastResult.id) {
return lastResult // ← ここで最後の結果を返す
}
return {
id: user.companyId,
name: user.companyName,
}
})
const coworkers = computed<User[]>(() => {
if (userCompany.value == null) return []
return users.value.filter((user) => user.companyId === userCompany.value.id)
})
userCompany
が lastResult
を返した場合(同じオブジェクトのため) coworkers
の関数は実行されません。
従来の記法で記述することができるケースもあります
なお、このケースでは従来通りに書くこともできます。
(今後はこのように多段に Computed プロパティを使用することはこれまで以上に望ましい手法となるはずです)
type User = {
id: number
name: string
age: number
companyId: string
companyName: string
}
type Company = {
id: string
name: string
}
const props = defineProps<{
userId: number
users: User[]
}>()
const userId = computed(() => props.userId) // toRef でも可
const users = computed(() => props.users) // toRef でも可
const selectedUser = computed<User | undefined>(() => users.value.find((user) => user.id === userId.value))
const userCompany = computed<Company | null>(() => {
if (selectedUser.value === undefined) return null
return {
id: selectedUser.value.companyId,
name: selectedUser.value.companyName,
}
})
const userCompanyId = computed<string | null>(() => userCompany.value?.id ?? null)
const coworkers = computed<User[]>(() => {
if (userCompanyId.value == null) return []
return users.value.filter((user) => user.companyId === userCompanyId.value)
})
userCompanyId
は毎回呼ばれますが、userCompany
は userCompanyId
の値が変更されたときのみ呼ばれます。
(このケースは coworkers
の計算量が多いときを想定)
最大の恩恵を受けるために気をつけたいこと
いずれにせよ Computed プロパティが(オブジェクト/配列の参照ではなく)オブジェクト/配列リテラルを使って値を返す場合は気をつけましょう。
また coworkers
が返す値は常に新しい配列です。
(Array.prototype.filter
や Array.prototype.map
等を使用する場合は気をつけないといけません)
そのため、これを使ってまた別の Computed プロパティを定義する場合は、coworkers
が lastResult
を返すような処理を含めておくとよいでしょう。
const coworkers = computed<User[]>((lastResult) => {
if (lastResult !== undefined) {
if (lastResult.length === 0 && userCompany.value == null) {
return lastResult
} else if (lastResult[0]?.companyId === userCompany.value.id) {
return lastResult
}
}
if (userCompany.value == null) return []
return users.value.filter((user) => user.companyId === userCompany.value.id)
})
JavaScript に慣れてない方へ
つぎのように理解しておくとよいでしょう。
- lastResult が不要なものの例
- プリミティブな値(
string
、number
、boolean
、undefined
、null
、Symbol
)-
NaN
も含みます
-
- オブジェクト・配列そのもの(
Map
、Set
、WeakMap
、WeakSet
も同様) - オブジェクト内のプロパティの値・配列内の要素(下記の例で
objFoo
など)-
{ foo: objFoo, bar: objBar, baz: objBaz }
のそれぞれのプロパティ -
[objFoo, objBar, objBaz]
のそれぞれの要素
-
-
Map
、Set
、WeakMap
、WeakSet
のそれぞれのキー・値
- プリミティブな値(
- lastResult が必要になるものの例
- オブジェクト・配列リテラル(
{}
、[]
)で新たに作成された値 - 新しいオブジェクトや配列を返す関数の戻り値
-
Object.assign
、Array.prototype.map
、Array.prototype.filter
、Array.prototype.slice
等
-
- オブジェクト・配列リテラル(
さいごに
Computed プロパティは、とても Vue らしい機能だと思っています。
Vue 3 の Composition API によって、より柔軟にアプリケーション内のステートを管理することができるようになりました。
Web アプリケーションの開発において、パフォーマンス等を過度に気にすることなく、アプリケーション本来のロジックに集中できることはとても有意義なことです。
一方で、リアクティブな値がどのように扱われるかを理解することは、Vue.js を使用した開発において大変重要です。
今回のようなアップデートがあった際に、公式ドキュメントやソースコードで確認する機会とするのは良い習慣だと思います。
また@ubugeeei氏によるThe chibivue Bookで学習するのもおすすめです。chibivue は、ステップバイステップで手を動かしながら Vue.js のなかを学ぶことができるチュートリアルです。
Vue.js をより深く理解したい方はぜひチャレンジしてみてください。
間違いや不充分な説明を見つけたら
間違いや不充分な説明を見つけたらぜひコメント欄等でお知らせください。
正確な情報を伝えることを大切にしています。些細なことで構いませんので、ぜひご協力お願いします。
Discussion