Vue3: 「リアクティビティーの探求」を探求する

Vue 3/Composition APIの公式ドキュメントには、フレームワークのコア機能の一つであるリアクティビティについて、「リアクティビティーの探求」というページを作って公式の解説があります。フレームワークやライブラリが自身の構造を丁寧に説明してくれることって少ない感覚で、Vue歴が短い自分にとっては嬉しい & 興味深い解説ページでした。

[リアクティビティーの探求 | Vue.js](https://ja.vuejs.org/guide/extras/reactivity-in-depth.html)

いっぽうで、ページにあるコードは断片的で、そのままでは動かず再現が難しいです（実際難しかった）。

せっかくなら、と@vue/reactivityを見たり解説動画見たりしながら動くコードを作ってみました。この記事はその超初歩の部分を言語化したものです。

# はじめに

リアクティビティの簡易な再現を試みることをゴールとします。そして、値の計算をリアクティブにできたとして、DOMレンダリングの問題が残りますがこれも扱いません。

なお、Vue Masteryから本格的な解説動画が出ているので、興味のある方は見てみると学びが深いです。実は、この記事は動画とVue 3のコードを読んで理解したことのサマリでもあります。

[Reactivity in Vue 3 - How does it work?](https://www.youtube.com/watch?v=NZfNS4sJ8CI&t=66s)

## コードの補足

記事等から再現を試みた際に、型というかオブジェクトの形がわからず苦労したので、できる限りTypeScriptで書きます。

記事に登場するコードは説明のために切り取ったり一部省略されているところがあります。前編はリポジトリに置かれているのでこちらもご参考ください。

- [github.com/simonNozaki/miactive](http://github.com/simonNozaki/miactive)

記事に書ききれなかったコードの全容を見れます。

# リアクティビティとはなんなのか

筆者は、はじめリアクティビティがいまいちわかったような気でわかっていませんでした。かんたんにどういうものかを整理します。

リアクティビティが実現するとは、以下のコードが成り立つ状態でもあります。

```tsx
const price = 5
const quantity = 2
const total = price * quantity

console.log(total) // 10

const price = 8
console.log(total) // 16
```

通常、 `total` が評価されたあとで構成要素に変化があろうが結果が更新されることはないわけです。`total` がリアクティブになるなら、`price`への代入を検知して`total`が再計算されるようになります。

では、リアクティビティをどう達成するかなのですが、すごーくざっくりいうと

**effectを構成するプロパティを変更追跡対象にして、変更を検知したらeffectを再評価する**

といった感じになります。

もうちょっと実装の話をすると、再評価したい`effect`（`total = price * quantity`のような式）を集めるSetを用意して、要素の変更を検知したらSetから取り出して評価を実行する流れとなります。扱いやすいように色々ゴニョゴニョするけど、コアのアイデアは本当にそんな感じです。

ちなみに、公式の解説にもコードにも `effect` という命名が頻出します。日本語にするとニュアンスが抜け落ちてしまう感覚があるので、以後断りなく `effect` と書きますが「複数の要素からなる式」、くらいの認識で問題ありません。

```jsx
function effect () {
  return price * quantity
}
```

見取り図はこのへんにして、具体的なコードで再現していきます。

# effectを追跡する

Vue3のリアクティビティはMapやSetで変更対象の追跡登録・再評価を行うため、どこに何が入っているのか結構混乱します…

コードと図で1つずつ読み解きます。

まずは、先ほど上げたSetと各種関数を書きます。追跡には `track` 、再評価には `trigger` を定義します。 `dep` や `track` 、 `trigger` の名前は本家コードにもそのまま登場するものです。

```jsx
let price = 5
let quantity = 2
let total = 0

let dep = new Set()

let effect = () => { total = price * quantity }

const track = () => { dep.add(effect) }
const trigger = () => { dep.forEach(effect => effect()) }

track()
effect()
```

図で整理してみると…

![スクリーンショット 2023-03-22 21.51.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183073/f7b9ab34-2e26-7cb9-eb23-120b7988940c.png)

## オブジェクトの導入

単純な数値や文字列だけにとどまらず、オブジェクトとそのプロパティも変更対象として検知したいことがあります。そこでプロパティをキーに `dep` を保持するMapを用意します。

```jsx
const depsMap = new Map<any, Set>()
```

このマップはオブジェクトのプロパティをキーに`dep`を対応させるので、`track`と`trigger`でそれぞれ対応付け・キーによる検索を実現させたいです。

```jsx
const track = (key: unknown) => {
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(effect)
}

const trigger = () => {
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}
```

追跡対象の登録、評価はそれぞれ呼び出すだけです。

```jsx
track('quantity')
effect()
```

逆にわかりづらいかもですが…図にすると以下のような感じになります。

![スクリーンショット 2023-03-23 8.29.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/183073/c7fa4aec-6024-3b60-4c8d-e7b255ddf1cf.png)


 `depsMap` はオブジェクトのプロパティと、プロパティに依存しているeffectの集合ということがわかりました。最後に、そもそも `depsMap` のキーを持つオブジェクト自体を格納しておくなにかがあると便利です（ないと不便）。オブジェクトをキーに、`depsMap` を持つ `targetMap` を定義します。

```tsx
const targetMap = new WeakMap<any, Map<any, Set>>()
```

WeakMap はオブジェクトをキーにできるよわいMapです。ここでは、普通のMapと同じように使うので特殊性などの説明は割愛します。

[WeakMap - JavaScript | MDN](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)

`track`, `triggerを` `targetMap` から順番に `dep` を引くように直します。なお、`get` の過程でSetがなければnewして空箱を用意しておきます。

```tsx
const track = (target: any, key: unknown) => {
  let dep = depsMap.get(key)
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(effect)
}

const trigger = (target: object, key: unknown) => {
  const dep = targetMap.get(target)?.get(key)
  if (dep) {
    dep.forEach(effect => effect())
  }
}
```

`track`と`trigger`を手動で呼び出して追跡と評価を実行してみます。

```tsx
const price = 5
const quantity = 2
const product = { quantity * price }
track(product, 'quantity')
trigger()
```

なお、普段目にするVueの機能とは少し距離があるな、と感じると思います。実際のフレームワーク内部は、これに色々かぶせて `ref` や `computed` などを実現しています。

# おわりに

ここで書いたようなアイデアがわかれば、Vueのコードが読みやすくなるしデバッグもいくばくか気楽になります。

記事で紹介した簡易再現コードのフルセットはリポジトリの `core/packages/reactivity` に実態があります。この記事や動画、公式ドキュメントと合わせてぜひご参照ください。

[core/packages/reactivity at main · vuejs/core](https://github.com/vuejs/core/tree/main/packages/reactivity)