雙向數據綁定已是一個談爛的話題,若談及原理,想必你們都能提到個defineProperty
。可是,對於如何完整地實現一個雙向數據綁定僞代碼,我想大概不少人都沒有去深究。因而,本文藉着梳理髮布訂閱模式由淺到深地實現一下雙向數據綁定。node
雙向數據綁定的底層設計模式爲:發佈訂閱模式。
舉一個最通俗的🌰:小紅、小明、小白同時關注拼多多的AJ1,當AJ1一降價,三我的都能接到通知。設計模式
class Pinduoduo { constructor() { // 訂閱者 this.subscribers = []; } // 訂閱方法 subscribe({name, callback}) { if (~this.subscribers.indexOf(name)) return; this.subscribers.push({ name, callback }); } // 發佈降價消息 publish() { this.subscribers.forEach(({name, callback}) => { let prize = 666; if (name === '小明') prize = 100; callback && callback(name, prize); }) } } const pinInstance = new Pinduoduo(); const commonFn = (name, prize) => { console.log(`${name}接收到了降價信息,AJ1如今的價格是${prize}`) } // 訂閱 pinInstance.subscribe({ name: '小紅', callback: commonFn }); pinInstance.subscribe({ name: '小明', callback: commonFn }); pinInstance.subscribe({ name: '小白', callback: commonFn }); // 發佈 pinInstance.publish();
// 輸出 // 小紅接收到了降價信息,AJ1如今的價格是666 // 小明接收到了降價信息,AJ1如今的價格是100 // 小白接收到了降價信息,AJ1如今的價格是666
因此——記住實現發佈訂閱模式的兩個要點:
發佈(觸發) & 訂閱(監聽)緩存
藉此咱們還能夠實現一下EventEmitter的僞代碼。app
function EventEmitter() { this.events = Object.create(null); } // 實現監聽方法 EventEmitter.prototype.on = (type, event) => { if (!this.events) this.events = Object.create(null); if (!this.events[type]) this.events[type] = []; this.events[type].push(event); } // 實現觸發方法 EventEmitter.prototype.emit = (type, ...args) => { if (!this.events[type]) return; this.events[type].forEach(event => { event.call(this, ...args); }) } // 執行 function Girl() {} // 實現繼承 Girl.prototype = Object.create(EventEmitter.prototype); const lisa = new Girl(); lisa.on('逛街', () => { console.log('買買買!'); }); lisa.emit('逛街'); // console: 買買買!
下述例子dom
結構基於以下代碼dom
<div id="app"> <input type="text" v-model="data"> <p v-text="data"></p> </div>
不註釋了,相信你們都能看懂。性能
const inputDom = document.getElementsByTagName('input')[0]; const textDom = document.getElementsByTagName('p')[0]; inputDom.addEventListener('input', e => { const val = e.target.value; textDom.innerText = val; });
一、極簡版this
const vm = { data: '' }; const inputDom = document.getElementsByTagName('input')[0]; const textDom = document.getElementsByTagName('p')[0]; Object.defineProperty(vm, 'data', { set(newVal) { if (vm['data'] === newVal) return; // 同時觸發視圖更新 textDom.innerText = newVal; } }); inputDom.addEventListener('input', e => { vm.data = e.target.value; });
二、進階版
假如咱們更換個屬性,或添加v-model
上述代碼就不能複用了。咱們迭代一下,能夠適應多個v-model
的狀況。prototype
首先梳理一下須要作什麼設計
Object.defineProperty
Dom Tree
對v-model
和v-text
進行解析。對v-model
進行事件綁定監聽變化,對v-text
添加訂閱者,訂閱vm
變化實現視圖更新。/* * 定義對象監聽 */ const vm = { data: '' }; function observe(obj) { Object.keys(obj).forEach(key => { let val = obj[key]; Object.defineProperty(obj, key, { get() { return val; }, set(newVal) { if (newVal === val) return; // 更新vm中的數據 val = newVal; } }) }) }
/* * 加入發佈訂閱模式 */ const Dep = { target: null, subs: [], addSubs(sub) { this.subs.push(sub) }, notify() { this.subs.forEach(sub => { sub.update(); }); } }
在getter中,添加watcher
在setter觀測到數據變化時,觸發全部【訂閱者】更新code
// ... get() { // 此時的target已經賦值成當前的watcher實例 if (Dep.target) Dep.addSubs(Dep.target); return val; }, set(newVal) { if (newVal === val) return; // 更新vm中的數據 val = newVal; Dep.notify(); } // ...
接下來定義【訂閱者】watcher
,在本例中能夠理解成每個node節點
function Watcher(node, vm, name) { Dep.target = this; this.node = node; this.vm = vm; // name是綁定數據的key this.name = name; // 將watcher添加進dep中 this.update(); Dep.target = null; } // Watcher包含update方法和get方法 Watcher.prototype = { update() { this.get(); this.node.innerText = this.value; }, // 這裏主要是爲了觸發getter中Dep.addSub get() { this.value = this.vm[this.name]; } }
而後是對相應節點進行解析處理
function complie(node, vm) { if (node.nodeType === 1) { [...node.attributes].forEach(attr => { const name = attr.nodeValue; if (attr.nodeName === 'v-model') { node.addEventListener('input', e => { vm[name] = e.target.value; }) } else if (attr.nodeName === 'v-text') { new Watcher(node, vm, name) } }) } }
如今能夠對每一個節點進行綁定處理了
function MVVM(id, vm) { observe(vm); const node = document.getElementById(id); // 用fragment緩存節點,節約性能開支 const fragment = document.createDocumentFragment(); let child; while(child = node.firstChild) { fragment.appendChild(child) } }
調用
MVVM(vm);
整個代碼初看起來會比較繞,但只要理解observe
、complie
、Dep
、Wacther
這幾個概念,相信就能基本看懂MVVM
了。
(未完待續...待更新Proxy的MVVM實現方法)