Vue も state 更新のたびにコンポーネント全体が再計算されてる
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ Vue は React と違ってテンプレートを分けて書くし Composition API の ref の仕組みは新しめのライブラリに似てる
◆ ゲッターとセッターを使って使用箇所の管理と更新の通知
◆ React などのレンダー関数再実行系と違って 変更があった値を使った場所だけを直接書き換えてる?
◆ 実際は React と同じで更新のたびにレンダー関数を再実行してた
◆ 余計な処理を減らすために {{ }} の中で処理するのは控えたほうが良さそう
◆ ゲッターとセッターを使って使用箇所の管理と更新の通知
◆ React などのレンダー関数再実行系と違って 変更があった値を使った場所だけを直接書き換えてる?
◆ 実際は React と同じで更新のたびにレンダー関数を再実行してた
◆ 余計な処理を減らすために {{ }} の中で処理するのは控えたほうが良さそう
こんな HTML があるとします
ボタンが 2 つあって ボタンを押すと押したボタンのテキストが切り替わります
テキストは何でもいいのでとりあえず押した回数を表示して 例としてよく使われるカウンターとします
この HTML のまとまりはコンポーネントに分けず 1 つのコンポーネントとして扱うことにします
こういう感じです
React では count1 や count2 の state が更新されるたびに App 関数が再実行されます
なので return のところで作ってる仮想 DOM を毎回作っています
今では数字をそのまま表示ですが 例えば漢数字で表示したいといった場合は
みたいにして convert 関数で変換が必要です
これは毎回実行されます
コンポーネントにしてメモ化等ありますが 今回のメインの話じゃないので置いておきます
React の考え方だと count1 を更新しただけでも count2 の分も計算が発生します
もちろん count2 の更新でも count1 の分も計算されます
state を使う場所以外でも仮想 DOM のオブジェクトを作って比較する処理はあります
それに Composition API の記法や変更通知の仕組みって React 系とは違って必要最小限の変更だけするライブラリに似ている気がします
ということは count1 を更新しても count2 などその他の部分には一切触れずに {{ }} の部分だけを変更してたりするのでしょうか
まず先に 他の最小限の更新だけするライブラリを見てみます
2 つめのボタンを表示するところには console.log を追加してます
Solid は React と違って 更新のたびに関数全体を再実行しません
1 つめのボタンを押すと count1 を使ったところだけ更新されます
count2 の方には影響しないので コンソールには何も表示されないです
2 つめのボタンを押すと count2 の部分が更新されるので コンソールに 2 が表示されます
Solid は見た目は React に似ていますがリアクティブにするための Signal が特殊で 値そのものでなくゲッターとセッターを取得します
これによって Solid に Signal の使用箇所を伝えることができて 必要なところだけを更新できます
Vue の ref ではゲッターとセッターを関数で受け取るのではなくプロパティにゲッターとセッターがついているものですが 仕組みは似ているように思います
ref を埋め込まれたところがわかっているので ref の更新を検知して自動で更新できそうに思います
見た目がすでに Vue の SFC と似ています
これも 1 つめのボタンを押したときにはコンソールに何も表示されず 2 つめのボタンを押したときにはコンソールに 2 が表示されます
Van の state は val プロパティのゲッターとセッターで これらを使って更新が必要なところと更新があったことを管理するので Vue の ref と近いと思います
Van では console.log を実行させるためには関数でラップしないといけないので少し特殊なことになっています
これも 1 つめのボタンを押したときにはコンソールに何も表示されず 2 つめのボタンを押したときにはコンソールに 2 が表示されます
Solid や Svelte や Van を見た感じだと React よりはこれらの仲間っぽさはありますけど
これまでと同じように 2 つめのボタンだけ console.log を追加して試してみます
結果 どっちを押してもコンソールに 2 が表示されました
React と同じタイプみたいです
なんか残念
仕組み的には ref の使用箇所が Vue 側でわかっていて そこだけを更新できそうなのですけど
console.log の代わりに debugger を入れて実際に実行されるコードを表示します
debugger は文なので 即時関数実行でラップします
これで devtools を開いてからボタンを押すと実行中のコード見れます
こうなってました
中で render 関数が作られて これ全体が毎回実行されているようです
{{ }} の中がそのまま入っています
() でラップしてくれないです
そのせいで Solid や Svelte の例のように
と書いたら
のように引数が分かれてしまいます
toDisplayString は引数を一つしか受け取りません
https://github.com/vuejs/core/blob/v3.3.4/packages/shared/src/toDisplayString.ts#L16
なので 最初の 「,」 までの式が表示されることになります
期待する内容がなぜか表示されなくて これに少し時間を取られました
{{ }} の中は 1 つの式にするというルールがあるなら 別にこれでもいいとは思いますが インジェクションできそうですよね
開発者が自分でそんなことする意味ないので実害は無いのかもですけど
試しにこういうテンプレートにしてみたらエラーなく動きました
<div>
<div>
<button>(1)</button>
</div>
<div>
<button>(2)</button>
</div>
</div>
ボタンが 2 つあって ボタンを押すと押したボタンのテキストが切り替わります
テキストは何でもいいのでとりあえず押した回数を表示して 例としてよく使われるカウンターとします
この HTML のまとまりはコンポーネントに分けず 1 つのコンポーネントとして扱うことにします
React だと
まずは React で考えてみますimport { useState } from "react"
const App = () => {
const [count1, setCount1] = useState(0)
const [count2, setCount2] = useState(0)
const up1 = () => setCount1(count1 + 1)
const up2 = () => setCount2(count2 + 1)
return (
<div>
<div>
<button onClick={up1}>{count1}</button>
</div>
<div>
<button onClick={up2}>{count2}</button>
</div>
</div>
)
}
こういう感じです
React では count1 や count2 の state が更新されるたびに App 関数が再実行されます
なので return のところで作ってる仮想 DOM を毎回作っています
今では数字をそのまま表示ですが 例えば漢数字で表示したいといった場合は
<button onClick={up1}>{convert(count1)}</button>
みたいにして convert 関数で変換が必要です
これは毎回実行されます
コンポーネントにしてメモ化等ありますが 今回のメインの話じゃないので置いておきます
React の考え方だと count1 を更新しただけでも count2 の分も計算が発生します
もちろん count2 の更新でも count1 の分も計算されます
state を使う場所以外でも仮想 DOM のオブジェクトを作って比較する処理はあります
Vue だと
Vue って HTML のテンプレート定義と JavaScript で処理を書く部分が分かれてますよねそれに Composition API の記法や変更通知の仕組みって React 系とは違って必要最小限の変更だけするライブラリに似ている気がします
<!doctype html>
<meta charset="utf-8" />
<script type="module">
import { createApp, ref } from "https://unpkg.com/vue@3.3.4/dist/vue.esm-browser.js"
createApp({
setup() {
const count1 = ref(0)
const count2 = ref(0)
const up1 = () => count1.value++
const up2 = () => count2.value++
return { count1, count2, up1, up2 }
}
}).mount("#root")
</script>
<div id="root">
<div>
<div>
<button @click="up1">{{ count1 }}</button>
</div>
<div>
<button @click="up2">{{ count2 }}</button>
</div>
</div>
</div>
ということは count1 を更新しても count2 などその他の部分には一切触れずに {{ }} の部分だけを変更してたりするのでしょうか
まず先に 他の最小限の更新だけするライブラリを見てみます
最小限の更新: Solid
Solid だとこんな感じですimport { createSignal } from "solid-js"
const App = () => {
const [count1, setCount1] = createSignal(0)
const [count2, setCount2] = createSignal(0)
const up1 = () => setCount1(count1() + 1)
const up2 = () => setCount2(count2() + 1)
return (
<div>
<div>
<button onClick={up1}>{count1()}</button>
</div>
<div>
<button onClick={up2}>{console.log(2), count2()}</button>
</div>
</div>
)
}
2 つめのボタンを表示するところには console.log を追加してます
Solid は React と違って 更新のたびに関数全体を再実行しません
1 つめのボタンを押すと count1 を使ったところだけ更新されます
count2 の方には影響しないので コンソールには何も表示されないです
2 つめのボタンを押すと count2 の部分が更新されるので コンソールに 2 が表示されます
Solid は見た目は React に似ていますがリアクティブにするための Signal が特殊で 値そのものでなくゲッターとセッターを取得します
これによって Solid に Signal の使用箇所を伝えることができて 必要なところだけを更新できます
Vue の ref ではゲッターとセッターを関数で受け取るのではなくプロパティにゲッターとセッターがついているものですが 仕組みは似ているように思います
ref を埋め込まれたところがわかっているので ref の更新を検知して自動で更新できそうに思います
最小限の更新: Svelte
Svelte だとこんな感じです<script>
let count1 = 0
const up1 = () => {
count1++
}
let count2 = 0
const up2 = () => {
count2++
}
</script>
<div>
<div>
<button on:click={up1}>{count1}</button>
</div>
<div>
<button on:click={up2}>{console.log(2), count2}</button>
</div>
</div>
見た目がすでに Vue の SFC と似ています
これも 1 つめのボタンを押したときにはコンソールに何も表示されず 2 つめのボタンを押したときにはコンソールに 2 が表示されます
最小限の更新: Van
Van だとこんな感じです<!doctype html>
<meta charset="utf-8" />
<script type="module">
import van from "https://cdn.jsdelivr.net/gh/vanjs-org/van/public/van-1.1.3.min.js"
import htm from "https://cdn.jsdelivr.net/npm/htm@3.1.1/mini/index.module.js"
const h = (type, props, ...children) => van.tags[type](props, ...children)
const html = htm.bind(h)
const App = () => {
const count1 = van.state(0)
const count2 = van.state(0)
const up1 = () => count1.val++
const up2 = () => count2.val++
return html`
<div>
<div>
<button onclick=${up1}>${count1}</button>
</div>
<div>
<button onclick=${up2}>${() => { console.log(2); return count2.val }}</button>
</div>
</div>
`
}
van.add(document.getElementById("root"), App())
</script>
<div id="root"></div>
Van の state は val プロパティのゲッターとセッターで これらを使って更新が必要なところと更新があったことを管理するので Vue の ref と近いと思います
Van では console.log を実行させるためには関数でラップしないといけないので少し特殊なことになっています
これも 1 つめのボタンを押したときにはコンソールに何も表示されず 2 つめのボタンを押したときにはコンソールに 2 が表示されます
Vue に戻ってきて
さて Vue だとどうなんでしょうかSolid や Svelte や Van を見た感じだと React よりはこれらの仲間っぽさはありますけど
これまでと同じように 2 つめのボタンだけ console.log を追加して試してみます
<!doctype html>
<meta charset="utf-8" />
<script type="module">
import { createApp, ref } from "https://unpkg.com/vue@3.3.4/dist/vue.esm-browser.js"
createApp({
setup() {
const count1 = ref(0)
const count2 = ref(0)
const up1 = () => count1.value++
const up2 = () => count2.value++
return { count1, count2, up1, up2 }
}
}).mount("#root")
</script>
<div id="root">
<div>
<div>
<button @click="up1">{{ count1 }}</button>
</div>
<div>
<button @click="up2">{{ (console.log(2), count2) }}</button>
</div>
</div>
</div>
結果 どっちを押してもコンソールに 2 が表示されました
React と同じタイプみたいです
なんか残念
仕組み的には ref の使用箇所が Vue 側でわかっていて そこだけを更新できそうなのですけど
テンプレート
テンプレートがどう扱われてるのかを見てみますconsole.log の代わりに debugger を入れて実際に実行されるコードを表示します
debugger は文なので 即時関数実行でラップします
<button @click="up2">{{ ((() => { debugger })(), count2) }}</button>
これで devtools を開いてからボタンを押すと実行中のコード見れます
こうなってました
(function anonymous(Vue
) {
const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = ["onClick"]
const _hoisted_2 = ["onClick"]
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("div", null, [
_createElementVNode("button", { onClick: up1 }, _toDisplayString(count1), 9 /* TEXT, PROPS */, _hoisted_1)
]),
_createElementVNode("div", null, [
_createElementVNode("button", { onClick: up2 }, _toDisplayString(((() => { debugger })(), count2)), 9 /* TEXT, PROPS */, _hoisted_2)
])
]))
}
}
})
中で render 関数が作られて これ全体が毎回実行されているようです
埋め込み部分のバグのような挙動
ちなみにこの部分_toDisplayString(((() => { debugger })(), count2))
{{ }} の中がそのまま入っています
() でラップしてくれないです
そのせいで Solid や Svelte の例のように
{{ console.log(2), count2 }}
と書いたら
_toDisplayString(console.log(2), count2)
のように引数が分かれてしまいます
toDisplayString は引数を一つしか受け取りません
https://github.com/vuejs/core/blob/v3.3.4/packages/shared/src/toDisplayString.ts#L16
なので 最初の 「,」 までの式が表示されることになります
期待する内容がなぜか表示されなくて これに少し時間を取られました
{{ }} の中は 1 つの式にするというルールがあるなら 別にこれでもいいとは思いますが インジェクションできそうですよね
開発者が自分でそんなことする意味ないので実害は無いのかもですけど
試しにこういうテンプレートにしてみたらエラーなく動きました
<button @click="up2">{{ count2), 9, _hoisted_2) // }}</button>