Dep 类(事件中心)
subs;
addSub(sub);
notify();
Dep 类代表了一种依赖关系或订阅关系。每个响应式数据对象的每个属性都对应一个 Dep 实例。 Dep 实例的主要作用是:
收集依赖 (订阅者/观察者/watcher 对象)
- **当组件依赖于某个响应式数据属性时,会将组件对应的 Watcher 实例添加到该属性对应的 Dep 实例的 subs 数组中。**这就建立了组件与数据属性之间的依赖关系。
- 翻译一下:看哪些组件( watcher)依赖了(用到了) 当前的响应式数据属性
发布通知
- 当响应式数据属性发生变化时,属性对应的 Dep 实例会通过调用 notify 方法,遍历它的 subs 数组,通知所有依赖于该属性的 Watcher 实例进行视图更新。
总的来说,Dep 类是一个状态管理器,用于管理依赖这个状态的订阅者。
class Dep {
constructor() {
/* 用来存放 Watcher 对象的数组 */
this.subs = [];
}
/* 在 subs 中添加一个 Watcher 对象 */
// 添加订阅者 - 收集依赖
addSub(sub) {
this.subs.push(sub);
}
/* 通知所有 Watcher 对象更新视图 */
// 发布通知 - 通知所有订阅者
notify() {
this.subs.forEach((sub) => {
sub.update();
});
}
}
// 订阅者(观察者) 指的是Watcher对象,Vue中指组件实例
这个 Dep 类实现了发布订阅模式的基本功能。它充当了一个中介的角色,负责管理订阅者(Watcher 对象)和发布者(数据变化)之间的关系。当数据发生变化时,对应的 Dep 实例会通知所有订阅了它的 Watcher 对象进行更新。
在 Vue.js 的响应式系统中, 每个数据对象的每个属性都对应一个 Dep 实例。 当组件渲染时,会生成对应的 Watcher 对象,并将它们添加到相应数据属性的 Dep 实例中。 当数据属性发生变化时,对应的 Dep 实例会通知所有订阅了它的 Watcher 对象,从而触发组件的重新渲染。
// 发布者 - 目标
class Dep {
constructor() {
// 记录所有的订阅者(观察者)
this.subs = [];
}
// 添加订阅者
addSub(sub) {
// 注意,sub这个对象具有update方法,无需判断
this.subs.push(sub);
}
removeSub(sub) {
const index = this.subs.indexOf(sub);
if (index !== -1) {
this.subs.splice(index, 1);
}
// 源码中 这么写的
// this.subs[this.subs.indexOf(sub)] = null;
}
// 发布通知 - 通知所有观察者
notify(...args) {
this.subs.forEach((sub) => {
sub.update(...args);
});
}
}
// 2. 定义具体的观察者(订阅者)
class Watcher {
constructor(name) {
this.name = name;
}
update(...args) {
// 数据变化更新视图
console.log(`观察者 ${this.name} 收到通知,附加信息: ${args.join(", ")}`);
}
}
// 发布者,将记录所有的观察者
const dep = new Dep();
const watcher1 = new Watcher("观察者1");
const watcher2 = new Watcher("观察者2");
const watcher3 = new Watcher("观察者3");
dep.addSub(watcher1);
dep.addSub(watcher2);
dep.addSub(watcher3);
dep.notify("状态发生改变", 123);
Dep 类 => 事件中心
Dep 类扮演了连接数据(发布者)和视图(订阅者)的中间件角色。具体来说,
- 订阅
- 当组件实例化时,会创建与之对应的 Watcher 实例。每个 Watcher 实例在执行依赖收集过程中,会读取它所依赖的响应式数据对象的属性,从而触发属性的 getter 函数。在 getter 中,会调用 dep.depend() 方法,将当前 Watcher 实例订阅到对应属性的 Dep 实例中(添加到 Dep 实例的 subs 数组中)。这相当于订阅了该属性值变化的事件。
- 发布通知
- 当响应式数据发生变化时,会触发对应属性的 setter 方法。在 setter 中,会调用 dep.notify() 方法,遍历该属性对应的 Dep 实例中的所有订阅者(Watcher 实例),并执行它们的 update 方法。这相当于发布了该属性值变化的事件。
- 解耦 Dep 充当了发布者(数据对象)和订阅者(Watcher 实例)之间的中介,实现了两者的解耦。数据对象只需要管理好自身的 Dep 实例,在发生变化时通知 Dep 实例即可。而 Watcher 实例只需订阅感兴趣的 Dep 实例,无需直接与数据对象产生联系。
// Dep 充当了发布者(数据对象)和订阅者(Watcher 实例)之间的中介
// 上面表述不太准确,发布者角色其实是由Observer类充当
// 准确地说,发布者是 Observer,而不是数据对象本身。数据对象只是数据的载体,
// Observer 通过遍历数据对象的属性,并添加 getter/setter 拦截器,
// 从而监听数据变化, 发布通知。
因此, Dep 类像一个事件中心,它维护了一组订阅者(Watcher 实例)列表,负责事件(数据变化)的广播。 当有事件发生时,它会遍历列表,逐个通知订阅者执行相应的操作(重新渲染视图)。这种设计模式使得发布者和订阅者之间解耦,也使系统具有良好的可扩展性。 需要注意的是,Vue 2.x 的响应式系统中每个响应式数据属性都对应一个 Dep 实例,因此可以看作是一种细粒度的发布订阅模式。这与传统的全局事件总线(Event Bus)有所不同,后者一般是整个应用程序级别的事件中心。
Watcher 类(订阅者)
Watcher 类代表了一种订阅者或观察者。 每个组件实例都对应一个 Watcher 实例,Watcher 实例会订阅它所依赖的响应式数据属性对应的 Dep 实例。 Watcher 实例的主要作用是:
订阅数据
- 在组件渲染过程中,Watcher 实例会读取它所依赖的响应式数据属性,从而触发这些属性对应的 Dep 实例的 addSub 方法,将自身添加为订阅者。(订阅数据的变化)
更新视图
- 当被订阅的响应式数据属性发生变化时,相应的 Dep 实例会通知所有订阅它的 Watcher 实例调用 update 方法,从而触发组件的重新渲染,使视图与数据保持同步。
总的来说,Watcher 类是一个观察者,它观察着被依赖的数据,当数据变化时,会被通知去更新视图。
PS: 依赖收集 实际上是把 Watcher 实例作为一个"依赖"添加到了用到当前数据对应的 Dep 实例中。这样当数据发生变化时, Dep 实例就能通知到所有"依赖"它的 Watcher 实例。
可以概括为以下两点:
- 组件渲染时,会读取所需的响应式数据,触发 getter
- 在 getter 中,会把当前渲染的 Watcher 添加到对应数据的 Dep 实例上,完成依赖收集
这种方式实现了:
- 精确捕获 watcher 所依赖的数据
- 建立数据属性与其依赖的 watcher 之间的联系
所以当数据发生变化时,对应的 Dep 就可以准确地通知到依赖自己的那些 Watcher,从而有效地触发组件或计算属性的重新渲染或计算。这是 Vue 响应式系统实现高效更新的关键所在。
Observer 类(发布者)
数据劫持:
- 递归的遍历数据对象的所有属性,使用 Object.defineProperty 为每个属性添加 gette 和 setter
依赖收集
- 当访问数据对象的某个属性时(模板用到了某个数据), 会触发属性的 getter 方法,此时 Observer 会调用 dep.depend将当前组件实例(Watcher 对象)收集为该属性的订阅者
// Dep类
class Dep {
addSub(sub: DepTarget) {
this.subs.push(sub);
}
depend(info?: DebuggerEventExtraInfo) {
// Dep.target 当前watcher实例
if (Dep.target) {
Dep.target.addDep(this);
}
}
}
// Watcher类
class Watcher {
addDep(dep: Dep) {
const id = dep.id;
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id);
this.newDeps.push(dep);
if (!this.depIds.has(id)) {
// 最终调用的还是dep.addSub(this)
dep.addSub(this);
}
}
}
}
派发更新
- 当数据发生变化时,会触发属性的 setter 方法, 调用dep.notify通知该属性所有订阅的 Watcher 对象,从而触发组件的重新渲染。
Compiler 类
• 进行编译模板,并解析内部指令与插值表达式。 • 进行页面的首次渲染 • 数据变化后,重新渲染视图