前言
双向数据绑定已经是一个谈烂的话题,若谈及原理,想必大家都能提到数据劫持defineProperty
。但是,对于如何完整地实现一个双向数据绑定伪代码,我想大概很多人都没有去深究。于是,本文借着梳理发布订阅模式由浅到深地实现一下双向数据绑定。
发布订阅模式
双向数据绑定的底层设计模式为:发布订阅模式。
- 发布订阅模式也称为观察者模式
- 观察者同时订阅一个对象,当对象发生改变时,每一个观察者都能接收到通知
通俗举例
举一个最通俗的?:小红、小明、小白同时关注拼多多的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
借此我们还可以实现一下EventEmitter的伪代码。
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
结构基于如下代码
<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;
});
defineProperty实现
1、极简版
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;
});
2、进阶版
假如我们更换个属性,或添加v-model
上述代码就不能复用了。我们迭代一下,可以适应多个v-model
的情况。
首先梳理一下需要做什么
- 添加对象劫持器
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观测到数据变化时,触发所有【订阅者】更新
// ...
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实现方法)
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。