设计模式:观察者模式与发布/订阅模式区别

1,232 阅读3分钟

一、观察者模式

概要:需要理解目标与观察者的关系

  • 目标与观察者之间的关系类似老师与学生的关系。老师办了一个补习班,学生必须先到老师那听课,老师开始教学。
  • 学生们听懂了老师的课,更新自己的知识。 老师和学生们有直接的联系,他们之间面对面的关系。

555379-20160313183429007-1351424959.png

总结:

  • 观察者模式,多用于单个应用内部,没有第三方来实现调度。 定义了对象之间的一对多关系

实现思路

  • 切入点 Subject(主题,老师讲课), Observer(观察者,学生听课);

代码

// util.js
   export const Subject = function(eventName = 'default') {
       this.observerList = [];
       this.eventName = eventName;
       this._t;
   }
   
   // 将观察者注册到目标实例中,可以理解为 observer(学生听课) -> Subject(老师讲课);
   Subject.prototype.add = function(observer) {
       // observerList很多学生
       if(Array.isArray(observer)) {
           this.observerList.push(...observer);
       } else {
           this.observerList.push(observer);
       }      
   }
   
   // 将指定观察者移除,可以理解为 observer(张三同学不来听课了);
   Subject.prototype.remove = function(observer) {
       const index = this.observerList.findIndex((item)=>item === observer);
       if(index !== -1) { 
           this.observerList.splice(index,1);
           return true;
       } else {
           return false;
       }
   }
   
   // 将内容通知到所有学生(observerList)
   Subject.prototype.notify = function(data){
       // 防抖处理
       clearTimeout(this._t);
       this._t = setTimeout(() => {
           const observerList = this.observerList;
           for(let i=0,len=observerList.length; i < len; i++) {
                //update是由observer暴露出来的接口,如果没有提供的话是无法通知的
                observerList[i].update({eventName: this.eventName, data})
           }
       }, 500); 
       //这里设置为500是为了测试效果,通常设置为50

   }
   export const Observer = function(fn) {
       const _fn = fn;
       this.update = function(data) {
           _fn(data)
       }
   }
   

实际用法

//实际用法
import * as React from 'React';
import { Subject, Observer } from './util';

const subject = new Subject();

export default class ClassRoom extends React.Component {
    constructor(props) {
        super(props);
        this.student = new Observer(this.callback);
        this.student2 = new Observer(this.callback2);
        subject.add([this.student,this.student2]);
        console.log(subject);
     }
     
     componentWillUnmount() {
        console.log(subject.remove(this.student));
        console.log(subject.remove(this.student2));
        console.log(subject);
     }
     
     callback = ({eventName, data}) => {
         console.log('student',data);
     };
     
     callback2 = ({eventName, data}) => {
         console.log('student',data);
     }
     
     teacher = () => {
         subject.notify('内容...');
     };
     
     render() {
         return <button onClick={this.teacher}>notify</button>
     }
}

二、发布订阅模式

概要:需要理解调度中心,订阅者,发布者之间的关系。

  • 罗翔老师在网上开了一个专栏,老师把课上传到平台上,喜欢这门课的同学订阅该专栏。后续只需要老师把视频传到平台上,平台会将更新的视频推送给订阅者。
  • 老师与学生的联系不是那么大了,两者之间通过平台来联系。

555379-20160313183439366-1623019133.png

总结

  • 发布订阅模式是观察者模式的一种变种,发布者和订阅者相互之间不知道彼此的存在,他们通过调度中心联系到彼此。事件名称一直是他们能联系彼此的条件。
  • 多应用于将多层透传通信方式扁平化。

实现思路

  • 主要是实现register,watcher,trigger,unregister这是个方法。整个流程的顺序也是按照这个来的。
  • watcher中会包含register和unregister。

代码(常规版)

// util.js
export const pubsubFactory = function (myPubsub) {
  let eventCenter = {};
  let eventTimeout = {};
  
  myPubsub.getEventCenter = function () {
    return eventCenter;
  };
  
  myPubsub.watcher = function (eventName, fn) {
    // 如果没有register需要先注册一下事件名,并给该事件列队增加fn
    if (!eventCenter[eventName]) {
      eventCenter[eventName] = [fn];
    } else {
      eventCenter[eventName].push(fn);
    }
    // unregister
    return function () {
      let events = eventCenter[eventName];
      eventCenter[eventName] = events.filter((handler) => handler !== fn);
    };
  };

  myPubsub.trigger = function (eventName, data) {
    const events = eventCenter[eventName];
    if (!events) {
      console.log(eventName + '事件未注册');
      return;
    }
    let _t = eventTimeout[eventName];
    clearTimeout(_t);
    eventTimeout[eventName] = setTimeout(() => {
      for (let i = 0, len = events.length; i < len; i++) {
        events[i]({ eventName, data });
      }
    }, 50);
  };
  return myPubsub;
};

export const pubsub = pubsubFactory({});

实际用法

// 实际用法
import * as React from 'React';
import { pubsub } from './util';

export default class PubSubDemo extends React.Component {
  constructor(props) {
    super(props);
    this.subject = pubsub.watcher('event', this.callback);
    this.subject2 = pubsub.watcher('event2', this.callback2);
  }

  componentWillUnmount() {
    this.subject();
    this.subject2();
    console.log(pubsub.getEventCenter())
  }

  callback = ({ eventName, data }) => {
    console.log(eventName, data);
  };

  callback2 = ({ eventName, data }) => {
    console.log(eventName, data);
  };

  publish = () => {
    pubsub.trigger('event', 'data1');
  };

  publish2 = () => {
    pubsub.trigger('event2','data2');
  };

  render() {
    return (
      <div>
        <button onClick={this.publish}>publish</button>
        <button onClick={this.publish2}>publish2</button>
      </div>
    );
  }
}

代码(装饰器版)

// util.js
// 该版本不支持hook组件
import * as _ from "lodash";
function decorator() {
  const events = [];
  let cache = undefined;
  let _t = null;

  const register = callback => {
    if (!events.includes(callback)) {
      events.push(callback);
    }
  };

  const unregister = callback => {
    const index = events.indexOf(callback);
    events.splice(index, 1);
    return index > -1;
  };

  const trigger = value => {
      clearTimeout(_t);
      cache = value;
      _t = setTimeout(() => {
        events.forEach(callback => {
          _.isFunction(callback) && callback(cache);
        });
      }, 50);
  };

  const watcher = function(inst, funcName) {
    const name = `__${funcName}__`;
    const { componentDidMount, componentWillUnmount } = inst;
    // 这里存在多级递归调用的问题,不建议一个组件绑定太多的pubsub事件。
    inst.componentDidMount = function() {
      this[name] = this[funcName].bind(this);
      register(this[name]);
      if (_.isFunction(componentDidMount)) {
        componentDidMount.call(this);
      }
    };

    inst.componentWillUnmount = function() {
      unregister(this[name]);
      delete this[name];
      if (_.isFunction(componentWillUnmount)) {
        componentWillUnmount.call(this);
      }
    };
  };

  return {
    watcher,
    trigger,
    register,
    unregister
  };
}

const DF = {};
export default function decoratorFactory(name = "default") {
  if (!(name in DF)) {
    DF[name] = decorator();
  }
  return DF[name];
}

实际用法

// 实际用法
import * as React from 'React';
import decoratorFactory from './util';

const state = decoratorFactory('event');
const state2 = decoratorFactory('event2');

export default class DecoratorPubsubDemo extends React.Component {

  componentWillUnmount() {
    console.log(state);
    console.log(state2);
  }
  @state.watcher
  callback = (data) => {
    console.log(data);
  };
  @state2.watcher
  callback2 = (data) => {
    console.log(data);
  };

  publish = () => {
    state.trigger('data1');
  };

  publish2 = () => {
    state2.trigger('data2');
  };

  render() {
    return (
      <div>
        <button onClick={this.publish}>publish</button>
        <button onClick={this.publish2}>publish2</button>
      </div>
    );
  }
}

三、参考文章

四、结语

以上观点只是个人结合实践产出的理解,欢迎各位过路的大佬补充斧正~