简单了解Vue2.0+的数据双向绑定
首先,我们提到MVVM双向数据绑定时,想到的先是:数据改变导致视图改变,视图改变同时数据发生改变。然后,我们紧接着就能说出Vue2的数据双向绑定是通过Object.defineProperty()
方法对data
中的数据进行数据劫持,转化为getter/setter
,当数据变化时通知视图更新。下面我们来一步步的实现一下这个绑定过程。
回顾一下Object.defineProperty()
直接先上一个例子,了解一下Object.defineProperty()
可以做些什么。
let testName = "李四"
let person = {
name: testName,
age: 23
}
当我们输出person
时,我们当然知道结果如下:
可是,当我们修改testName
时,再次输出person
时,是不是想当然的以为应该输出修改后的值?我们看一下。
可是,事实上却不是,我们看到的效果好像只有在初始化的时候对person.name
进行了赋值。接下来我们使用Object.defineProperty()
来实现一下我们认为的效果。
let testName = "李四"
let person = {
name: testName,
age: 23
}
Object.defineProperty(person, 'name', {
get: function () {
console.log("name属性被读取了");
return testName
},
set: function (newVal) {
console.log("name属性被修改了");
val = newVal
}
})
当person.name
被访问的时候会调用get
方法,被赋值时会调用set
方法,我们修改输出一下:
很明显实现了预期的效果。而在Vue的数据双向绑定时,我们也是通过Object.defineProperty()
的get/set
方法知道什么时候被访问,什么时候被赋值,然后通过发布-订阅的方式完成视图更新。
实现一个数据监听器(Observer)
根据上面的例子,我们首先实现一个数据监听器,用来监听数据的所有属性的变化,如有变化则通知视图更新。
let person = {
name: "李四",
age: 23
}
let p = Observer(person)
function Observer(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
return obj
}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`${key}属性被读取了`);
return val;
},
set(newVal) {
val = newVal;
console.log(`${key}属性被修改了`);
}
})
}
到这里,我们这个对象的所有属性都进行了监听。
实现一个订阅器(依赖收集)
上一步,完成了对所有属性的监听,知道数据什么时候发生变化,那我们就可以在数据发生改变的时候通知依赖该数据的视图进行更新,这就是上面提到的发布订阅者模式我们需要实现一个订阅者容器,用来存放所有的订阅者,当数据发生变化时,通知对应的订阅者,执行它的更新函数。
function defineReactive(obj, key, val) {
let dep = new Dep()
Object.defineProperty(obj, key, {
get() {
dep.depend();
console.log(`${key}属性被读取了`);
return val;
},
set(newVal) {
val = newVal;
console.log(`${key}属性被修改了`);
dep.notify()
}
})
}
class Dep {
constructor() {
this.subs = []
}
//增加订阅者
addSub(sub) {
this.subs.push(sub)
}
//判断是否增加订阅者
depend() {
if (Dep.target) {
this.addSub(Dep.target)
}
}
//通知订阅者更新
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
Dep.target = null
实现订阅者
class Watcher {
constructor(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器的操作
}
get() {
Dep.target = this; // 缓存自己
let value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
update() {
let value = this.vm.data[this.exp];
let oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
}
}
vm
是Vue的实例,
exp
是node节点的属性值,
cb
是更新函数
一个简单的订阅者完成,我们来测试一下:
但是在Vue2.0+中,当对对象属性进行删除或者新增或者根据索引修改数组内容时,双向绑定和响应式还是有些问题的:
let person = {
name:"张三",
age:15,
hobbies:["抽烟","喝酒","烫头"]
}
delete person.name
person.sex = "男"
person.hobbies[0] = "学习"
但是可以通过Vue提供的API进行操作:Vue.set(),Vue.delete
细节正在补充中。。。