一、手写一个vue数据劫持
二、介绍
这里学习的是 Vue 2.x 下双向绑定的方法,是 数据劫持 + 订阅发布者模式。用 Object.defineProperty(obj,prop,descriptor) 方法去绑定 data 中的值,然后在发现值变化的时候触发更新函数,去更新视图。
2.1介绍下函数
Object.defineProperty(obj,key,descriptor)
`obj` : 目标对象。
`key` : 需要定义的属性或方法的名称。
`descriptor` : 目标属性所拥有的特性。
`descriptor` 有以下的属性:
// 1. value: 属性的值
// 2. writable: 如果为false,属性的值就不能被重写。
// 3. get: 一旦目标属性被访问就会调回此方法,并将此方法的运算结果返回用户。
// 4. set: 一旦目标属性被赋值,就会调回此方法。
// 5. configurable: 可否被修改,如果为false,writable, configurable, enumerable 就不能改。
// 6. enumerable: 是否可被枚举,就是在循环或者 keys 的时候是不是可以被遍历到。
2.2 举个小栗子
let data = {
name: 'nys',
}
Object.keys(data).forEach(key=> {
Object.defineProperty(data, key, {
enumerable: true, //可被枚举
configurable: true, //可被修改
get: function(){
console.log('get');
},
set: function(){
console.log('name changed');
}
})
})
console.log(data.name)
// get
data.name = 'test'
// name changed
三、发布订阅
上面对 data 的值进行了监听,在 set 函数里,当数据变化,对其有操作就可以被监听到,然后就可以对数据视图做出改变。但是同时也存在一个问题,当数据被初始化的时候,系统对每个数据进行了订阅和监听,如果后期在对象上加一个属性,那么就不会被监听到,所以需要使用 Vue.set() 。
3.1 订阅器Dep
为了完成了数据的’可观测’,我们需要创建一个依赖收集容器,用来容纳所有的“订阅者”,我们就可以在数据被使用的时候通知那些依赖该数据的视图更新了,一旦数据发生变化,就统一通知更新。创建消息订阅器Dep:
function Dep(){
let self = this;
self.subs = [];
self.target = null;
// 添加订阅
self.addSub = function(data){
self.subs.push(data);
}
// 判断是否增加订阅者
self.depend = function() {
if (Dep.target) {
this.addSub(Dep.target);
}
}
// 通知试图更新
self.notify = function(){
self.subs.forEach(sub=>{
sub.update();
})
}
}
然后我们把数据劫持和添加订阅改造下并封装成个函数:
function defineReactive(data, key, val) {
let dep = new Dep();
Object.defineProperty(data, key, {
get() {
dep.depend();
return val;
},
set(newVal) {
val = newVal;
dep.notify();
}
})
}
function observable(data) {
if (!data || typeof data !== 'object') {
return;
}
let keys = Object.keys(data);
keys.forEach((key) => {
defineReactive(data, key, data[key])
})
return data;
}
3.2 订阅者Watcher
订阅者 Watcher 在初始化的时候需要将自己添加进订阅器 Dep 中,那该如何添加呢?我们已经知道监听器 Observer 是在 get 函数执行了添加订阅者 Watcher 的操作的,所以我们只要在订阅者 Watcher 初始化的时候触发对应的 get 函数去执行添加订阅者操作即可,那要如何触发 get 的函数,再简单不过了,只要获取对应的属性值就可以触发了,核心原因就是因为我们使用了 Object.defineProperty( ) 进行数据监听。这里还有一个细节点需要处理,我们只要在订阅者 Watcher 初始化的时候才需要添加订阅者,所以需要做一个判断操作,因此可以在订阅器上做一下手脚:在Dep.target 上缓存下订阅者,添加成功后再将其去掉就可以了。订阅者 Watcher 的实现如下:
function watcher(vm, exp, cb) {
let self = this;
self.vm = vm; //实例对象
self.exp = exp; //绑定的属性值
self.cb = cb; //视图更新函数
self.value = self.get();
self.update = function () {
let value = self.vm.data[self.exp];
let oldVal = self.value;
if (value !== oldVal) {
self.value = value;
self.cb.call(self.vm, value, oldVal);
}
}
self.get = function() {
Dep.target = self; // 缓存自己
let value = self.vm.data[self.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放自己
return value;
}
}
4、总结
大概的流程就是,我们用 Object.defineProperty( ) 设置一个监听器,用来监听所有的属性,如果发生了变化就通知订阅者,是不是要更新,然后通过订阅器来同意管理订阅者。