◆ Vue は React と違ってテンプレートを分けて書くし Composition API の ref の仕組みは新しめのライブラリに似てる
   ◆ ゲッターとセッターを使って使用箇所の管理と更新の通知
◆ React などのレンダー関数再実行系と違って 変更があった値を使った場所だけを直接書き換えてる?
◆ 実際は React と同じで更新のたびにレンダー関数を再実行してた
   ◆ 余計な処理を減らすために {{ }} の中で処理するのは控えたほうが良さそう

こんな HTML があるとします

<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>