实现一个观察者模式

497 阅读4分钟

前言

观察者模式也成为了发布订阅模式,以下面三部分组成

  1. 发布者
  2. 订阅者
  3. 消息队列

上面介绍了组成可能你还有疑惑,下面就举一个例子,小明打算去售楼处去买一套房子,销售小姐告诉他这套住宅暂时没有房源,小明于是留了手机号码给他,某一天有房源的时候通知他。 上面例子中,发布者就是售楼中心,订阅者就是小明,消息队列就是小明留的手机号码,观察者模式可以让对象松耦合在一起。

实现

下面使用 ES6 的语法来写,如果没有基础,推荐看一遍 es6 入门再来

上面介绍了定义,就根据上面的定义编写一个可以取消以及支持传递参数的观察者实例

const Event = new (class Watch {
  constructor() {
    // 消息队列
    this.list = {};
  }
  // 订阅
  subscribe(key, fn) {
    if (!this.list[key]) {
      // 避免重复
      this.list[key] = new Set();
    }
    this.list[key].add(fn);
  }
  // 触发
  trigger(key, ...args) {
    const v = this.list[key];
    if (!v) {
      return;
    }
    v.forEach(f => f.apply(this, args), this);
  }
  // 删除,key是必须的
  remove(key, fn) {
    const v = this.list[key];
    if (!v) {
      return false;
    }
    if (v.has(fn)) {
      return v.delete(fn);
    }
    // clear没有返回值,这里返回一个true
    return v.clear() || true;
  }
})();
Event.subscribe("abc", function(a) {
  console.log(a); // x
});
Event.trigger("abc", "x");

上面就实现了简单的观察者模式,不过可以观察上面代码可以发现观察者运行的机制是先订阅后发布,有没有办法类似于 QQ 消息一样,可以接收到离线消息,当然这个离线消息只能接收一次。

支持先发布后订阅

实现的思路很简单,就是通过一个离线消息队列,发布的时候判断这个离线消息队列存在么,如果存在,将消息存放在离线消息队列,当订阅的时候如果发现有离线消息队列就执行一次,之后清空

const Event = new (class Watch {
  constructor() {
    // 消息队列
    this.list = {};
    this.offLine = new Set();
  }
  // 订阅
  subscribe(key, fn) {
    if (!this.list[key]) {
      // 避免重复
      this.list[key] = new Set();
    }
    this.list[key].add(fn);
    if (this.offLine) {
      this.offLine.forEach(f => f(), this);
    }
    this.offLine = null;
  }
  // 触发
  trigger(key, ...args) {
    // 关键代码
    const fn = () => {
      const v = this.list[key];
      if (!v) {
        return;
      }
      v.forEach(f => f.apply(this, args), this);
    };
    if (this.offLine) {
      this.offLine.add(fn);
    }
    fn();
  }
  // 删除,key是必须的
  remove(key, fn) {
    const v = this.list[key];
    if (!v) {
      return false;
    }
    if (v.has(fn)) {
      return v.delete(fn);
    }
    // clear没有返回值,这里返回一个true
    return v.clear() || true;
  }
})();
Event.trigger("abc", "x");
Event.subscribe("abc", function(a) {
  console.log(a); // x
});

撒花,一个支持取消和先发布后订阅的观察者模式已经实现了,不过还是有待优化的地方,比如命名,我们能不能通过Event.created('zhangsan').trigger的形式来调用呢?

命名

动手写之前,我们先缕清一下头绪

  • 命名是否必须,能不能不通过created先调用,比如直接就是Event.trigger发布之后再订阅

ok,下面就是针对上面问题实现

// 定义一个基础类,这个类是实现的核心层
class Basics {
  _obj = {};
  _default = "default";
  _created(name = this._default) {
    // 定义消息队列和离线消息队列
    const list = {};
    let offLine = new Set();
    const then = this;
    const obj = {
      // 触发,如果第一次触发就添加到离线队列中
      trigger(key, ...rest) {
        const fn = () => {
          const arr = [list, key, ...rest];
          return then._trigger.apply(then, arr);
        };
        if (offLine) {
          offLine.add(fn);
        }
        return fn();
      },
      // 添加订阅者同时执行离线队列
      subscribe(key, fn) {
        then._subscribe(...[list, key, fn]);
        if (offLine) {
          offLine.forEach(f => f());
        }
        offLine = null;
      },
      // 删除,key是必须的
      remove(key, fn) {
        const v = this.list[key];
        if (!v) {
          return false;
        }
        if (v.has(fn)) {
          return v.delete(fn);
        }
        // clear没有返回值,这里返回一个true
        return v.clear() || true;
      }
    };
    // 判断命名来决定返回
    return name
      ? this._obj[name]
        ? this._obj[name]
        : (this._obj[name] = obj)
      : obj;
  }
  // 触发
  _trigger(l, k, ...args) {
    const v = l[k];
    if (!v || !v.size) {
      return;
    }
    return Array.from(v, f => f.apply(this, args), this);
  }
  _subscribe(list, key, fn) {
    if (!list[key]) {
      list[key] = new Set();
    }
    list[key].add(fn);
  }
}
// 这个是实现类
class Watch extends Basics {
  constructor() {
    // 必须,es6规定
    super();
    this.created = super._created;
  }
  trigger(key, ...rest) {
    const v = this.created();
    v.trigger(key, ...rest);
  }
  subscribe(key, fn) {
    const v = this.created();
    v.subscribe(key, fn);
  }
  remove(key, fn) {
    const v = this.created();
    v.remove(key, fn);
  }
}
const Event = new Watch();
Event.trigger("abc");
Event.subscribe("abc", function() {
  console.log(123);
});
Event.created("zhangsan").subscribe("abc", function(c) {
  console.log(c);
});
Event.created("zhangsan").trigger("abc", 456);

在实际中我们使用观察者模式的例子也有很多,比如一个网站,分为导航和侧边,当用户信息更新的时候展示部分需要更换就可以用到,还有我们用的框架,比如vue、React