深入设计模式第二节——发布订阅模式

69 阅读2分钟

定义

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。

订阅者把自己想订阅的事件注册到调度中心,当发布者发布该事件到调度中心,也就是该事件触发时,由调度中心统一调度订阅者注册到调度中心的处理代码。

优缺点

优点

  • 对象之间解耦

缺点

  • 创建订阅者本身要消耗一定的时间和内存
  • 多个发布者和订阅者嵌套在一起的时候,程序难以跟踪维护

用途

  • node.js EventEmitter 中的 on 和 emit 方法
  • Vue 中的 onon 和 emit 方法
  • 微信公众号、b 站关注后,当有文章发布后,关注的人可以接收到消息推送
  • 等等

经典的发布订阅模式实现

const eventEmitter = {
  list: {},
  on: function (event, fn) {
    if (!this.list[event]) {
      this.list[event] = [];
    }
    this.list[event].push(fn);
    return this;
  },
  emit: function (...args) {
    const _this = this;
    const event = args[0],
      fns = _this.list[event];
    if (!fns || fns.length === 0) {
      return false;
    }
    fns.forEach((fn) => {
      fn.apply(_this, args.slice(1, args.length));
    });
    return _this;
  },
};

function fans1(content) {
  console.log('粉丝1收到了:', content);
}
function fans2(content) {
  console.log('粉丝2收到了:', content);
}
// 订阅
eventEmitter.on('up', fans1);
eventEmitter.on('up', fans2);
// 发布
eventEmitter.emit('up', '发布订阅模式');

增强版

补充了 once 和 off 方法。

const eventEmitter = {
  list: {},
  on(event, fn) {
    if (!this.list[event]) {
      this.list[event] = [];
    }
    this.list[event].push(fn);
    return this;
  },
  emit(...args) {
    const _this = this;
    const event = args[0],
      fns = _this.list[event];
    if (!fns || fns.length === 0) {
      return false;
    }
    fns.forEach((fn) => {
      fn.apply(_this, args.slice(1, args.length));
    });
    return _this;
  },
  // 取消订阅
  off(event, fn) {
    const fns = this.list[event];
    if (!fns) return false;
    if (!fn) {
      fns.length = 0;
    } else {
      let cb;
      for (let i = 0, len = fns.length; i < len; i++) {
        cb = fns[i];
        if (cb === fn || cb.fn === fn) {
          fns.splice(i, 1);
          break;
        }
      }
    }
    return this;
  },
  // 监听一次
  once(event, fn) {
    const _this = this;
    function on(...args) {
      _this.off(event, on);
      fn.apply(_this, args);
    }
    on.fn = fn;
    _this.on(event, on);
    return _this;
  },
};

function fans1(content) {
  console.log('粉丝1收到了:', content);
}
function fans2(content) {
  console.log('粉丝2收到了:', content);
}
// 订阅
eventEmitter.on('up', fans1);
eventEmitter.off('up', fans1);
eventEmitter.once('up', fans2);
// 发布
eventEmitter.emit('up', '发布订阅模式');
eventEmitter.emit('up', '发布订阅模式');

Vue 中的实现

在 eventsMixin 中挂载到 Vue 构造函数的 prototype 中

vm.$on

将回调 fn 注册到事件列表中即可,_events 在实例初始化时创建。

Vue.prototype.$on = function (event, fn) {
  const vm = this;
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      this.$on(event[i], fn);
    }
  } else {
    (vm._events[event] || (vm._events[event] = [])).push(fn);
  }
  return vm;
};

vm.$off

支持offoff('eventName')off('eventName', fn)off(['eventName1', 'eventName2'])off(['eventName1', 'eventName2'], fn)多种情况

Vue.prototype.$off = function (event, fn) {
  const vm = this;
  if (!arguments.length) {
    vm._events = Object.create(null);
    return vm;
  }

  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      this.$off(event[i], fn);
    }
    return vm;
  }

  const cbs = vm._events[event];
  if (!cbs) {
    return vm;
  }
  if (!fn) {
    vm._events[event] = null;
    return vm;
  }

  if (fn) {
    const cbs = vm._events[event];
    let cb;
    let i = cbs.length;
    while (i--) {
      cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break;
      }
    }
  }

  return vm;
};

vm.$once

先移除事件监听,再执行函数。

Vue.prototype.$once = function (event, fn) {
  const vm = this;
  function on() {
    vm.$off(event, on);
    fn.apply(vm, arguments);
  }
  on.fn = fn;
  vm.$on(event, on);
  return vm;
};

vm.$emit

取出对应 event 回调函数列表,再遍历执行

Vue.prototype.$emit = function (event) {
  const vm = this;
  let cbs = vm._events[event];
  if (cbs) {
    const args = Array.from(arguments).slice(1);
    for (let i = 0, l = cbs.length; i < l; i++) {
      try {
        cbs[i].apply(vm, args);
      } catch (e) {
        console.error(e, vm, `event handler for "${event}"`);
      }
    }
  }
  return vm;
};