双向数据绑定已是一个谈烂的话题,若谈及原理,想必你们都能提到个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实现方法)