前言
本系列查阅顺序:
- [vue2数据响应式原理——数据劫持(初始篇)]
- [vue2数据响应式原理——数据劫持(对象篇)]
- [vue2数据响应式原理——数据劫持(数组篇)]
- [vue2数据响应式原理——依赖收集和发布订阅]
前几篇我们已经研究过了数据劫持,并多次提到依赖
这个词,这一篇我们就将针对依赖来进行深入探讨:什么是依赖,以及收集依赖和发布订阅。
收集依赖,发布订阅
依赖是谁?
需要用到数据的地方成为依赖!
前面说了在getter中收集依赖,在setter中触发依赖。
那依赖是谁呢?我们需要在访问数据时收集谁,更新数据时触发谁呢?
这个依赖其实就是我们定义的一个类:Watcher
什么是Watcher?
Watcher就是需要用到数据的地方!
Watcher
是个中介角色,数据发生变化时通知它,它再通知其他地方。
关于Wather
,可以先看vue2
中watch
侦听的使用方式:
vm.$watch('a.b.c',function (newVal,oldVal) {
//....
})
看上面的描述可能会开始懵了,别急,我们根据代码来看,新建一个Watcher.js
:
Watcher.js
export default class Watcher {
//vm:监听对象;expOrFn监听的表达式或函数,如a.m;cb:回调函数;
constructor(vm, expOrFn, cb) {
this.vm = vm;
//执行this.getter(),就可以读取a.m的内容
this.getter = parsePath(expOrFn);
this.cb = cb;
this.value = this.get();
}
get() {
//进入依赖收集阶段
window.target = this;
// call(上下文,参数1,参数2,..),这里值将this.getter方法放到this.vm里执行,后面的this.vm是传入this.getter方法的参数
//这里执行this.getter()函数的时候获取了响应式数据的值,触发了响应式数据的getter,即defineReactive中defineProperty的get
//则defineProperty的get就可以通过window.target收集这个依赖
let value = this.getter.call(this.vm, this.vm);
window.target = undefined;
return value;
}
update() {
const oldValue = this.value;
this.value = this.get();
this.cb.call(this.vm, this.value, oldValue);
}
}
function parsePath(str) {
const segments = str.split(".");
return (obj) => {
for (let i = 0; i < segments.length; i++) {
if (!obj) return;
obj = obj[segments[i]];
}
return obj;
};
}
看一下这个Watcher.js
的使用方式:
import Watcher from "./Watcher.js";
let obj = {
a: {
m: 99,
},
};
observe(obj);
new Watcher(obj, "a.m", (newVal, oldVal) => {
console.log("新的值--》", newVal);
console.log("老的值--》", oldVal);
});
结合上面Watcher类的用法来看Watcher.js的内容:
使用Watcher类时需要传入三个参数:
- 一个我们想要监听的对象obj
- 一个监听值在obj中的表达式a.m
- 一个接收两参数的回调函数
根据用法再看Watcher.js
内容就大致能够明白的差不多了,其中parsePath
是我们写的一个工具函数,作用是根据传入的第二个参数表达式获取到指定的值,例如下列用法:
let fn = parsePath("a.m");
let value = fn({
a: {
m: 6,
},
});
console.log(value); //打印结果6
Watcher.js
最重要的部分是:
get() {
//进入依赖收集阶段
window.target = this;
// call(上下文,参数1,参数2,..),这里值将this.getter方法放到this.vm里执行,后面的this.vm是传入this.getter方法的参数
//这里执行this.getter()函数的时候获取了响应式数据的值,触发了响应式数据的getter,即defineReactive中defineProperty的get
//则defineProperty的get就可以通过window.target收集这个依赖
let value = this.getter.call(this.vm, this.vm);
window.target = undefined;
return value;
}
在调用get()
获取数据的时候,Watcher
先是将自己绑定到了 window.target
上,而window.target
是全局的一个唯一的地方
之后调用了 this.getter()
函数,根据前面可以知道this.getter()
其实就是parsePath
返回的一个函数,作用就是获取对应的值
因为在使用Watcher
类之前,数据肯定是响应式的(即已经被observe
),所以Watcher
在获取值的时候肯定会触发响应式数据的getter
所以我们就可以在响应式数据的getter
里根据window.target
这个全局唯一值来获取到当前正在访问数据的依赖,并对其进行收集
那Watcher
收集到哪呢?
我们可以把依赖收集的代码封装成一个Dep
类,它专门用来管理依赖,每个Observer
的实例成员中都有一个Dep
的实例。
说到这你会问,为什么每个Observer
的实例都要有一个Dep
的实例呢?
因为每一个响应式数据都有一个Observer
的实例,而每一个响应式数据都需要一个Dep
实例来存放哪些地方用到了自己(即Watcher
),并且当数据变化发生set
后,需要通过Dep
实例去遍历通知储存在Dep
实例中的所有依赖,所以可以知道:每个响应式数据和Observer
和Dep
都是是一对一的关系,而它们对Watcher
是一对多的关系。
Dep.js
export default class Dep {
constructor() {
//存储自己的订阅者,即Watcher实例
this.subs = [];
}
//添加订阅
addSub(sub) {
this.subs.push(sub);
}
//添加依赖
depend() {
//window.target是我们指定的全局唯一的位置,当有依赖产生时Watcher会将依赖绑定到该位置
if (window.target) {
this.addSub(window.target);
}
}
//通知更新
notify() {
//浅克隆:防止循环引用
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
只看Watcher
和Dep
还不能理解收集依赖以及发布订阅的完整过程,还需要继续看:
Observer.js
在Observer
内创建Dep
实例。
//...
import Dep from "./Dep.js"; //增加
export default class Observer {
constructor(value) {
//每个Observer的实例上都有一个Dep
this.dep = new Dep(); //增加
//.......
}
//.......
}
defineReactive.js
defineReactive
中也需要new
一个Dep
实例,目的是调用Dep
中的depend
和notify
方法:
import observe from "./observe.js";
import Dep from "./Dep.js"; //新增
export default function defineReactive(data, key, val) {
const dep = new Dep(); //新增
if (arguments.length == 2) {
val = data[key];
}
//子元素要进行observe,至此形成了递归调用。(多个函数循环调用)
let childOb = observe(val);
Object.defineProperty(data, key, {
//可枚举
enumerable: true,
//可以被配置,比如delete
configurable: true,
// 读取key时触发 getter
get: function() {
console.log(`你试图访问${data}的${key}属性,它的值为:${val}`);
//如果现在处于依赖收集阶段 //新增
dep.depend(); //新增
if (childOb) { //新增
childOb.dep.depend(); //新增
} //新增
return val;
},
//更改key时触发 setter
set: function(newVal) {
console.log(`你试图改变${data}的${key}属性,它的新值为:${newVal}`);
if (val === newVal) {
return;
}
val = newVal;
childOb = observe(newVal);
dep.notify(); //新增
},
});
}
到这里就能完全明白,文章开头的那句话:
在getter中收集依赖,在setter中触发依赖。
若Watcher
监听了数据,Watcher
内部会先调用this.get()
,将自己赋值到window.target
,之后访问了数据
在getter中收集依赖:当访问数据时触发getter
,执行了 dep.depend()
,并且如果该数据的值也是响应式数据,即childOb
存在,也即 observe(val)
有返回值(这就解释了在数据劫持那部分为什么我们要让 observe
返回ob
,以及为什么要将observe(val)
赋值给childOb
)就执行childOb.dep.depend()
(因为父元素改变了,如果子元素是响应式数据,那父子元素都要收集依赖),dep.depend()会将当前正在使用数据的依赖存放到subs中
在setter中触发依赖:当数据修改时触发setter
,执行了dep.notify()
,继而遍历了subs
里的Watcher
依赖逐个对Watcher
执行update
,Watcher
调用update
进行更新数据,并调用cb
返回新值和旧值
综合以上,当用Watcher
监听某一数据后,只要该数据改变,就会通知到Watcher
,之后就可以在Watcher
中写一些操作让它去通知其它地方
其实对于数组而言,上述触发依赖的方式有所变化,因为数组的修改并不会触发setter
,而是会触发我们对 “ 七个方法 ” 的封装,所以对数组而言是:
在getter中收集依赖,在拦截器中触发依赖。
在数组拦截器array.js
中新增ob.dep.notify()
:
methodName.forEach(function(method) {
const original = arrayProto[method];
def(
arrayMethods,
method,
function(...args) {
//...
//触发依赖
ob.dep.notify(); // 新增
console.log("调用了我们改写的7方法");
return result;
},
false
);
});
之后总结一下:
依赖就是Watcher
。只有Watcher
触发的getter
才会收集依赖,哪个Watcher
触发了getter
,就把哪个Watcher
收集到Dep
中。
Dep
使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有Watcher
都通知一遍。
Watcher
把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter
。在getter
中就能得到当前正在读取数据的Watcher
,并把这个Watcher
收集到Dep
中。
每个响应式数据都有一个dep
实例,里面的subs
存放了用到该数据的Watcher
依赖,让数据变化时发生set
,触发dep.notify
,然后去通知每一个用到该数据的Watcher
。