发布订阅、观察者设计模式

观察者模式

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新

观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯

实现代码如下:

被观察者模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Subject {

constructor() {
this.observerList = [];
}
addObserver(observer) {
this.observerList.push(observer);
}
removeObserver(observer) {
const index = this.observerList.findIndex(o => o.name === observer.name);
this.observerList.splice(index, 1);
}
notifyObservers(message) {
const observers = this.observeList;
observers.forEach(observer => observer.notified(message));
}

}

观察者:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Observer {

constructor(name, subject) {
this.name = name;
if (subject) {
subject.addObserver(this);
}
}

notified(message) {
console.log(this.name, 'got message', message);
}
}

使用代码如下:

1
2
3
4
5
6
7
const subject = new Subject();
const observerA = new Observer('observerA', subject);
const observerB = new Observer('observerB');
subject.addObserver(observerB);
subject.notifyObservers('Hello from subject');
subject.removeObserver(observerA);
subject.notifyObservers('Hello again');

上述代码中,观察者主动申请加入被观察者的列表,被观察者主动将观察者加入列表

发布订阅模式

发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在

同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在

实现代码如下:

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
class PubSub {
constructor() {
this.messages = {};
this.listeners = {};
}
// 添加发布者
publish(type, content) {
const existContent = this.messages[type];
if (!existContent) {
this.messages[type] = [];
}
this.messages[type].push(content);
}
// 添加订阅者
subscribe(type, cb) {
const existListener = this.listeners[type];
if (!existListener) {
this.listeners[type] = [];
}
this.listeners[type].push(cb);
}
// 通知
notify(type) {
const messages = this.messages[type];
const subscribers = this.listeners[type] || [];
subscribers.forEach((cb, index) => cb(messages[index]));
}
}

发布者代码如下:

1
2
3
4
5
6
7
8
9
class Publisher {
constructor(name, context) {
this.name = name;
this.context = context;
}
publish(type, content) {
this.context.publish(type, content);
}
}

订阅者代码如下:

1
2
3
4
5
6
7
8
9
class Subscriber {
constructor(name, context) {
this.name = name;
this.context = context;
}
subscribe(type, cb) {
this.context.subscribe(type, cb);
}
}

使用代码如下:

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
const TYPE_A = 'music';
const TYPE_B = 'movie';
const TYPE_C = 'novel';

const pubsub = new PubSub();

const publisherA = new Publisher('publisherA', pubsub);
publisherA.publish(TYPE_A, 'we are young');
publisherA.publish(TYPE_B, 'the silicon valley');
const publisherB = new Publisher('publisherB', pubsub);
publisherB.publish(TYPE_A, 'stronger');
const publisherC = new Publisher('publisherC', pubsub);
publisherC.publish(TYPE_C, 'a brief history of time');

const subscriberA = new Subscriber('subscriberA', pubsub);
subscriberA.subscribe(TYPE_A, res => {
console.log('subscriberA received', res)
});
const subscriberB = new Subscriber('subscriberB', pubsub);
subscriberB.subscribe(TYPE_C, res => {
console.log('subscriberB received', res)
});
const subscriberC = new Subscriber('subscriberC', pubsub);
subscriberC.subscribe(TYPE_B, res => {
console.log('subscriberC received', res)
});

pubsub.notify(TYPE_A);
pubsub.notify(TYPE_B);
pubsub.notify(TYPE_C);

上述代码,发布者和订阅者需要通过发布订阅中心进行关联,发布者的发布动作和订阅者的订阅动作相互独立,无需关注对方,消息派发由发布订阅中心负责

区别

  • 在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。
  • 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。
  • 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列)

vue2的响应式原理

双向绑定的构成

双向绑定由三个重要部分构成

  • 数据层(Model):应用的数据及业务逻辑
  • 视图层(View):应用的展示效果,各类UI组件
  • 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来

理解ViewModel

它的主要职责就是:

  • 数据变化后更新视图
  • 视图变化后更新数据

当然,它还有两个主要部分组成

  • 监听器(Observer):对所有数据的属性进行监听
  • 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数

双向绑定流程

1. 初始化阶段

  • 响应式属性的创建:通过 observe 函数和 Observer 类,Vue 将组件的 data 对象转换成响应式对象。对每个属性,通过 Object.defineProperty 设置其 getter 和 setter,以便进行依赖收集和派发更新。
  • Dep** 实例的创建**:在响应式属性的 getter 和 setter 中闭包的 Dep 实例负责收集当前属性的所有依赖(即观察者,也就是 Watcher 实例)。

2. 组件渲染和依赖收集

  • Watcher** 实例的创建**:当组件渲染(或计算属性被访问,或侦听器被初始化)时,Vue 创建一个 Watcher 实例。这个实例负责评估表达式或渲染函数,并收集依赖(即被访问的响应式属性)。
  • 依赖收集过程:在 Watcher 的构造过程中,Dep.target 被临时设置为当前的 Watcher。然后,当访问任何响应式属性的 getter 时,这个属性的 Dep 实例会调用 addDep 方法将当前的 Dep.target(也就是这个 Watcher)添加到它的依赖列表中。这样,每个被访问的响应式属性都会收集到当前的 Watcher 作为它的依赖。

3. 更新过程

  • 当响应式属性的值发生变化(触发 setter)时,属性对应的 Dep 实例会通知它的所有依赖(即收集到的所有 Watcher 实例),调用它们的 update 方法来响应数据变化。
  • Watcherupdate 方法最终会导致组件的重新渲染,或者计算属性/侦听器的重新评估。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
class Vue {  
constructor(options) {
this.$options = options;
this.$data = options.data;

// 对data选项做响应式处理
observe(this.$data);

// 代理data到vm上
proxy(this);

// 执行编译
new Compile(options.el, this);
}
}
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
return;
}
new Observer(obj);
}

class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 获取dom
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍历子元素
if (this.isElement(node)) { // 判断是否为节点
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素
this.compile(node); // 对子元素进行递归遍历
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
// 负责更新视图
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this
// 读一下key,触发get
vm[key]
// 置空
Dep.target = null
}

// 未来执行dom更新函数,由dep调用的
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
class Dep {
constructor() {
this.deps = []; // 依赖管理
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}

function defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例
return val;
},
set(newVal) {
if (newVal === val) return;
dep.notify(); // 通知dep执行更新方法
},
});
}

依赖收集实现思路:

  1. defineReactive时为每⼀个key创建⼀个Dep实例
  2. 初始化视图时读取某个key,例如name1,创建⼀个watcher1
  3. 由于触发name1getter方法,便将watcher1添加到name1对应的Dep中
  4. name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新

从上面的实现可以看出来,vue2的基于Object.defineProperty实现的get和set方法存在一定的缺陷:

  • 检测不到对象属性的添加和删除
  • 数组API方法无法监听到
  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题

vue3的响应式原理

Proxy的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了

ES6中,我们详细讲解过Proxy的使用,就不再述说了

下面通过代码进行展示:

定义一个响应式方法reactive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`设置${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`删除${key}:${res}`)
return res
}
})
return observed
}

测试一下简单数据的操作,发现都能劫持

1
2
3
4
5
6
7
8
9
10
11
12
const state = reactive({
foo: 'foo'
})
// 1.获取
state.foo // ok
// 2.设置已存在属性
state.foo = 'fooooooo' // ok
// 3.设置不存在属性
state.dong = 'dong' // ok
// 4.删除属性
delete state.dong // ok

再测试嵌套对象情况,这时候发现就不那么 OK

1
2
3
4
5
6
const state = reactive({
bar: { a: 1 }
})

// 设置嵌套对象属性
state.bar.a = 10 // no ok

如果要解决,需要在get之上再进行一层代理

1
2
3
4
5
6
7
8
9
10
11
12
13
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相当于在对象外层加拦截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
return isObject(res) ? reactive(res) : res
},
return observed
}
  1. 为什么用 Reflect.get() 和 Reflect.set(),而不是直接用 target[key]?

根据 MDN 介绍 set() 要返回一个布尔值,比如 Reflect.set() 会返回一个是否修改成功的布尔值,直接赋值 target[key] = newValue,而不返回 true 就会报错。而且不管 Proxy 怎么修改默认行为,都可以通过 Reflect 获取默认行为。get() 同理。

vue3 的响应式核心原理即是通过 Proxy 代理目标对象的存取器,拦截存取操作,在执行收集依赖 track 以及触发更新 trigger 的方法后再完成原先的存取操作。

vue3.0 建立响应式的方法有两种: 第一个就是运用composition-api中的reactive直接构建响应式,composition-api的出现我们可以在.vue文件中,直接用setup()函数来处理之前的大部分逻辑,也就是说我们没有必要在 export default{ } 中在声明生命周期 , data(){} 函数,watch{} , computed{} 等 ,取而代之的是我们在setup函数中,用vue3.0 reactive watch 生命周期api来到达同样的效果,这样就像react-hooks一样提升代码的复用率,逻辑性更强。

第二个就是用传统的 data(){ return{} } 形式 ,vue3.0没有放弃对vue2.0写法的支持,而是对vue2.0的写法是完全兼容的,提供了applyOptions 来处理options形式的vue组件。但是options里面的data , watch , computed等处理逻辑,还是用了composition-api中的API对应处理。

composition-api reactive

Reactive 相当于当前的 Vue.observable () API,经过reactive处理后的函数能变成响应式的数据,类似于option api里面的vue处理data函数的返回值。

我们用一个todoList的demo试着尝尝鲜。

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
40
41
42
43
44
45
46
47
48
49
50
51

const { reactive , onMounted } = Vue
setup(){
const state = reactive({
count:0,
todoList:[]
})
/* 生命周期mounted */
onMounted(() => {
console.log('mounted')
})
/* 增加count数量 */
function add(){
state.count++
}
/* 减少count数量 */
function del(){
state.count--
}
/* 添加代办事项 */
function addTodo(id,title,content){
state.todoList.push({
id,
title,
content,
done:false
})
}
/* 完成代办事项 */
function complete(id){
for(let i = 0; i< state.todoList.length; i++){
const currentTodo = state.todoList[i]
if(id === currentTodo.id){
state.todoList[i] = {
...currentTodo,
done:true
}
break
}
}
}
return {
state,
add,
del,
addTodo,
complete
}
}


options data

options形式的和vue2.0并没有什么区别

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
40
41
export default {
data(){
return{
count:0,
todoList:[]
}
},
mounted(){
console.log('mounted')
}
methods:{
add(){
this.count++
},
del(){
this.count--
},
addTodo(id,title,content){
this.todoList.push({
id,
title,
content,
done:false
})
},
complete(id){
for(let i = 0; i< this.todoList.length; i++){
const currentTodo = this.todoList[i]
if(id === currentTodo.id){
this.todoList[i] = {
...currentTodo,
done:true
}
break
}
}
}
}
}


不同类型的Reactive

vue3.0可以根据业务需求引进不同的API方法。这里需要

① reactive

建立响应式reactive,返回proxy对象,这个reactive可以深层次递归,也就是如果发现展开的属性值是引用类型的而且被引用,还会用reactive递归处理。而且属性是可以被修改的。

② shallowReactive

建立响应式shallowReactive,返回proxy对象。和reactive的区别是只建立一层的响应式,也就是说如果发现展开属性是引用类型也不会递归

③ readonly

返回的proxy处理的对象,可以展开递归处理,但是属性是只读的,不能修改。可以做props传递给子组件使用。

④ shallowReadonly

返回经过处理的proxy对象,但是建立响应式属性是只读的,不展开引用也不递归转换,可以这用于为有状态组件创建props代理对象。

储存对象与proxy

上文中我们提及到,用Reactive处理过并返回的对象是一个proxy对象。

假设存在很多组件,或者在一个组件中被多次reactive,就会有很多对proxy对象和它代理的原对象。为了能把proxy对象和原对象建立关系,vue3.0采用了WeakMap去储存这些对象关系。WeakMaps 保持了对键名所引用的对象的弱引用,即垃圾回收机制不将该引用考虑在内。只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

1
2
3
4
5
const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>() /* 只读的 */
const readonlyToRaw = new WeakMap<any, any>() /* 只读的 */