vue每个组件实例vm都有一个渲染watcher。每个响应式对象的属性key都有一个dep对象。所谓的依赖收集,就是让每个属性记住它依赖的watcher。但是属性可能用在多个模板里,所以,一个属性可能对应多个watcher。因此,在vue2中,属性要通过dep对象管理属性依赖的watcher。在初始化时编译器生成render函数,此时触发属性的依赖收集dep.depend。组件挂载完成后,操作页面,当数据变化后,对应的响应时对象会调用dep.notify方法通知自己对应的watcher更新。在watcher实例中有updateComponent方法,可以进行对应组件的更新。
依赖收集的作用
假设我们现在有一个全局的对象,我们可能会在多个 Vue 对象中用到它进行展示。
let globalObj = {
text1: 'text1'
};
let o1 = new Vue({
template:
`<div>
<span>{{text1}}</span>
<div>`,
data: globalObj
});
let o2 = new Vue({
template:
`<div>
<span>{{text1}}</span>
<div>`,
data: globalObj
});
这个时候,我们执行了如下操作。
globalObj.text1 = 'hello,text1';
我们应该需要通知
o1
以及o2
两个vm实例进行视图的更新,「依赖收集」会让text1
这个数据知道“哦~有两个地方依赖我的数据,我变化的时候需要通知它们~”。 最终会形成数据与视图的一种对应关系,
dep是跟着key走的还是object走的
在 Vue 中,
Dp
e
对象是跟着对象的属性(key)走的,而不是跟着整个对象走的。每个响应式数据(如data
中的属性)都会有一个对应的Dep
实例,Vue 会为对象的每个属性创建一个独立的Dep
实例来管理依赖关系。当访问对象的某个属性时,Vue 会将该属性对应的Dep
实例与当前的Watcher
实例建立关联,从而实现依赖收集和更新机制。
发布-订阅设计模式
在vue2源码设计过程,参考了发布订阅的设计模式。发布订阅和观察者有一个区别,就是发布订阅的发布者和订阅者之间没有直接的依赖关系,通过中间件进行消息传递。观察者模式中,观察对象直接锁定目标,当模板对象发生变化时,直接通知观察者。
发布订阅模式:发布订阅模式中,发布者和订阅者之间的耦合度较低,发布者和订阅者之间通过事件或消息进行通信,彼此不直接依赖。发布订阅模式具有更好的扩展性,可以动态添加新的订阅者或发布者,不影响现有的系统结构。
观察者模式:观察者模式中,目标对象和观察者对象之间的耦合度较高,观察者对象直接订阅目标对象,目标对象需要维护观察者对象的列表。观察者模式在设计时需要明确目标对象和观察者对象之间的关系,扩展性相对较差。
在 Vue 2 中的依赖收集过程中,主要有以下角色:
-
发布者(Dep):在 Vue 2 中,
Dep
(Dependency)充当了发布者的角色。Dep
是一个依赖收集器,用于管理依赖关系。每个响应式数据(如data
中的属性)都会有一个对应的Dep
实例,用于存储依赖于该数据的 Watcher 实例。 -
订阅者(Watcher):在 Vue 2 中,
Watcher
充当了订阅者的角色。Watcher
是一个观察者对象,用于监听数据的变化并执行相应的回调函数。当数据发生变化时,与该数据相关的Watcher
实例会被通知,从而执行更新操作。
整个过程说人话就是:
初始时模板经过render函数渲染,render过程中,模板new一个watcher实例,并且在Dep这个类中,将该wacher实例赋给Dep.target。然后渲染过程中对模板中使用到的数据进行响应式定义。就是通过Object.defineProperty那套对对象中的所有属性拦截,重写get和set方法。get和set分别在读取数据和更新数据的时候自动访问到。这个dep对象跟着响应式对象的key属性走的,每个属性key都对应一个dep实例。
在访问数据时触发get方法,将之前存的Dep.target的watcher实例绑定在当前key的dep对象中。在修改数据的时候触发set方法,dep对象更新key所关联的watcher。通过watcher取更新页面。进行组件渲染
伪代码实现
发布者Dep
首先我们来实现一个订阅者
Dep
,它的主要作用是用来存放Watcher
观察者对象。
class Dep {
constructor() {
this.subs = []; /* 用来存放Watcher对象的数组 */
this.target = null; /**用来存放当前watcher对象 */
}
addSub(sub) {
this.subs.push(sub); /* 在subs中添加一个Watcher对象 */
}
notify() {
/* 通知所有Watcher对象更新视图 */
this.subs.forEach((sub) => {
sub.update();
});
}
}
为了便于理解我们只实现了添加的部分代码,主要是两件事情:
- 用
addSub
方法可以在目前的Dep
对象中增加一个Watcher
的订阅操作;- 用
notify
方法通知目前Dep
对象的subs
中的所有Watcher
对象触发更新操作。
订阅者Watcher
watcher在实例化后会更新Dep的静态属性target。让Dep.target存储当前渲染的模板watcher
class Watcher {
constructor() {
Dep.target =
this; /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
}
update() {
console.log("更新视图");
}
}
Dep.target = null;
Observer和defineReactive
首先在
observer
的过程中会注册get
方法,该方法用来进行「依赖收集」。在它的闭包中会有一个Dep
对象,这个对象用来存放 Watcher 对象的实例。其实「依赖收集」的过程就是把Watcher
实例存放到对应的Dep
对象中去。get
方法可以让当前的Watcher
对象(Dep.target)存放到它的 subs 中(addSub
)方法,在数据变化时,set
会调用Dep
对象的notify
方法通知它内部所有的Watcher
对象进行视图更新。
//observer观察对象
function observer(data) {
if (typeof data != "object" || data == null) {
return;
}
//先不考虑数组,考虑对象的响应式
Object.keys(data).forEach((key) => {
defineReactive(data, key, data[key]);
});
}
//对象的属性key定义响应式
function defineReactive(obj, key, val) {
observer(val); //递归调用val,防止对象的val也是对象
const dep = new Dep(); //对每个key生成一个dep实例
Object.defineProperty(obj, key, {
//使用defineProperty重写get和set方法
get: function reactiveGetter() {
dep.addSub(Dep.target); //将Dep.target当前模板wacher实例
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) return;
observer(newVal);
dep.notify();
},
});
}
那么「依赖收集」的前提条件还有两个:
- 触发
get
方法;- 新建一个 Watcher 对象。
模拟new Vue()
在template生成-》进行初始化操作=》定义数据响应式=》模板渲染=》挂载前定义好更新方法并为模板创建一个watcher实例。在访问响应式属性的时候,defineReactive里的get方法会将当前wacher加入到dep中。在修改属性的时候,deReactive里的set方法会将利用dep通知wacher,而watcher内部有通知组件更新的方法。
这样就实现了数据发生变化,所有依赖的模板都会更新。
function Vue(options) {
this._data = options.data;
observer(this._data);//定义响应式
}
Vue.prototype.$mount = function (el) {
const options = this.$options;
const { render } = compileToFunctions(options.template);
options.render = render;
return mountComponent(this, el);
};
Vue.prototype._render = function () {
let vNode = this.$options.render.call(this);
return vNode;
};
function mountComponent(vm, el) {
let updateComponent = () => {
vm._update(vm._render());
};
new Watcher(vm, updateComponent);
}
const vm = new Vue();
vm.$mount(el);
源码解析
源码就去看下面两个,一个是vue执行过程,定义响应式、模板渲染、更新的逻辑;在instance文件夹下。一个是响应式、依赖收集的属性和方法,在observer文件下。
如果你想看render函数过程,要看编译时被重写的$mount方法,那里是render函数生成的核心。 vue-main\src\platforms\web\runtime-with-compiler.ts
响应式入口
init.ts文件在new Vue,并且执行._init方法时完成vue一些初始化和生命周期钩子函数。这里就调用了initState方法。这个方法处理vue实例的相关数据,通过调用oberver完成数据的观测,从而进行响应式依赖收集,这是数据响应式的关键入口。
依赖收集入口
_init方法中,最后是不是执行了$mount方法。这个$mount方法中调用了mountComponent。在mountComponent方法里通过new Watcher创建一个watcher实例。这里是依赖收集的入口。
Observer类
Observer类通过构造方法,实现了对响应式对象、数组添加响应式的功能。
对于数组重写数组原型的push\pop\splice\unshift\shiift\reverse\sort方法。对于对象通过defineReactive定义对象key的响应式。
defineReactive函数
定义响应式的核心方法,在这个方法中,首先定义一个dep对象,dep是一个闭包,在defineReactive之后后仍然能被get和set方法访问到。
递归处理val,进行响应式观察observe(val)
使用Object.defineProperty定义get方法。在使用属性的时候,通过dep.depend将当前模板渲染watcher加入到key的依赖中。
递归处理子对象。
Dep类
dep类是依赖收集的核心。定义了一个target静态变量,全局使用。定一个subs数组,用于存储wathcer实例。并提供了四种方法:addSub\removeSub\depend\notify。
dep.depend做了什么
depend是dep对象的方法,先去找了全局变量Dep.target。然后调用Dep.target的addDep方法。这个Dep.target其实是一个watcher对象。在addDep方法里,让dep对象调用了自身的addSub方法将这个Dep.target也就是这个watcher实例加入到subs中。
这块写的好绕对吧,源码实现里,dep对象没有自己去调addSub方法,而是让wacher实例转了个手,wacher实例调自己的addDep,然后这个addDep去调的addSub方法。 为什么呢?因为watcher也想记住它对应哪些dep对象。watcher里维护一个newDeps数组,里面存放了相关的dep对象。所以watcher和dep对象是多对多的关系!
Dep.target是什么
前面我们默认这个Dep.target就是当前模板渲染wacher,为什么呢,什么时候放到Dep.target里的了
在dep.ts这个文件里对外抛出了pushTarget方法这里可以修改Dep.target值
在源码里查找,发现在watcher的get方法里调用了pushTarget方法 。而get在watcher的构造函数里使用。说明在生成wacher实例的时候,如果不是lazy,watcher执行会自动调pushTarget方法,将Dep.target更新为当前的wacher实例。
当watcher重新执行或计算的时候会再次调get方法,更新Dep.target
因此,结论就是,Dep.target就是当前watcher实例对象
在去看dep.depend方法,除了前面说的addDep绕了一圈将这个Dep.target放到了dep的subs数组里。还调用了Dep.target的,也就是watcher实例的onTrack方法,这个方法用于在追踪依赖时执行额外的调试操作。
dep.notify方法
notify
方法的主要作用是通知所有订阅者进行更新操作,确保它们按照正确的顺序执行更新,并在开发环境下提供调试信息。这样可以保证在数据变化时,所有订阅者都能及时更新自身状态。
首先方法会对订阅者列表
this.subs
进行稳定化处理,过滤掉可能为null
的订阅者,并将剩下的订阅者转换为DepTarget
类型的数组subs
。如果在开发环境下(
__DEV__
)且不是异步模式(config.async
为假),则需要对订阅者列表进行排序,以确保它们按正确的顺序触发更新。通过对订阅者数组subs
按照id
属性排序,保证它们按照正确的顺序执行。遍历订阅者数组
subs
,对每个订阅者执行以下操作:
- 如果在开发环境下且传入了
info
参数,则调用订阅者的onTrigger
方法(如果存在),并传入包含额外信息的对象{ effect: subs[i], ...info }
。- 调用订阅者的
update
方法,用于执行订阅者的更新操作。
watcher
前面说dep收集的subs订阅者,是不是watcher啊,那watcher是干啥的呢
首先,看下,watcher是在组件挂载前生成的 ,
在响应式数据收集依赖里,我们关注的watcher上的以下几个属性:deps、newDeps数组;depIds、newDepIds的set对象;记录watcher模板关联的dep对象。id集合的作用是保证deps、newDeps的唯一性,防止dep被重复添加。
核心方法——get方法,主要在new Watcher创建实例的时候调用,创建Dep.target=当前wacher实例
核心方法——addDep,在响应式数据访问的时候,响应式属性通过Object.defineProperty重写的get方法中的dep.depend方法,通过Dep.target拿到当前wacher实例的访问。Dep.target通过调用addDep方法,完成了wacher和dep对象的双向记录。wacher将dep对象记录在当前的newDeps数组中;而dep对象通过调用addSub方法,将wacher实例记录在自己的subs数组中。
核心方法——cleanupDeps,完成wacher和dep对象的相互清除操作
OK,到这里,你对vue依赖收集是不是有了更深刻的理解呢