观察者和发布订阅
很多人都知道,发布订阅和生产消费的区别,但是我想问:发布订阅和观察者,有什么区别?
观察者模式:当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。观察者模式属于行为型模式。
我们先来看一个Java的观察者模式:
以前写C++时,观察者模式非常地常用,最早的时候,想着两个库之间需要传值,比如子组件被父组件引用,那么当子组件发生改变,想要通知父组件更新,该怎么办呢?总不能再把父组件的头文件再引进来吧,这时候就有前辈告诉我,应该用观察者模式。
Java 的观察者模式
// Subject.java
import java.util.ArrayList;
import java.util.List;
public class Subject {
private List<Observer> observers
= new ArrayList<Observer>();
private int state;
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
notifyAllObservers();
}
public void attach(Observer observer){
observers.add(observer);
}
public void notifyAllObservers(){
for (Observer observer : observers) {
observer.update();
}
}
}
// Observer.java
public abstract class Observer {
protected Subject subject;
public abstract void update();
}
// BinaryObserver.java
public class BinaryObserver extends Observer{
// subject 作为了 Binary 的一个成员变量,
// 同时将这个 Observer 注入到了 Subject
// 自己的 observers 成员变量中
public BinaryObserver(Subject subject){
this.subject = subject;
this.subject.attach(this);
}
@Override
public void update() {
System.out.println( "Binary String: "
+ Integer.toBinaryString( subject.getState() ) );
}
}
我们可以看到
- 43行:
BinaryObserver在自己的构造函数中,将subject对象作为了自己的成员变量,同时,将自己注入到了subject对象的成员变量observers中; - 15行:当
setState进行执行时会调用notify函数,紧接着会调用observers中所有observer的update方法;
- (我猜,肯定有人看着像 vue2.x的
Watcher实现)
基于此,我们的测试用例如下:
public class ObserverPatternDemo {
public static void main(String[] args) {
Subject subject = new Subject();
new BinaryObserver(subject);
System.out.println("First state change: 15");
subject.setState(15);
System.out.println("Second state change: 10");
subject.setState(10);
}
}
但是这种方式我们可以看到,核心的观察逻辑,在于 observers的直接引用,导致了Subject和 Observer.update方法的松耦合。
但是,不是完全无耦合。
狭义的来说,这是观察者模式,而不是发布订阅模式。
如果 Java 语言看起来有些困难,我们来看一下下面的 JS 的版本
JavaScript 的观察者模式
为了更加 standard一些,我们尝试使用 typeScript 写一个观察者:
class Subject {
deps: Array<Observer>;
state: Number
constructor() {
this.deps = [];
this.state = 0;
}
attach(obs: Observer) {
this.deps.push(obs);
}
setState(num: Number) {
this.state = num;
this.notifyAllObservers();
}
notifyAllObservers():void {
this.deps.forEach(obs => {
obs.run(this.state);
})
}
}
abstract class Observer {
subject: Subject;
constructor(subject: Subject) {
this.subject = subject;
this.subject.attach(this);
};
abstract run(data: String | Number | undefined): void;
}
class BinaryObserver extends Observer {
constructor(subject: Subject) {
super(subject);
}
run(data: String | Number | undefined): void {
console.log("hello, this is binaryObserver:" + data)
}
}
class ArrayObserver extends Observer {
constructor(subject: Subject) {
super(subject);
}
run(data: String | Number | undefined): void {
console.log("hello, this is ArrayObserver:" + data)
}
}
// main
const subject = new Subject();
const obs = new BinaryObserver(subject);
subject.setState(10);
subject.setState(15);
简单地使用命令:tsc ./observer.ts && node ./observer.js,即可以得到结果,这里,我们不再赘述。
JavaScript 的发布订阅模式
基于函数式编程的 JavaScript语言,把函数当成了一等公民,可以轻松地做到完全无耦合。
一个简单的发布订阅
// 立即执行的匿名函数形成闭包
const emitter = (function(){
var deps = {};
return {
on: function(label, func) {
deps[label] = deps[label] || [];
deps[label].push(func);
},
emit: function(label, ...rest) {
deps[label] instanceof Array &&
deps[label].forEach(fn => fn.apply(null, rest))
}
}
})();
一个简单的发布订阅就这样完成了。
我们写一个简单的测试用例
emitter.on("test", function(data) {
console.log(data);
})
setTimeout(() => {
emitter.emit("test", "123")
},1000)
// 1000ms 后打印 '123'
解释一下:
- 在
on函数执行时,我们将函数体push到了deps中; emit函数执行时,将对应的函数数组拿出来执行一遍;
就是这样的一种逻辑,就构成了发布订阅的雏形,当然,我们还需要再完善一下:
增加 off 函数
// 立即执行的匿名函数形成闭包
const emitter = (function(){
var deps = {};
return {
on: function(label, func) {
deps[label] = deps[label] || [];
deps[label].push(func);
return () => this.off(label, func)
},
emit: function(label, ...rest) {
deps[label] instanceof Array &&
deps[label].forEach(fn => fn.apply(null, rest))
},
off: function(label, func) {
deps[label] = deps[label] || [];
let num = deps[label].findIndex(fn => fn !== func);
if(num !== -1) deps[label].splice(num, 1);
},
}
})();
这样,我们就有了 off 函数,用来关闭某个订阅的信息,同时,我们可以在 on函数里 return一个 off;
这样有什么好处呢,我们拿 React 的 useEffect 函数来说。函数本身在卸载时,即会调用其 return 的函数,这样我们就可以做如下形式的改写
useEffect(() => {
events.on(label, func);
return events.off(label, func)
},[...]);
// 改写成 --->>
useEffect(() => events.on(label, func)},[...]);
增加 once 功能
如果要增加一个 once 功能,也就是说,我们在处理 on 函数中,push(func) 这一步的时候,需要能够标记哪些 event 需要在第一次 emit 之后被 off 掉,所以我们增加一个功能,作如下的修改;
// 立即执行的匿名函数形成闭包
const emitter = (function(){
var deps = {};
return {
on: function(label, func) {
deps[label] = deps[label] || [];
deps[label].push({once:false,func});
return () => this.off(label, func)
},
once:function(label, func) {
deps[label] = deps[label] || [];
deps[label].push({once:true,func});
return () => this.off(label, func)
},
emit: function(label, ...rest) {
deps[label] instanceof Array &&
// 这里也做相应的修改
deps[label].forEach(item => item.func.apply(null, rest))
deps[label] = deps[label].filter(item => !item.once);
},
off: function(label, func) {
deps[label] = deps[label] || [];
// 这里也做相应的修改
let num = deps[label].findIndex(item => item.fn !== func);
if(num !== -1) deps[label].splice(num, 1);
},
}
})();
基于此,我们来看一个业界常用的库EventEmitter,在这里,我们来看一下核心的实现
整个库里有很多函数,我们对标到上面描述的功能,进行一定程度的精简:
on / once 函数
function alias(name) {
return function aliasClosure() {
return this[name].apply(this, arguments);
};
}
// 实现代码:
proto.on = alias('addListener');
proto.once = alias('addOnceListener');
proto.addOnceListener = function addOnceListener(evt, listener) {
return this.addListener(evt, {
listener: listener,
once: true
});
};
proto.addListener = function addListener(evt, listener) {
var listeners = this.getListenersAsObject(evt);
var listenerIsWrapped = typeof listener === 'object';
var key;
for (key in listeners) {
if (listeners.hasOwnProperty(key) && indexOfListener(listeners[key], listener) === -1) {
listeners[key].push(listenerIsWrapped ? listener : {
listener: listener,
once: false
});
}
}
return this;
};
这里可以看到,在第22-25行中,根据对类型的判断,使用了{once:true|false} 进行了函数的存储;
off 函数
proto.off = alias('removeListener');
proto.removeListener = function removeListener(evt, listener) {
var listeners = this.getListenersAsObject(evt);
var index;
var key;
for (key in listeners) {
if (listeners.hasOwnProperty(key)) {
index = indexOfListener(listeners[key], listener);
if (index !== -1) {
listeners[key].splice(index, 1);
}
}
}
return this;
};
emit 函数
proto.emitEvent = function emitEvent(evt, args) {
var listenersMap = this.getListenersAsObject(evt);
var listeners;
var listener;
var i;
var key;
var response;
for (key in listenersMap) {
if (listenersMap.hasOwnProperty(key)) {
listeners = listenersMap[key].slice(0);
for (i = 0; i < listeners.length; i++) {
// If the listener returns true then it shall be removed from the event
// The function is executed either with a basic call or an apply if there is an args array
listener = listeners[i];
if (listener.once === true) {
this.removeListener(evt, listener.listener);
}
response = listener.listener.apply(this, args || []);
if (response === this._getOnceReturnValue()) {
this.removeListener(evt, listener.listener);
}
}
}
}
return this;
};
proto.emit = function emit(evt) {
var args = Array.prototype.slice.call(arguments, 1);
return this.emitEvent(evt, args);
};
在前端,发布订阅可能是我们最常见到的一种设计模式:包括 redux 、qiankun(微前端框架) 的数据管理,都能看到发布订阅的身影,甚至是一个 Promise的实现:
- 我们在
then函数中,将函数体 push 到了promise的 两个数组中; - 当我们在
resolve或者reject函数执行时,会将数据value/reason放到这个函数体中进行调用。
针对 Promise 的实现,我们这里不多聊,可以来一起看一下 redux 的实现部分。
redux 的实现原理
store的构建
这里我把无关的逻辑代码删减一下:
export default function createStore(reducer, preloadedState, enhancer) {
let currentReducer = reducer
let currentState = preloadedState
let currentListeners: (() => void)[] | null = []
let nextListeners = currentListeners
let isDispatching = false
function getState() {
return currentState
}
// 订阅函数,类似上文中的 on 函数
function subscribe(listener) {
let isSubscribed = true;
nextListeners.push(listener);
// 返回取消订阅,类似上文中的 off 函数的返回。
return function unsubscribe() {
isSubscribed = false
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}
function dispatch(action) {
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 对相应的方法进行执行。类似上文中的 emit 函数。
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action;
}
const store = {
dispatch,
subscribe,
getState
}
return store;
}
redux 的核心代码解读如下::
- 初始化时,第3行的
currentState即为我们的发布订阅所驱动的数据; - 第8行的
getState方法,让我们可以随时获取这个数据;
- 而第13-15行,
subscribe函数,即为我们注册了一个监听器; - 第32行,除了基本的订阅信息的发布,有一个
currentReducer(currentState, action),我们再看一下;
分析如下:
currentReducer是什么,是外面传进来的一个函数;- 我们看一下一个常用的
reducer的样子;
function reducer(state = initState, action) {
switch (action.type) {
case 'INCREMENT':
return { number: state.number + 1 }
case 'DECREMENT':
return { number: state.number - 1 }
default:
return state
}
}
export default reducer
这里就非常明确了,当我们 dispatch一个 action的时候,currentState通过相应的 case 条件的处理,就得到一个新的 currentState,这样在 getState的时候,数据就更新了。
除此之外,我之前看过阿里开源的微前端框架 qiankun 的源码,其中的 globalState 的处理,也是类似的逻辑,感兴趣的话,小伙伴可以看一看。