Hypocrite Nush
  1. 1 Hypocrite Nush
  2. 2 Life Will Change Lyn
  3. 3 かかってこいよ NakamuraEmi
  4. 4 Last Surprise Lyn
  5. 5 Quiet Storm Lyn
  6. 6 Warcry mpi
  7. 7 One Last You Jen Bird
  8. 8 Flower Of Life 发热巫女
  9. 9 Libertus Chen-U
  10. 10 The Night We Stood Lyn
  11. 11 Time Bomb Veela
2017-05-24 22:47:46

设计模式——观察者模式

观察者模式又叫发布/订阅模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听一个主对象。当主对象的状态发生变化时就会通知所有观察者对象进行更新。


事件模型

在JS中我们一般使用事件模型来替代传统的观察者模式。只要我们在DOM节点上绑定过事件函数,那我们就使用过观察者模式了。来看个最简单的例子:

document.body.addEventListener('click', function() {
  alert('clicked');
}, false);

我们订阅了document.body上的click事件,当body被点击时触发click事件,就向订阅者发布通知,触发回调函数。

除了DOM事件,我们也可以自定义事件来实现观察者模式。流程如下:

  1. 指定发布者。
  2. 给发布者对象添加缓存列表,用于存放回调函数以便触发事件时通知订阅者执行。
  3. 事件触发时发布者遍历缓存列表,依次触发缓存列表里存放的订阅者回调函数。

一个全局观察者模式的通用实现如下:

const Event = (function(){
  let clientList = {},
      listen,
      trigger,
      remove;
  listen = function(key, fn) {  // 监听
    if (!this.clientList[key]) {
      this.clientList[key] = [];
    }
    this.clientList[key].push(fn);
  },
  trigger = function() {  // 触发
    let key = Array.prototype.shift.call(arguments),
        fns = this.clientList[key];
    if (!fns || fns.length === 0) {
      return false;
    }
    for (let i = 0, fn; fn = fns[i++];) {
      fn.apply(this, arguments);
    }
  },
  remove = function(key, fn) { // 移除
    let fns = this.clientList[key];
    if (!fns) {
      return false;
    }
    if (!fn) {
      fns && (fns.length = 0) // 取消所有订阅
    } else {
      for (let l = fns.length - 1; l >= 0; l--) {
        let _fn = fns[l];
        if (_fn === fn) {
          fns.splice(l, 1); // 删除订阅回调函数
        }
      }
    }
  };
return { // 暴露全局接口
  listen,
  trigger,
  remove
}
})();

全局通信实例

基于上一节实现的全局观察者模式,我们就可以在两个模块间进行事件通信了。假设要实现这样一个需求: a模块中有一个button,点击之后触发add事件,b模块监听到该事件后会显示按钮点击次数。我们用全局观察者模式实现如下:

let a = (function() {
  let count = 0;
  let button = document.getElementById('count');
  button.onclick = function() {
    Event.trigger('add', count++);
  }
})();

let b =(function() {
  let div = document.getElementById('show');
  Event.listen('add', function(count) {
    div.innerHTML = count;
  })
})();

至此我们就用观察者模式实现了一个简单的全局事件通信。功能虽然实现了,但仍然存在可以改进的地方。


观察者模式改进

我们目前实现的观察者模式,都是必须先订阅一个消息推入缓存,才能在触发事件后接收消息。但在不少的需求中,我们都需要先把消息接收到,等到有订阅时再发布给订阅者。比如QQ中的离线消息,当用户接收到消息可能还并没有订阅,等到用户上线时才触发订阅事件,接收到发布者的离线消息。为了满足这个需求,我们需要将离线事件缓存,当发布事件触发时,如果没有订阅事件则将发布回调函数继续包装后缓存。等到有对象订阅时再遍历取出执行。

除了订阅和发布的时间先后问题,我们还必须注意到这个观察者模式是基于全局对象的,一不小心就会出现变量名冲突的情况,所以我们还可以通过命名空间来保护这个观察者模式。具体优化的代码如下:

const Event = (function() {
  let global = this,
      Event,
      _default = 'default';

  Event = function() {
    let _listen,
        _trigger,
        _remove,
        _slice = Array.prototype.slice,
        _shift = Array.prototype.shift,
        _unshift = Array.prototype.unshift,
        namespaceCache = {},
        _create,
        find,
        each = function(ary, fn) {
          let ret;
          for (let i = 0, l = ary.length; i < l; i++) {
            let n = ary[i];
            ret = fn.call(n, i, n)
          }
          return ret;
        };
    _listen = function(key, fn, cache) {
      if (!cache[key]) {
        cache[key] = [];
      }
      cache[key].push(fn);
    };
    _remove = function(key, cache, fn) {
      if (cache[key]) {
        if (fn) {
          for (let i = cache[key].length;i>=0;i--) {
            if (cache[key][i] === fn) {
              cache[key].splice(i, 1);
            }
          }
        } else {
          cache[key] = [];
        }
      }
    };
    _trigger = function() {
      let cache = _shift.call(arguments),
          key = _shift.call(arguments),
          args = arguments,
          _self = this,
          ret,
          stack = cache[key];
      if (!stack || !stack.length) {
        return;
      }
      return each(stack, function() {
        return this.apply(_self, args);
      });
    };
    _create = function(namespace) {
      let namespace = namespace || _default;
      let cache = {},
          offlineStack = [],
          ret = {
            listen: function(key, fn, last) {
              _listen(key, fn, cache);
              if (offlineStack === null) {
                return;
              }
              if (last === 'last') {
                offlineStack.length && offlineStack.pop()();
              } else {
                each(offlineStack, function(){
                  this();
                });
              }
              offlineStack = null;
            },
            one: function(key, fn, last) {
              _remove(key, cache);
              this.listen(key, fn, last);
            },
            remove: function(key, fn) {
              _remove(key, cache, fn);
            },
            trigger: function() {
              let fn,
                  args,
                  _self = this;
              _unshift.call(arguments, cache);
              args = arguments;
              fn = function() {
                return _trigger.apply(_self, args);
              };
              if (offlineStack) {
                return offlineStack.push(fn);
              }
              return fn();
            }
          };
      return namespace ? (namespaceCache[namespace]) ? namespaceCache[namespace] : namespaceCache[namespace] = ret) : ret;
    };
    return {
      create: _create,
      one: function(key, fn, last) {
        let event = this.create();
        event.one(key, fn, last);
      },
      remove: function(key, fn) {
        let event = this.create();
        event.remove(key, fn);
      },
      listen: function(key, fn, last) {
        let event = this.create();
        event.listen(key, fn, last);
      },
      trigger: function() {
        let event = this.create();
        event.trigger.apply(this, arguments);
      }
    };
  }();
  return Event;
})();
// 先发布后订阅实例
Event.trigger('click', 1);
Event.listen('click', function(a) {
  console.log(a);
});
// 命名空间
Event.create('namespace1').listen('click', function(a) {
  console.log(a); // 1
});
Event.create('namespace1').trigger('click', 1);

Event.create('namespace2').listen('click', function(a) {
  console.log(a); // 2
});
Event.create('namespace2').trigger('click', 2);

小结

观察者模式在实际开发中非常有用,目前常见的MV*框架的数据绑定也离不开观察者模式。它不仅解耦了对象还解耦了时间,可以广泛应用于异步编程中。

-- EOF --

添加在分类「 前端开发 」下,并被添加 「设计模式」 标签。

文章目录

 - [事件模型](#事件模型)
 - [全局通信实例](#全局通信实例)
 - [观察者模式改进](#观察者模式改进)
 - [小结](#小结)
回到首页