フロントエンドエンジニアをやっています。
頑張るから読んでほしい。

Vue.jsのリアクティブシステムみたいなのを作ってみた。

Vue.jsの中身を読んでみる!というのを最近やってみていて、算出プロパティ部分を読んでいたのですが、 実際に同じようなコードが書けないかなあと思い、少し書いてみました。

Vue.js 算出プロパティ部分のコード

Vue.jsでの実現方法は多分こんな感じ

initDataobserveが呼ばれている

vue/state.js at dev · vuejs/vue · GitHub

function initData (vm: Component) {
  let data = vm.$options.data
  // ...
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  
  // observe data
  observe(data, true /* asRootData */)
}

observeObserverオブジェクトを生成

vue/index.js at dev · vuejs/vue · GitHub

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

ObserverクラスでdefineReactiveが呼ばれてる。

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
  // ...
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

defineReactiveObject.defineProperty を使用してリアクティブシステムを実現している...?

vue/index.js at dev · vuejs/vue · GitHub

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })



自分で同じような仕組み書いてみる

Object.definePropertyを使用しているみたいなので調べてみました。

Object.defineProperty

あるオブジェクトに新しいプロパティを直接定義したり、オブジェクトの既存のプロパティを変更したりして、そのオブジェクトを返します。

developer.mozilla.org

実際に書いてみる

<html>
  <body>
    <ul>
      <li>
        <p>テキスト</p>
        <p id="message"></p>
        <input type="text" id="text">
      </li>
      <li>
        <p>チェックボックス</p>
        <input type="checkbox" id="checkbox">
        <label for="checkbox" id="label-checkbox"></label>
      </li>
      <li>
        <p>セレクトボックス</p>
        <select>
          <option disabled value="">Please select one</option>
          <option>A</option>
          <option>B</option>
          <option>C</option>
        </select>
        <p id="selectedText"></p>
      </li>
    </ul>
  </body>
</html>[f:id:cidermitaina:20190128100626g:plain]
const data = {};

const p = document.getElementById('message');
const label = document.getElementById('label-checkbox');

const input = document.querySelectorAll('input');
const inputTypeText = document.getElementById('text');
const inputTypeCheckbox = document.getElementById('checkbox');
const select = document.querySelector('select');
const selectedText = document.getElementById('selectedText');

p.textContent = inputTypeText.value = 'Hello, World!';
label.textContent = inputTypeCheckbox.value = 'false';
selectedText.textContent = inputTypeCheckbox.value = 'selected';

// リアクティブプロパティの定義
Object.defineProperties(data, {
  message: {
    get() { return message; },
    set(newVal) {
      message = newVal;
      p.textContent = message;  
    }
  },
  checked: {
    get() { return checked; },
    set(newVal) {
      checked = newVal;
      label.textContent = checked;  
    }
  },
  selected: {
    get() { return selected; },
    set(newVal) {
      selected = newVal;
      selectedText.textContent = selected;  
    }
  },
});

input.forEach((el) => {
  el.addEventListener('input', (ev) => {
    data.message = ev.target.value;
    data.checked = ev.target.checked;
  });
});

select.addEventListener('change', (ev) => {
  data.selected = ev.target.value;
});

以下のようになります。

f:id:cidermitaina:20190128100626g:plain