前端搬运工——发布订阅模式

240 阅读4分钟

以下内容来源于《JavaScript设计模式与开发实践》中的第八章——发布订阅模式。只是修改了部分内容(比如var改为let/const、一些函数的位置、变量名等),方便大家看清逻辑、消化、吸收。

全局的发布订阅对象

const globalEvent = (function () {
  const listener = {};
  const listen = (key, fn) => {
    if (!listener[key]) {
      listener[key] = [];
    }
    listener[key].push(fn);
  }
  const remove = (key, fn) => {
    const fns = listener[key];
    if (!fns) {
      // 如果 key 对应的消息没有被人订阅,则直接返回
      return;
    }
    if (!fn) {
      fns = []
      return;
    }
    for (let i = fns.length - 1; i >= 0; i--) {
      // 反向遍历订阅的回调函数列表
      const _fn = fns[ i ];
      if (_fn === fn) {
        fns.splice(i, 1);
      }
    }
  }
  const trigger =  (key, val) => {
    const fns = listener[key];
    if (!fns || fns.length === 0) {
      return;
    }
    for (let i = 0, fn; (fn = fns[i++]); ) {
      fn(val);
    }
  }
  return {
    listen,
    remove,
    trigger,
  }
}());

globalEvent.listen('squareMeter88', (fn1 = function (price) {
    console.log('价格= ' + price);
  })
);
globalEvent.listen('squareMeter88', (fn2 = function (price) {
    console.log('价格= ' + price);
  })
);
globalEvent.remove('squareMeter88', fn1); // 删除订阅
globalEvent.trigger('squareMeter88', 2000000); // 输出:2000000

跟通用模式相比,就是多了个闭包,然后只暴露出三个方法给外层,clientList等于是闭包内是私有变量,外层访问不到。

全局对象,先订阅后发布,并且加了命名空间的处理

const eventGlobal = (function () {
  const event = (function() {
    const nameSpaceMap = {};
    const defaultName = '_default_';
    const _listen = (key, cache, fn) => {
      if (!cache[key]) {
        cache[key] = [];
      }
      cache[key].push(fn);
    }
    const _remove = (key, cache, fn) => {
      const fns = cache[key];
      if (!fns || !fns.length) {
        return;
      }
      if (!fn) {
        fns = [];
        return;
      }
      for (let i = fns.length - 1; i >= 0; i--) {
        const _fn = fns[i];
        if (_fn === fn) {
          fns.splice(i, 1);
        }
      }
    }
    const _trigger = (key, cache, val) => {
      const fns = cache[key] || [];
      each(fns, function() {
        this.call(null, val);
      })
    }
    const each = (fns, fn) => {
      fns.map(_fn => {
        fn.call(_fn);
      })
    }
    const _create = (namespace) => {
      const name = namespace || defaultName;
      const cache = {};
      let offlineStack = [];
      const ret = {
        create: _create,
        listen: (key, fn) => {
          _listen(key, cache, fn);
          if (!offlineStack) {
            return;
          }
          each(offlineStack, function() {
            this()
          });
          offlineStack = null;
        },
        remove: (key, fn) => _remove(key, cache, fn),
        trigger: (key, val) => {
          const fn = () => _trigger(key, cache, val);
          if (!offlineStack) {
            return fn();
          }
          offlineStack.push(fn);
        }
      }
      return nameSpaceMap[name] || (nameSpaceMap[name] = ret);
    }
    return {
      create: _create,
      listen: function (key, fn) {
        const event = _create();
        event.listen(key, fn);
      },
      remove: function (key, fn) {
        const event = _create();
        event.remove(key, fn);
      },
      trigger: function (key, fn) {
        const event = _create();
        event.trigger(key, fn);
      },

    }
  } ());
  return event;
} ())

  /************** 先发布后订阅 ********************/
  eventGlobal.trigger("click", 1);
  eventGlobal.trigger("click", 12);
  eventGlobal.listen("click", function (a) {
    console.log(a); // 输出:1
  });


  /************** 使用命名空间 ********************/
  eventGlobal.create("namespace1").trigger("click", 8);
  eventGlobal.create("namespace1").listen("click", (fn1 = function (a) {
    console.log(a); // 输出:1
  }));

  eventGlobal.create("namespace1").remove("click", fn1);
  eventGlobal.create("namespace1").trigger("click", 10);

小结

发布—订阅模式在实际开发中非常有用。发布—订阅模式的优点非常明显,一为时间上的解耦,二为对象之间的解耦。它的应用非常 广泛,既可以用在异步编程中,也可以帮助我们完成更松耦合的代码编写。发布—订阅模式还可 以用来帮助实现一些别的设计模式,比如中介者模式。 从架构上来看,无论是MVC 还是MVVM, 都少不了发布—订阅模式的参与,而且JavaScript 本身也是一门基于事件驱动的语言。 当然,发布—订阅模式也不是完全没有缺点。创建订阅者本身要消耗一定的时间和内存,而 且当你订阅一个消息后,也许此消息最后都未发生,但这个订阅者会始终存在于内存中。另外, 发布—订阅模式虽然可以弱化对象之间的联系,但如果过度使用的话,对象和对象之间的必要联 系也将被深埋在背后,会导致程序难以跟踪维护和理解。特别是有多个发布者和订阅者嵌套到一 起的时候,要跟踪一个bug 不是件轻松的事情。

彩蛋

这里存在了一个经典的js问题,即回调函数this的隐式绑定,及绑定丢失的问题。有关this的深度剖析,请查阅《你不知道的JavaScript(上卷)》第二部分的第二章——this的全面解析。

   each(getAvt, getName); // 控制台输出:111111111
    function each(n, fn) {
      fn.call(n);
    }
    function getAvt() {
      console.log('111111111');
    }
    function getName() {
      this();
    }

call函数的使用,改变了getName里的函数this的指向,所以这个时候调用this()即为调用getAvt函数本身。 通常我们使用的时候是后面跟着一个对象,这里跟着一个函数,函数也继承自对象,所以这里应该是绑定一个内存地址。

拓展《你不知道的JavaScript(上卷)》里的经典关于this变化的例子

function foo() {
  console.log( this.a );
}
var obj = {
  a: 2,
  foo: foo
};
var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"

虽然bar 是obj.foo 的一个引用,但是实际上,它引用的是foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑

function foo() {
  console.log( this.a );
}
function doFoo(fn) {
  // fn 其实引用的是foo
  fn(); // <-- 调用位置!
}
var obj = {
  a: 2,
  foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一 个例子一样。