こんばんは。
前回の記事であれだけSolidJSを褒めちぎっておいてアレですが、
結局Vue.jsでTauriのツール開発を続けています。
なんだかんだ言いましても仮想DOMはWEBアプリ成長期から現在に至るまで世界中のWEBを支え続けてきた技術ですから。
「仮想DOMレスの技術が出てきたのでお払い箱」と切って捨てるのはあまりにも不誠実だと思い直しまして、
改めて画面のパフォーマンスに考慮した仮想DOMフレームワークの書き方を考えてみました。
VueとかReactの方が圧倒的にお金になる案件多いしね
前回Vueを使ったときに悩まされた画面フリーズの問題についても原因の切り分けと対策を確立しましたので、軽くここで説明させていただきます。
コンポーネントがフリーズする原因を切り分けてみた。
細かい部分を全部すっ飛ばして極限まで簡略化すると、Vueの基本構成は以下のような感じになると思います。
親コンポーネントから子コンポーネントを呼び出し、設定したいパラメーター等をpropsで渡す。
コンポーネントの垣根を越えて値を共有したい場合は外部でrefを宣言してexportしたり、
useStateやpiniaの状態管理機能でグローバル値を定義するイメージです。
この中で画面フリーズを引き起こす最大の原因はグローバル値です。
例えば、下記のような形式で親と子のコンポーネントで同じrefを共有したとしましょう。
import { ref } from "vue";
export const state = ref<string[]>(["りんご", "バナナ", "みかん"])
<script lang="ts" setup>
import { state } from "./state";
import Ko from "./Ko.vue"
state.value.forEach((item) => {
item = item + "ジュース"
})
</script>
<template>
<Ko v-for="item in state" :item="item"/>
</template>
<script lang="ts" setup>
import { state } from "./state";
const props = defineProps({
item: String
})
const addItem = ["ぶどう", "スイカ", "いちご"]
addItem.forEach((item) => {
state.value.push(item)
})
</script>
<template>
<h1>{{ props.item }}</h1>
</template>
子と親、両方でグローバルrefのstateに要素を追加し合っていますね。
これだけでもうTauri上ではフリーズしたような状態になりまともに動作しなくなります。
アプリは真っ白のまま、devtoolも一切状態をキャッチしてくれません。
何故こうなるのか
原因を簡単に説明します。
仮想DOMはコンポーネントを構成している要素の変更を検出すると、自動でキャッシュされている変更前の状態と変更後の状態を比較し、その差分を検出します。
変更部分だけを仮想DOM上で変更し、変更した仮想DOMを画面に適用することで高速な画面描画の変更を可能にしているわけです。
ここで気を付けなければいけないのは、差分検出と画面の再描画が行われるたび、描画されたコンポーネント内のscript処理が繰り返し実行されてしまうということ。
例題のコードでは子コンポーネントからグローバル配列に対し["ぶどう", "スイカ", "いちご"]をpushしていますが、グローバル配列の各要素は子コンポーネントを構成しているpropsでもあります。
propsが入った配列に要素が追加されると、当然子コンポーネントは再描画されます。
再描画されるたび配列に対して["ぶどう", "スイカ", "いちご"]を追加し続ける、所謂「無限ループ」のような状態になってしまいます。
どうやって防ぐのか
- 子コンポーネントでpropsの構成要素に触らない。
これだけです。
どうしてもグローバル値をpropsとして使用したい場合は別の配列に格納して固定値化するか、子コンポーネント内からグローバル値への参照を消してしまいましょう。