[发布订阅模式] - EventEmitter - redux的实现

1,536 阅读6分钟

观察者和发布订阅

很多人都知道,发布订阅和生产消费的区别,但是我想问:发布订阅和观察者,有什么区别

观察者模式:当对象间存在一对多关系时,则使用观察者模式(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 中所有 observerupdate方法;
  • (我猜,肯定有人看着像 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的直接引用,导致了SubjectObserver.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

这样有什么好处呢,我们拿 ReactuseEffect 函数来说。函数本身在卸载时,即会调用其 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);
};

在前端,发布订阅可能是我们最常见到的一种设计模式:包括 reduxqiankun(微前端框架) 的数据管理,都能看到发布订阅的身影,甚至是一个 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),我们再看一下;

分析如下:

  1. currentReducer 是什么,是外面传进来的一个函数;
  2. 我们看一下一个常用的 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 的处理,也是类似的逻辑,感兴趣的话,小伙伴可以看一看。