在vue2中,实现监听的主要方法是 Object.defineProperty ,将一个对象的每一个属性,都变为getter/setter的方式。但是在数据和视图中间,还做了很多其他的工作。
1、如何实现object的变化侦测
想要实现数据侦测,首先要获取到两个重要的时机:数据赋值和数据读取。当数据读取时,我们来确定有哪些地方使用了数据;当数据赋值时,我们来通知这些地方,数据已经更新:
解释下这个图
data:源数据
observer :给数据源增加getter 和 setter
getter:获取数据,这时候要记录哪些地方,用到了数据,记录在 Dep 中
setter:设置数据,这时候,要通知用到了这些数据的地方,数据已经更新
watcher :绑定某个UI,或者其他的东西。代表了一个使用了数据的东西,就是getter中“记录哪些地方”的那个地方
dep:容器,存储watcher作用。
2、代码实现
首先实现 Observer 部分,这部分的作用,是将一个对象的每一个属性,都转为getter/setter。注意,Observer 只处理 对象,不处理数组。
class Observer{
constructor(data){
this.value = data;
// 区分 object 还是 array
if(Array.isArray(this.value)){
// 当传入的是 数组 时的处理逻辑
}else {
this.walk(this.value);
}
}
/**
* walk 方法,将对象的每一个属性,都转为 getter/setter
* 只有数据类型是 object 时才调用
* 所以,遍历调用 defineReactive,defineReactive是封装了Object.defineProperty的方法
*/
walk(d){
let keys = Object.keys(d);
for(let k of keys){
defineReactive(d,k,d[k]);
}
}
}
因为 Observer 中调用的 defineReactive ,我们继续实现这个函数:
function defineReactive(data,key,val){
if(typeof val === 'object'){
new Observer(val)
}
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get() {
// 01
// get 时,存储哪些地方使用了数据
// 也就是将使用了数据的watcher,存储到 dep 中
return val
},
set(v) {
if(v !== val){
// 02
// set 时,通知使用了数据的地方
// 也就是将dep中存储的watcher,都调用一遍
val = v;
}
}
})
}
我们首先实现一个Dep,用来存储watcher。这个dep应该具备如下功能:
- 能存储watcher
- 新增一个watcher进行存储
- 当数据变化时,将新旧值,传入watcher
class Dep{
constructor() {
// let watchers = []
this.watchers = []
}
/**
* 添加 watcher
*/
apend(w){
this.watchers.push(w)
}
/**
* 更新时,通知依赖
* 将更新后的值,和原来的值,都传递给依赖
* cb 依赖在值变化时的回调方法
*/
update(newVal,oldVal){
for(let k of this.watchers){
k.cb(newVal,oldVal)
}
}
}
我们收集依赖时,不是直接收集使用 数据 的依赖,而是收集一个叫做watcher的东西。watcher绑定依赖。我们数据变化时,先通知到对应的watcher,watcher在去操作对应的依赖。所以,实现watcher如下:
/**
* 实现 watcher 构造函数
* */
class Watcher{
/**
* @param vm : 绑定的vm实例
* @param expOrFn : 在这个vm 实例上,要操作的属性的路径
* @param cb : 数据变化时的回调函数,接受到变化的数值
*/
constructor(vm,expOrFn,cb){
this.vm = vm;
this.cb = cb;
this.vPath = expOrFn;
}
/**
* 根据传入的 属性路径,获取对象对应路径下的值
* @param path
*/
getObjVal(path){
// 会对具体情况做一些容错
let p_list = path.split('.');
let v = this.vm['data'];
for(let k of p_list){
v = v[k]
}
return v;
}
/**
* 将当前 watcher 实例,添加到 window.target 上
* 然后 通过 读取属性,来触发依赖收集
* 将当前 watcher 添加到依赖收集里
*/
addDep(){
window.target = this;
this.value = this.getObjVal(this.vPath);
window.target = undefined;
}
}
这时候我们在修改下 defineReactive 函数,使其依赖收集规范化:
function defineReactive(data,key,val){
if(typeof val === 'object'){
new Observer(val)
}
let dep = new Dep();
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
get() {
// 01
// get 时,存储哪些地方使用了数据
// 也就是将使用了数据的watcher,存储到 dep 中
dep.apend(window.target)
return val
},
set(v) {
if(v !== val){
// 02
// set 时,通知使用了数据的地方
// 也就是将dep中存储的watcher,都调用一遍
val = v;
dep.update(v,val)
}
}
})
}