猿でも分かる！Vueのリアクティブシステム

# Vueのリアクティブシステムを理解する
（**Zenn**にも[同じ記事](https://zenn.dev/kj455/articles/c740c72edeabbe)を書いています。そちらにはcodesandboxを貼ってあります）

皆さん、Vue.jsを使っていますか？個人的には結構好きでよく使っているのですが、あのリアクティブ性はどうやって実現しているのか気になりませんか？本記事ではVue2系のリアクティブシステムの原理を簡単に説明していきます。

フロントは流行り廃りの激しい分野だからこそ、主要技術がどうやって実現しているのかを知ることが今後にも生きるはずです。

今回使用したコード(sandbox)を記事最下部に貼っておきます。

## 今回の設定
`item`オブジェクトには価格`price`と量`quantity`プロパティがあります。
それらの総額に相当する`total`プロパティをitemオブジェクトの変更を検知して自動的に自動的に再計算されて欲しい。

vue.js的に書くとこんな感じ。

```example.vue
<script>
export default {
  data() {
    item: {
      price: 100,
      quantity: 3,
    }
  },
  computed: {
    total() {
      return this.item.price * this.item.quantity; // -> 300
    }
  }
}
</script>
```

（vueの原理を解説するので以降はバニラjsで説明します。やりたいことは変わりません。）

## もしリアクティブ性がないと...
`item.quantity`に5を代入しても、totalが再計算されないので300のままになってしまいます。
これが自動的に500になるようにしたい。

```ts:level1.ts
const item = {
  price: 100,
  quantity: 3
};
let total: number = 0;
const calcTotal = () => (total = item.price * item.quantity);

calcTotal();

console.log(total); // -> 300

item.quantity = 5;

console.log(total); // -> 300（これが500になって欲しい）

```

## どんな機能が必要か
上の例から、リアクティブ性というのは、
**プロパティが変更されたらそれを検知してそのプロパティが使用されている関数を自動的に再計算すること**
と言い換えることができます。つまり、これを実現するには

- プロパティ毎にそのプロパティがどんな関数で使われているかを保存しておく
- そのプロパティの更新時に関連する関数を全て再計算する

機能があれば良いわけです。どうですか？なんだかできそうな気がしてきませんか？

## `quantity`に対してこれを実装してみる

```level2.ts
const item = {
  price: 100,
  quantity: 3
};
let total: number = 0;

const storageForQuantity: Function[] = [];
const record = (func: Function) => {
  storageForQuantity.push(func);
};
const reCalculate = () => {
  storageForQuantity.forEach((func) => func());
};

const calcTotal = () => (total = item.price * item.quantity);

calcTotal(); // 初期totalの計算

record(calcTotal); // calcTotalはquantityに依存しているのでこの関数をstorageに保存
console.log(total); // -> 300

item.quantity = 5;
reCalculate(); // quantityを変更したのでstorage内部の関数を全て再実行
console.log(total); // -> 500
```

まだ手動で再計算をしていますが、なんとなく処理の流れは掴めたでしょうか。
1. `item.quantity`に対して、依存関数を入れておく
2. `item.quantity`が変更されたら、依存関数を全て再計算する

では、上の機能を`Dep`というクラスにまとめてみましょう！（Dep: Dependancy）

## `Dep`クラスの導入
vueの内部で使われている命名と合わせるために、先程とは名前を変えていますが、機能は変わりません！

- `storageForQuantity` -> `subscribers`: 依存関数を保存する配列
- `record` -> `depend`: 依存関数を追加するメソッド
- `reCalculate` -> `notify`: 依存関数を再計算するメソッド

また、`target`という変数を導入しました。dependする時にtargetの中に入っている関数を依存関数として追加します。
（これによってdependが副作用のある関数になってしまうのですが、こうしないと後々困るので甘んじて受け入れてください...本家vueもこのやり方です...）

```ts:level3.ts
let target: Function = () => {}; // 依存関数を保存する際の対象関数を入れておく変数

interface Dep {
  subscribers: Function[];
}
class Dep {
  constructor() {
    this.subscribers = [];
  }
  depend() {
    if (target && !this.subscribers.includes(target)) {
      this.subscribers.push(target);
    }
  }
  notify() {
    this.subscribers.forEach((func) => func());
  }
}

const item = {
  price: 100,
  quantity: 3
};
let total: number = 0;
const calcTotal = () => (total = item.price * item.quantity);

target = calcTotal;  // quantityの依存先をtargetに入れておくことで後でdependできる
calcTotal(); // 初期totalの計算

const dep = new Dep();
dep.depend();
console.log(total); // -> 300

item.quantity = 5;
dep.notify();
console.log(total); // -> 500
```

そろそろ手動で依存関数を追加したり再実行するのに飽きてきましたね...次のセクションではこの部分を自動化したいと思います。

## Object.definePropertyの導入
`Object.defineProperty`というメソッドを使います。
詳細はMDNドキュメントを見ていただきたいですが、このメソッドは**オブジェクトのプロパティを宣言する際にgetter, setterを定義できる**というものです。
詳しくは[こちら](https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty)

これを用いて、

```js
get() {
  depend(`プロパティを呼び出した関数`) // 依存関数を保存
  return `プロパティの値`
}
set(value) {
  `value`をセット
  notify() // 依存関数を再計算
}
```

のように書けば、

- 値を参照した(`item.quantity`)時に`getter`が呼ばれて関数を`subscribes`に保存
- 値を変更した(`item.quantity = 5`)時に`setter`が呼ばれて依存関数を全て再計算

することができます。これを実装すればほぼ完成です！！

```level4.ts
let target: Function = () => {}; // 依存関数を保存する際の対象関数を入れておく変数

interface Dep {
  subscribers: Function[];
}
class Dep {
  constructor() {
    this.subscribers = [];
  }
  depend() {
    if (target && !this.subscribers.includes(target)) {
      this.subscribers.push(target);
    }
  }
  notify() {
    this.subscribers.forEach((func) => func());
  }
}

const item = {
  price: 100,
  quantity: 3
};
let total: number = 0;
const calcTotal = () => (total = item.price * item.quantity);

// itemのそれぞれのプロパティに対してDepを作成し、getterではdepend, setterではnotifyをする
Object.keys(item).forEach(key => {
  let _value = item[key];
  const dep = new Dep();
  Object.defineProperty(item, key, {
    get() {
      dep.depend();
      return _value;
    },
    set(value) {
      _value = value
      dep.notify()
    }
  })
})

target = calcTotal; // quantityの依存先をtargetに入れておくことで後でdependできる
calcTotal(); // 初期totalの計算

console.log(total); // -> 300

item.quantity = 5;

console.log(total); // -> 500 （quantityを書き換えただけなのに！！！！）
```


最後を見てください！！！
`item.quantity`を書き換えただけなのにも関わらず**自動的**に`total`が更新されました🎉
これにて**リアクティブ性の獲得**を達成することができました。

最後はもう少し処理を抽象化します。

## watcherの追加（おまけ）
前回までは`target`に依存関数を入れてから関数を実行して...という処理を行なっていました。しかし、これは関数が複数になった場合に大変なことになってしまいます。
そこで、関数群をまとめて上記の処理を行なってくれる`watcher`関数を作成します。

今回は`item.taxRate`を追加し、税込みの総額を入れておく`totalWithTax`もリアクティブな変数にしてあります。

おまけ程度に、オブジェクトを渡すとリアクティブにしてくれる`initData`関数も作成しました。

```ts:level5.ts
let target: Function | null = null; // 依存関数を保存する際の対象関数を入れておく変数

interface Dep {
  subscribers: Function[];
}

class Dep {
  constructor() {
    this.subscribers = [];
  }
  depend() {
    if (target && !this.subscribers.includes(target)) {
      this.subscribers.push(target);
    }
  }
  notify() {
    this.subscribers.forEach((func) => func());
  }
}

const watcher = (funcObj: Record<string, Function>) => {
  Object.values(funcObj).forEach((func) => {
    target = func;
    func();
    target = null;
  });
};

const initData = (dataObj: Object) => {
  Object.keys(dataObj).forEach((key) => {
    let _value = dataObj[key];
    const dep = new Dep();
    Object.defineProperty(dataObj, key, {
      get() {
        dep.depend();
        return _value;
      },
      set(value) {
        _value = value;
        dep.notify();
      }
    });
  });
}

const item = {
  price: 100,
  quantity: 3,
  taxRate: 1.1,
};
let total = 0;
let totalWithTax = 0;
const calcTotal = () => (total = item.price * item.quantity);
const calcTotalWithTax = () => (totalWithTax = item.price * item.quantity * item.taxRate);

// itemをリアクティブに
initData(item)

// vueで言うところのcomputedのような関数をwatcherに渡す。
watcher({
  calcTotal,
  calcTotalWithTax,
})

console.log(`total: ${total}, totalWithTax:${totalWithTax}`); // -> 300
// -> total: 300, totalWithTax:330 

item.quantity = 5;

console.log(`total: ${total}, totalWithTax:${totalWithTax}`); // -> 300
// -> total: 500, totalWithTax:550 
```

# おわりに
ここまで読んでくださりありがとうございます！

Vueのリアクティブシステムがどのように実現されているかを説明しました。実際のvueもこのような実装になっているので興味がある方は覗いてみると面白いと思います！（vueのレポジトリ内で`depend()`や`notify()`で検索すると出てくると思います）

Vue3系のリアクティブシステムは`Proxy`を使っているらしく、こちらも勉強したら記事を書こうと思います。

少しでも参考になったという方はいいね！お願いします！

[今回使ったコード](https://codesandbox.io/s/vue-reactive-system-example-nm1bi)