Vue のリアクティブシステム

勉強会で発表した資料です。
Vue のリアクティブシステム、その中でも依存関係のある関数の収集と再実行をどのように実現しているのかについて解説しています。Object.defineProperty, Dep Class and Watcher.

--- # 話すこと - Vue のリアクティブシステムの一部を説明 - リアクティブシステム
=> モデルの変更がDOMに反映される仕組み --- ![inline](model-dom.png) https://blog.thoughtram.io//angular/2016/02/22/angular-2-change-detection-explained.html --- ![inline](data.png) [リアクティブの探求 — Vue.js](https://jp.vuejs.org/v2/guide/reactivity.html) --- # 一旦サンプルを --- **値段**と**数**から**合計金額**を表示するアプリケーション
<!-- html -->
<div id="app">
  <div>price: {{ price }}</div>
  <div>quantity: {{ quantity }}</div>
  <div>total: {{ total }}</div>
</div>
// js
const app = new Vue({
  el: '#app',
  data: {
    price: 100,
    quantity: 2
  },
  computed: {
    total() { return this.price * this.quantity; }
  }
});
https://stackblitz.com/edit/vue-reactivity(https://stackblitz.com/edit/vue-reactivity) --- - `data` - Vue インスタンスのためのデータオブジェクト - Vue インスタンスが作成されるとリアクティブシステムに追加される - `computed` - Vue インスタンスに組み込まれる算出プロパティ - 算出プロパティは依存関係にもとづきキャッシュされる --- # 今日のゴールは ---
let data = { price: 100, quantity: 2 };
let total = data.price * data.quantity;

console.log(total);
// => 200

data.quantity = 3;
console.log(total);
// => 300 ?
--- # その1 ## 関数を保存 ---
let data = { price: 100, quantity: 2 };

// 再計算できるように関数を保存
let target = 
  () => data.total = data.price * data.quantity;

target();
console.log(data.total);
// => 200

data.quantity = 3;
target();
console.log(data.total);
// => 300

// https://stackblitz.com/edit/vue-reactivity-step-by-step?file=step-96.js
--- - 保存される関数は複数必要 - 依存関係のある関数だけを管理したい --- # その2 ## Dependency Class ---
export let target = null;
export class Dep {
  constructor() {
    this.subscribers= [];
  }
  depend() {
    if (target && !this.subscribers.includes(target)) {
      this.subscribers.push(target);
    }
  }
  notify() {
    this.subscribers.forEach(sub => sub());
  }
}
---
let data = { price: 100, quantity: 2 };
let watcher = 
  () => data.total = data.price * data.quantity;

const dep = new Dep();

target = watcher;
dep.depend();

target();
target = null;

console.log(data.total);
// => 200

data.quantity = 3;
dep.notify();
console.log(data.total);
// => 300

// https://stackblitz.com/edit/vue-reactivity-step-by-step?file=step-97.js
--- # Dependency Class の役割 --- ![inline](dep.png) https://medium.com/vue-mastery/the-best-explanation-of-javascript-reactivity-fea6112dd80d(https://medium.com/vue-mastery/the-best-explanation-of-javascript-reactivity-fea6112dd80d) --- ![inline](dep-2.png) https://medium.com/vue-mastery/the-best-explanation-of-javascript-reactivity-fea6112dd80d(https://medium.com/vue-mastery/the-best-explanation-of-javascript-reactivity-fea6112dd80d) --- ![inline](dep-3.png) https://medium.com/vue-mastery/the-best-explanation-of-javascript-reactivity-fea6112dd80d(https://medium.com/vue-mastery/the-best-explanation-of-javascript-reactivity-fea6112dd80d) --- - プロパティごとに `Dep` インスタンスが必要 - 依存関係のある関数だけを管理したい - (未解決) --- # その3 ## `Object.defineProperty` ---
Object.keys(data).forEach(key => {
  let _value = data[key];
  Object.defineProperty(data, key, {
    get() {
      console.log('get', key, _value);
      return _value;
    },
    set(value) {
      console.log('set', key, _value);
      _value = value;
    }
  });
});
---
Object.keys(data).forEach(key => {
  let _value = data[key];
  const dep = new Dep();
  Object.defineProperty(data, key, {
    get() {
      dep.depend();
      return _value;
    },
    set(value) {
      _value = value;
      dep.notify();
    }
  });
});

// ...

target = watcher;
target();
target = null;
--- # 完成
console.log(data.total);
// => 200

data.quantity = 3;
console.log(data.total);
// => 300
https://stackblitz.com/edit/vue-reactivity-step-by-step?file=step-99.js(https://stackblitz.com/edit/vue-reactivity-step-by-step?file=step-99.js) --- ![inline](data_with_annotations.png) https://medium.com/vue-mastery/the-best-explanation-of-javascript-reactivity-fea6112dd80d(https://medium.com/vue-mastery/the-best-explanation-of-javascript-reactivity-fea6112dd80d) --- # 振り返り - Vue のリアクティブシステムの一部を説明 - `Object.defineProperty` - `Dep` class - `Watcher` --- # 参考資料 - [The Best Explanation of JavaScript Reactivity 🎆 – Vue Mastery – Medium](https://medium.com/vue-mastery/the-best-explanation-of-javascript-reactivity-fea6112dd80d) --- # ご静聴ありがとうございました --- # おまけ1 ## Angular の場合 ---
export class AppComponent  {
  price: number;
  quantity: number;
  constructor() {
    this.price = 100;
    this.quantity = 2;
    (window as any).app = this;
  }
  get total() {
    return this.price * this.quantity;
  }
}

// コンソールから実行
window.app.quantity = 10;
// => DOMは更新されない
// => ボタンクリックなどをすると反映される
https://stackblitz.com/edit/vue-reactivity-case-of-angular(https://stackblitz.com/edit/vue-reactivity-case-of-angular) --- Angular のレンダリングに興味がある方は [日本語訳 Angular 2 Change Detection Explained | ](https://blog.lacolaco.net/post/translation-angular-2-change-detection-explained/) がおすすめです --- # おまけ2 ## Vue インスタンス作成時の実際のコードを追ってみる --- **instance/index** - https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/index.js#L8(https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/index.js#L8) `function Vue (options) {` - https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/index.js#L14(https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/index.js#L14) `this._init(options)` --- **instance/init** - https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/init.js#L16(https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/init.js#L16) `Vue.prototype._init = function (options?: Object) {` - https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/init.js#L57(https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/init.js#L57) `initState(vm)` --- **instance/state** - https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/state.js#L48(https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/state.js#L48) `export function initState (vm: Component) {` - https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/state.js#L54(https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/state.js#L54) `initData(vm)` - https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/state.js#L112(https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/state.js#L112) `function initData (vm: Component) {` --- - https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/state.js#L151(https://github.com/vuejs/vue/blob/v2.5.17/src/core/instance/state.js#L151) `observe(data, true /* asRootData */)` --- **observer/index** - https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js(https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js) `export function observe (value: any, asRootData: ?boolean): Observer | void {` - https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L123(https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L123) `ob = new Observer(value)` - https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L37(https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L37) `export class Observer {` --- - https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L66(https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L66) `defineReactive(obj, keys[i])` - https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L134(https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L134) `export function defineReactive (` - https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L156(https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L156) ` Object.defineProperty(obj, key, {` --- - https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L162(https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L162) `dep.depend()` - https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L188(https://github.com/vuejs/vue/blob/v2.5.17/src/core/observer/index.js#L188) `dep.notify()`