Vue.jsのリアクティブシステムみたいなのを作ってみた。
Vue.jsの中身を読んでみる!というのを最近やってみていて、算出プロパティ部分を読んでいたのですが、 実際に同じようなコードが書けないかなあと思い、少し書いてみました。
Vue.js 算出プロパティ部分のコード
Vue.jsでの実現方法は多分こんな感じ
initData
でobserve
が呼ばれている
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 */) }
↓
observe
はObserver
オブジェクトを生成
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]) } }
↓
defineReactive
でObject.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
あるオブジェクトに新しいプロパティを直接定義したり、オブジェクトの既存のプロパティを変更したりして、そのオブジェクトを返します。
実際に書いてみる
<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; });
以下のようになります。