JavaScript设计模式之发布-订阅模式

99 阅读3分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。

什么是发布-订阅模式

发布-订阅模式又叫观察者模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知。这种模式就像我们在掘金上看到了一篇文章,觉得这篇文章写的很好,对我们有帮助,于是我们还想看以后这个作者的文章,但是我们也不知道作者会在什么时候再次发布,于是我们通常会去关注这个作者,等到这个作者发布了文章后,会有一条消息来通知我们,这时我们就知道已经有新文章可以看了。

发布-订阅模式可以做什么

了解了什么是发布-订阅模式后,我们看看发布-订阅模式能够做什么。在JavaScript设计模式与开发实践中写道,发布-订阅模式可以广泛应用于异步编程,是一种替代传统回调函数的方案。比如,我们可以订阅ajax请求的error,succ等事件,这样我们就无需关注对象在异步运行期间的内部状态,而只需订阅感兴趣的事件发生点。Vue.js的事件系统也是采用的发布-订阅模式实现的,后面我们看看Vue中on,on,off,once,once,emit的具体实现。

mitt tiny-mitter

下面我们看发布-订阅模式的具体实现,直接上代码

mitt

function mitt(all) {
    all = all || new Map()
    return {
        all: all,
        on: function (type, handler) {
            var handlers = all.get(type)
            if (handlers) {
                handlers.push(handler)
            } else {
                all.set(type, [handler])
            }
        },
        off: function (type, handler) {
            var handlers = all.get(type)
            if (handlers) {
                if (handler) {
                    // indexOf没找到会返回-1,而对splice来说-1会从数组末尾开始计算
                    // 所以这里用到了右移运算符 -1>>>0 4294967295,这个数对splice并没有影响
                    handlers.splice(handlers.indexOf(handler) >>> 0, 1)
                } else {
                    all.set(type, [])
                }
            }
        },
        emit: function (type, evt) {
            var handlers = all.get(type)
            if (handlers) {
                handlers.slice().map(function (handler) {
                    handler(evt)
                })
            }
            handlers = all.get('*')
            if (handlers) {
                handlers.slice().map(function (handler) {
                    handler(type, evt)
                })
            }
        }
    }
}

tiny-emitter

function E () {
  // 为空更容易继承
}

E.prototype = {
  on: function (name, callback, ctx) {
    var e = this.e || (this.e = {});

    (e[name] || (e[name] = [])).push({
      fn: callback,
      ctx: ctx
    });
    // 链式调用
    return this;
  },

  once: function (name, callback, ctx) {
    var self = this;
    function listener () {
      self.off(name, listener);
      callback.apply(ctx, arguments);
    };

    listener._ = callback
    return this.on(name, listener, ctx);
  },

  emit: function (name) {
    var data = [].slice.call(arguments, 1);
    var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
    var i = 0;
    var len = evtArr.length;

    for (i; i < len; i++) {
      evtArr[i].fn.apply(evtArr[i].ctx, data);
    }

    return this;
  },

  off: function (name, callback) {
    var e = this.e || (this.e = {});
    var evts = e[name];
    var liveEvents = [];

    if (evts && callback) {
      for (var i = 0, len = evts.length; i < len; i++) {
        if (evts[i].fn !== callback && evts[i].fn._ !== callback)
          liveEvents.push(evts[i]);
      }
    }

    (liveEvents.length)
      ? e[name] = liveEvents
      : delete e[name];

    return this;
  }
};

vue中的事件系统

Vue.prototype.$on = function (event, fn) {
    const vm = this
    // event为数组时,递归调用
    if (Array.isArray(event)) {
        for (let i = 0; i < event.length; i++) {
            this.$on(event[i], fn)
        }
    } else {
    // 添加回调
        (vm._events[event] || (vm._events[event] = [])).push(fn)
    }
    return vm
}

Vue.prototype.$off = function (event, fn) {
    const vm = this
    // 移除所有事件的监听器
    if (!arguments.length) {
        vm._events = Object.create(null)
        return vm
    }
    // event为数组,递归调用$off
    if (Array.isArray(event)) {
        for (let i = 0; i < event.length; i++) {
            this.$off(event[i], fn)
        }
        return vm
    }
    const cbs = vm._events[event]
    if (!cbs) {
        return vm
    }
    // 只提供事件名,移除该事件的所有监听器
    if (arguments.length === 1) {
        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
}

// 监听一个自定义事件,只触发一次,第一次触发后移除监听器
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
}

Vue.prototype.$emit = function (event) {
    const vm = this
    let cbs = vm._events[event]
    if (cbs) {
        const args = toArray(arguments, 1)
        for (let i = 0; i < cbs.length; i++) {
            try {
                cbs[i].apply(vm, args)
            } catch (error) {
                handleError()
            }
        }
    }
    return vm
}

总结

  1. 什么是发布-订阅模式
  2. 发布-订阅模式能做什么
  3. 如何实现一个发布-订阅模式
  4. 无符号右移操作符的使用
  5. 删除数组中某一项的不同操作之间的区别