Vue.js 3 をつくって学ぶ リアクティブシステム

本記事では、**Vue.js v3 のコア部分であるリアクティブシステムを実装し、その仕組みを学びます。**

今回実装するリアクティブシステムを用いれば、以下のような簡単なアプリケーションが作れます。

![simple-vue-app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109606/64819d86-ec70-f424-f273-506b3b1342f3.gif)

最終的なコードはGitHubにあります。
https://github.com/hareku/simple-vue-reactivity

## リアクティブとは
**「リアクティブである」とは、「その値が監視され、変更が検知される状態のこと」**を指します。

Vue.jsやReactでは、コンポーネントが保持している状態を変更すれば、その変更が検知されてHTMLに反映されます。それらを実現するためには、**値の変更を検知可能な状態にするリアクティブシステムが必要**です。

### Vue.js v3 におけるリアクティブシステム

Vue.js のリアクティブシステムは、主に以下の3つで構成されます。

- ref
- effect
- computed

まずは`ref` と `effect` を使ったサンプルコードを見てみましょう。

```ref-effect-example.ts
const counter = ref(0) // 0 は object になり、valueプロパティでアクセスできる

// counter.value が変更されたら、effectの引数に渡した関数が自動で再実行される
let double = 0
effect(() => {
  double = counter.value * 2
})

console.log(double) // 0
counter.value = 5
console.log(double) // 10 <- Changed!
```

このように `ref` でリアクティブにした値を `effect` に渡した関数内で参照すれば、変更を検知して自動で関数を再実行してくれます。

これは一見すると魔法のようですが、単純な仕組みがあります。
それでは、これらのリアクティブシステムを実装していきましょう。

## リアクティブシステムの実装
※Vue.js はオブジェクトもリアクティブにできるのですが、今回は簡略化のためオブジェクトには対応しません。数値や文字列などのオブジェクトではない値のみ、リアクティブにします。

### Ref の実装
`ref` 関数は、引数で渡した値をリアクティブにします。



`ref` に渡した値が value をキーとしたオブジェクトに変換される理由は、getter/setter によるフック処理を行うためです。


```ref.ts
export function ref(value: any) {
  return {
    get value() {
      // TODO: track
      return value
    },
    set value(newVal) {
      value = newVal
      // TODO: trigger
    }
  }
}
```

JavaScript では、object のプロパティ名に get/set を付けることで getter/setter を実装できます。

TODO の通り、getter に「値が使われていることを知らせる処理 (track)」と、setter に「値が変更されたことを知らせる処理 (trigger)」が必要です。これらの処理は、次に解説する `effect` 関数に依存しているため、そちらで実装します。

### Effect の実装

それでは `effect` 関数を実装します。これはとてもシンプルです。

```effect.ts
let activeEffect: Function | null = null

export function effect(fn: Function) {
  activeEffect = fn
  fn()
}
```

`effect` 内では、引数に渡された関数を `activeEffect` という変数に格納しています。この変数は次のトラッキング処理で用います。

#### トラッキング処理（track）

次は `ref` の getter で必要なトラッキング処理である `track` を実装します。

```ref.ts
const ref = {
  get value() {
    track(ref) // これを実装する
    return value
  },
  set value(newVal) {
    value = newVal
    // TODO: trigger
  }
}
```

`track` は、どの `effect` がどういった `ref` に依存しているかという依存関係を管理します。
一度 `track` のコードを見てみましょう。

```effect.ts
// Set はユニークな値のみを格納できる配列
// https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Set
type Deps = Set<Function>

// WeakMap は Object をキーにできるオブジェクト
// targetMap では ref をキーとして、参照している effect のリストを格納する
const targetMap = new WeakMap<object, Deps>()

export function track(target: object) {
  // target (ref) に依存している関数リスト (deps) を取得する
  let deps = targetMap.get(target)

  // deps がなければ初期化する
  if (deps === undefined) {
    deps = new Set()
    targetMap.set(target, deps)
  }

  // 現在実行中の effect 関数を依存関係 (deps) に追加する
  if (activeEffect && !deps.has(activeEffect)) {
    deps.add(activeEffect)
  }
}
```

複雑に見えますが、やっていることは WeakMap で `ref` に依存している `effect` リストを管理しているだけです。

これらの依存関係をもとに、次のトリガー処理で、特定の `ref` に依存している `effect` をすべて再実行できます。

#### トリガー処理（trigger）

`ref` の setter 時の処理であるトリガーを実装します。

```ref.ts
const ref = {
  get value() {
    track(ref)
    return value
  },
  set value(newVal) {
    value = newVal
    trigger(ref) // これを実装する
  }
}
```

```effect.ts
export function trigger(target: object) {
  // target に依存している effect リストを取得する
  const deps = targetMap.get(target)
  if (deps === undefined) {
    return
  }

  // target に依存している effect を全て再度実行する
  deps.forEach(effect => {
    effect()
  })
}
```

これにより、 `ref` の value が更新されるたびに、依存関係にある `effect` を全て再実行することができます。これが Vue.js におけるリアクティブシステムの仕組みです。

### computed

先ほど実装した `ref` と `effect` を組み合わせれば、Vue.js の `computed` を実装できます。

まずは `computed` の仕様の確認のため、jest でテストを書いてみます。

```computed.test.ts
describe('reactivity/computed', () => {
  it('should observe ref', () => {
    const counter = ref(0)
    const double = computed(() => counter.value * 2)

    expect(double.value).toBe(0)
    counter.value = 5
    expect(double.value).toBe(10)
  })
})
```

`computed` を実装すれば、わざわざ `effect` の外に定義していた `let double` のような変数を省略することができます。`computed`はこのように実装します。

```computed.ts
export function computed<T = any>(getter: () => T): { value: T } {
  let value: T
  effect(() => {
    value = getter()
  })

  return {
    get value() {
      return value
    }
  }
}
```

## アプリケーションの実装
それでは実装したリアクティブシステムを用いて、冒頭のサンプルアプリケーションを実装してみましょう。

![simple-vue-app.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109606/64819d86-ec70-f424-f273-506b3b1342f3.gif)

### HTML

JavaScript 側で操作するために、各タグに適当な id を付与しておきます。

```app.html
<div>
  <span id="count"></span> * 2 = <span id="multiplied"></span>
</div>
<div>
  <button id="increment">Increment ++</button>
  <button id="decrement">Decrement --</button>
</div>
```

### JavaScript

まずは、 count とそれを2倍した値である multiplied をリアクティブな値として作ってみます。

```app.ts
const count = ref(0)
const multiplied = computed(() => count.value * 2)
```

そして、`effect` 関数内で HTML の操作を行います。

```app.ts
const countElement = document.getElementById('count')
const multipliedElement = document.getElementById('multiplied')

effect(() => {
  countElement.innerText = String(count.value)
  multipliedElement.innerText = String(multiplied.value)
})
```

次に値を増減させるため、ボタンにクリックイベントを登録します。

```app.ts
const incrementButton = document.getElementById('increment')
const decrementButton = document.getElementById('decrement')

incrementButton.addEventListener('click', () => {
  count.value++
})
decrementButton.addEventListener('click', () => {
  count.value--
})
```

`addEventListener` に渡した関数内で `count.value` を変更しているので、変更する度に `effect` 内の HTML 操作が実行され、Viewが更新されます。

これでアプリケーションの完成です。

### 最終的なアプリケーションコード

```app.ts
import { ref } from '../ref'
import { effect } from '../effect'
import { computed } from '../computed'

const countElement = document.getElementById('count')
const multipliedElement = document.getElementById('multiplied')
const incrementButton = document.getElementById('increment')
const decrementButton = document.getElementById('decrement')

if(countElement && multipliedElement && incrementButton && decrementButton) {
  const count = ref(0)
  const multiplied = computed(() => count.value * 2)

  incrementButton.addEventListener('click', () => {
    count.value++
  })
  decrementButton.addEventListener('click', () => {
    count.value--
  })

  effect(() => {
    countElement.innerText = String(count.value)
    multipliedElement.innerText = String(multiplied.value)
  })
}
```

## 次なるステップ

本記事の解説は以上になります。

さらに理解を深めるためのステップとしては、オブジェクトのリアクティブ化があります。対応方法として、依存関係を管理していた WeakMap をさらに深くし、オブジェクトの各キーごとの `effect` を管理する手法があります。
また他にできることとして、プロパティの追加や削除への対応や、監視を浅く (shallowに) してパフォーマンスを改善することなどがあります。

実装したい方は Vue.js v3 のソースコード (@vue/reactivity) が参考になります。
https://github.com/vuejs/vue-next/tree/master/packages/reactivity