发布-订阅模式的实现与解析 —— Vue 2 和 Vue 3 的实现方式

发布-订阅模式(Publish-Subscribe Pattern),也叫做观察者模式(ObserverPattern),是软件设计中常见的设计模式之一。在前端开发中,发布-订阅模式主要用于解耦组件之间的通信,尤其是在响应式框架如 Vue 中,能够有效地实现数据的双向绑定与组件之间的同步。本文将探讨发布-订阅模式的原理,并通过 Vue 2 和 Vue 3 中的实现方式加以说明,分析它们如何运用此模式来构建高效的响应式系统。

1. 发布-订阅模式是什么

发布-订阅模式允许一个对象(称为“发布者”)向多个订阅者发布信息,而无需了解这些订阅者的具体实现。订阅者(Observers)通过订阅发布者的消息,在消息触发时响应并执行特定的动作。发布-订阅模式具有以下特点:

  • 解耦 :发布者和订阅者之间没有直接联系,它们通过中心化的消息代理(Event Bus、消息队列等)进行交互。
  • 动态性 :订阅者可以动态地订阅或取消订阅发布者的消息,且可以根据具体情况改变对信息的响应。
  • 异步性 :消息的发布和订阅通常是异步的,这使得应用能够高效地响应用户行为和异步事件。

在 Vue 中,数据的变化触发视图的更新就是采用发布-订阅模式实现的。

2. Vue 2 中的发布-订阅模式实现

Vue 2 的响应式系统通过 Object.defineProperty 实现数据的代理,订阅者和发布者之间通过Dep(依赖收集器)进行关联和管理。具体的实现步骤如下:

2.1 数据劫持与 Dep 类

Vue 2 中的响应式原理依赖于 Object.defineProperty 方法。它通过劫持对象的 getter 和 setter来实现对数据的监听。当访问或修改对象属性时,getter 和 setter 会被触发。每个属性都有一个与之对应的 Dep实例,该实例充当了发布-订阅模式中的“发布者”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Dep {
constructor() {
this.subscribers = [];
}

addSub(sub) {
this.subscribers.push(sub);
}

notify() {
this.subscribers.forEach((sub) => sub.update());
}
}

function defineReactive(obj, key, val) {
const dep = new Dep();

Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set(newVal) {
if (newVal !== val) {
val = newVal;
dep.notify();
}
},
});
}

在上面的代码中,Dep 类就是发布者,它维护一个订阅者数组 subscribers。每当数据发生变化时,notify方法会被调用,通知所有的订阅者更新视图。

2.2 订阅者与视图更新

在 Vue 2 中,订阅者通常是组件的视图更新函数。当组件依赖的数据发生变化时,Dep.target 会指向组件实例,从而将该组件加入到 Dep的订阅者队列中。每当数据被修改时,Dep 会通知所有的订阅者(即依赖该数据的视图),从而实现视图的更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
this.value = this.get();
}

get() {
Dep.target = this; // 当前 watcher 被注册到 Dep.target
const value = this.vm[this.expr]; // 触发 getter,依赖收集
Dep.target = null;
return value;
}

update() {
const newValue = this.vm[this.expr];
if (newValue !== this.value) {
this.cb(newValue);
}
}
}

在 Vue 2 中,Watcher 负责作为订阅者,当数据变动时调用 update 方法,从而实现视图更新。

3. Vue 3 中的发布-订阅模式实现

Vue 3 相较于 Vue 2 在响应式系统上做了显著的改进。Vue 3 使用了基于 Proxy 的全新响应式实现,摒弃了Object.defineProperty,引入了更轻量的 代理对象Effect系统,这使得数据和视图的同步更加高效且易于扩展。

3.1 Proxy 的使用

Vue 3 的响应式系统通过 Proxy 对象来代理原始数据对象的操作。通过代理的方式,可以实现对象的所有读写操作的拦截,从而在 getter 和setter 中进行依赖收集与通知机制的触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function reactive(target) {
const handler = {
get(target, key, receiver) {
if (Dep.target) {
track(target, key);
}
return Reflect.get(...arguments);
},
set(target, key, value, receiver) {
const result = Reflect.set(...arguments);
trigger(target, key);
return result;
},
};

return new Proxy(target, handler);
}

在上述代码中,Proxy 通过拦截对象的读取和修改操作,自动将操作过程中的依赖收集和通知逻辑融入其中。每当数据发生变化时,trigger会被调用,触发相应的副作用更新。

3.2 Effect 和依赖收集

在 Vue 3 中,Effect 是执行副作用函数(如视图更新)的核心机制。Effect在访问响应式数据时,会自动将当前执行的函数注册为依赖(订阅者),而数据的变化会触发 Effect 重新执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
let activeEffect;
function effect(fn) {
activeEffect = fn;
fn();
activeEffect = null;
}

function track(target, key) {
if (activeEffect) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
}
}

function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (deps) {
deps.forEach((effect) => effect());
}
}

通过 track 和 trigger 方法,Vue 3 实现了高效的依赖收集与更新机制。当响应式数据变化时,trigger会遍历所有依赖于该数据的 effect,从而触发相应的视图更新。

4. Vue 2 与 Vue 3 发布-订阅模式的对比

特性 Vue 2 Vue 3
响应式系统基础 基于 Object.defineProperty 数据劫持 基于 Proxy 对象代理
依赖收集 通过 Dep 类与 Watcher 实现依赖收集与更新 通过 track 和 trigger 实现依赖收集与副作用更新
性能与灵活性 性能较为低效,且只能劫持对象的属性,无法处理数组等复杂情况 性能更高,支持更多数据结构,且可以处理数组、对象等复杂情况
更新机制 视图更新通过 Watcher 类的 update 方法触发 通过 effect 和 Proxy 结合实现自动更新机制

5. 总结

发布-订阅模式是 Vue 的核心机制之一,它有效地将数据的变化与视图的更新解耦。在 Vue 2 中,通过 Object.defineProperty 和Dep 类的结合,实现了数据变动时视图的更新;而在 Vue 3 中,基于 Proxy的响应式系统使得数据的代理更加高效,依赖收集与更新也变得更加灵活和高性能。两者在实现上有所不同,但都遵循了发布-订阅模式的核心思想。