😽

Vueのrefは破壊的な値更新でも変更検知できるのは何故なのか

2024/02/07に公開
この記事でのVueはVue3を前提としています

Vue歴2ヶ月くらいで、それまで業務ではReactを触っていたのですが
Vueのrefについて気になることがあって、調べてみたのでメモ的に書いていこうと思います。

Vueでも、Reactでも書き方は違えど、ステート(Vueではリアクティブ)を管理できますが、
Vueの書きっぷりに少し違和感がありました。

Vueの比較対象としてReactで簡単なステート更新をやってみました。

Reactでは破壊的な方法でステートを更新できない

  const [items, setItems] = useState([]);

  const handleUpdate = () => {
    // push! push!
    const newItems = items.push("hello world");
    setItems([...newItems]);
  };

Reactに慣れ親しんでいる方は、お分かりかと思いますが、
このような破壊的な方法でステートを更新すると、コンパイラがnewItems is not iterableというエラーを投げます。そのため上記のコードは動きません。

Reactでは、ステートの更新が反映されるようにするために、イミュータブルな方法でステートを更新する必要があります。例えば、スプレッド構文を使用して新しい配列を作成してからステートを更新します。

setItems([...items, "hello world"])

Vueでは破壊的な方法でもリアクティブな値(ステート)を更新できる

一方で、Vueではpushなどの破壊的なメソッドを使っても、リアクティブな値(ステート)を更新することができます。(ここではリアクティブな値の管理としてrefを使います。)

Reactで書き慣れていたため、ステートの更新においてイミュータブルであるべきだ と思っていることから直感的に、このpush をみた時に驚きましたが、

実のところrefはミュータブルです。

ref オブジェクトはミュータブルです - すなわち、.value に新しい値を割り当てることができます。それはまたリアクティブです。つまり、.value へのあらゆる読み取り操作は追跡され、書き込み操作は関連するエフェクトを引き起こします。
https://ja.vuejs.org/api/reactivity-core.html#ref

(Vue3)

const items = ref([]);

const handleUpdate = () => {
  // push! push!
  items.value.push("hello world");
};

ちなみにvueのrefには、reactのstate更新で使うようなコールバックは存在しない(ミュータブルな)ので、Ref<Array>に限らず、オブジェクト代入のような形で更新できます。

text.value = "good bye"
isDisabled.value = true

Vueのrefは、内部のProxyオブジェクトでリアクティブな変更を実現している

const items = ref([]);itemsをログで出力してみましょう。

 Proxy(Array) {0: {}} 'items.value'
[[Handler]]: MutableReactiveHandler
[[Target]]: Array(1)
[[IsRevoked]]: false

このProxyオブジェクトが肝心で、
リアクティブに処理できる理由は、Vueが内部でこのProxyオブジェクトを使用してリアクティブな変更を実現しているためです。

Vueのrefは、JavaScriptのProxyオブジェクトを使用してラップされています。
refがラップする値への変更を監視し、変更が検出されたときに自動的にリアクティブな更新をしてくれます。

これにより、破壊的な操作(例であった、配列への破壊的な要素の追加)が行われても、
Vueのrefは内部でプロキシングされており、そのプロキシが破壊的な操作を、良い感じにリアクティブな変更として処理できるということみたいです。

100%理解できてはいないですが、気になって調べてみて良かったです。

Discussion