Vue双向绑定中的发布-订阅模式究竟是什么

发布-订阅模式的定义

发布订阅模式不管是在程序还是现实生活中都非常地常见,例如,过年或者节假日火车票买不到票时,购票软件会有一个候补下单的功能,用户可以向购票软件后台发出一个订阅请求:如果有人退票或某些原因增加座位后,软件后台会帮助用户下单并及时通知用户成功购买该车次车票。
这就是一个发布-订阅的例子,购票软件是一个发布消息的发布者,而用户则是订阅消息的订阅者。可以看到,这是一个一对多的依赖关系,当发布的对象状态发生改变时,所有依赖他的对象都将得到通知。[1]
这就是发布-订阅模式的定义。

Vue 中的双向绑定

了解了发布订阅模式的定义,再来看一下 vue 双向绑定的原理。
下面是 vue 官网双向绑定原理的图,他是通过Object.defineProperty或者new Proxy去监听对象数据变化,对象相当于一个**发布者(publisher),当监听到对象改变后对象的setter就会发送消息通知订阅者(watcher)**,订阅者收到消息后通知对 DOM 文本进行update操作。

image.png

image.png

image.png

上面图是用 Proxy 实现一个双向绑定的部分代码,从代码片段中可以看到observer作为一个发布者,它可以同时被多个变量订阅,当监听到数据发生改变时,调用set方法通知watcher(订阅者)去改变 node 节点和值。

image.png

同理,上图代码描述的是,当我们给input输入框作为一个发布者,watcher作为订阅者。当用户输入数据时,发布者(input)订阅者(watcher) 发送改变信息的通知,watcher收到通知后进行值得修改和 node 节点数据更改,这就是 vue 通过发布订阅模式实现的数据双向绑定。

JavaScript 中的发布订阅模式

除了 vue 中数据双向绑定使用的是发布订阅模式,相信我们在 JavaScript 也会经常使用到的事件也是一个发布订阅模式。例如addEventListener

1
2
3
4
5
6
7
8
9
10
11
document.body.addEventListener("click", () => {
console.log("click 111");
});

document.body.addEventListener("click", () => {
console.log("click 222");
});

document.body.addEventListener("click", () => {
console.log("click 333");
});

image.png
在给 body 订阅了点击事件后,点击 body 都会依次触发上面的三个函数。

又例如,在 node 中的EventEmitter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const event = require("events").EventEmitter;

event.on("data", () => {
console.log("111");
});
event.on("data", () => {
console.log("222");
});
event.on("data", () => {
console.log("333");
});

setTimeout(() => {
event.emit("data");
}, 1000);

image.png

可见,发布-订阅模式在 JavaScript 中非常有用而且使用广泛。

实现一个简单的发布-订阅模式通用函数

了解了发布订阅的模式后,最后我们来实现一个通用的发布订阅模式函数吧~

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
33
34
35
36
37
38
39
class CommonEvent {
constructor() {
this.events = {};
}

// 订阅函数
listen(name, eventFun) {
if (!this.events[name]) this.events[name] = [];
if (typeof eventFun !== "function") throw "eventFun must be a function!";
this.events[name].push(eventFun);
}

// 触发事件函数
emit(name, ...params) {
if (!this.events[name]) return;
this.events[name].forEach((val) => {
val(...params);
});
}

// 清除订阅事件
remove(name, eventFun) {
if (!this.events[name]) return;
this.events[name] = this.events[name].filter((val) => val !== eventFun);
console.log(this.events[name]);
}
}

const event = new CommonEvent();
function handleFun(msg) {
console.log(msg);
}
event.listen("data", handleFun);
event.listen("data", handleFun);
event.listen("data", handleFun);

event.emit("data", "111"); // 111 111 111

event.remove("data", handleFun); // []

发布-订阅模式的优缺点 [1]

使用发布订阅模式编写程序,他的优点有:

1. 实现时间上的解耦(组件,模块之间的异步通讯)
2. 对象之间的解耦,交由发布订阅的对象管理对象之间的耦合关系

当然发布订阅模式也存在缺点:

1. 创建的订阅者本身要消耗一定的时间和内存,当我们订阅一个消息后,如果不去销毁,这个订阅这会一直存在于内存之中。
2. 对象之间解耦的同时,他们的关系也会被深埋在代码背后,这会造成一定的维护成本

小结

本文主要介绍了设计模式中的发布订阅模式
阅读文本可以了解到:

  1. 发布订阅模式的定义
  2. vue 是如何使用发布订阅模式实现数据双向绑定的
  3. JavaScript 中有哪些实现是通过发布订阅模式编写的
  4. 实现一个简单的通用发布订阅函数
  5. 了解了发布订阅模式的优缺点

参考

  1. JavaScript 设计模式与开发实践
  2. 从发布-订阅模式到 Vue 响应系统