高级前端
手写代码
【Q613】如何使用 JS 实现一个发布订阅模式

如何使用 JS 实现一个发布订阅模式

更多描述 使用 JS 实现一个发布订阅器,Event,示例如下:

const e = new Event();
 
e.on("click", (x) => console.log(x.id));
 
e.once("click", (x) => console.log(id));
 
//=> 3
e.emit("click", { id: 3 });
 
//=> 4
e.emit("click", { id: 4 });

API 如下:

class Event {
  emit(type, ...args) {}
 
  on(type, listener) {}
 
  once(type, listener) {}
 
  off(type, listener) {}
}

Issue 欢迎在 Gtihub Issue 中回答此问题: Issue 631 (opens in a new tab)

Author 回答者: shfshanyue (opens in a new tab)

一个简单的订阅发布模式实现如下,主要有两个核心 API

  • emit: 发布一个事件
  • on: 监听一个事件
  • off: 取消一个事件监听

实现该模式,使用一个 events 维护发布的事件:

const events = {
  click: [
    {
      once: true,
      listener: callback,
    },
    {
      listener: callback,
    },
  ],
};

具体实现代码如下所示

class Event {
  events = {};
 
  emit(type, ...args) {
    const listeners = this.events[type];
    for (const listener of listeners) {
      listener.listener(...args);
      if (listener.once) {
        this.off(type, listener.listener);
      }
    }
  }
 
  on(type, listener) {
    this.events[type] = this.events[type] || [];
    this.events[type].push({ listener });
  }
 
  once(type, listener) {
    this.events[type] = this.events[type] || [];
    this.events[type].push({ listener, once: true });
  }
 
  off(type, listener) {
    this.events[type] = this.events[type] || [];
    this.events[type] = this.events[type].filter(
      (listener) => listener.listener !== listener,
    );
  }
}

以上代码不够优雅,且有点小瑕疵,再次实现如下,代码可见 如何实现发布订阅器 - codepen (opens in a new tab)

class Event {
  events = {};
 
  emit(type, ...args) {
    const listeners = this.events[type];
    for (const listener of listeners) {
      listener(...args);
    }
  }
 
  on(type, listener) {
    this.events[type] = this.events[type] || [];
    this.events[type].push(listener);
  }
 
  once(type, listener) {
    const callback = (...args) => {
      this.off(type, callback);
      listener(...args);
    };
    this.on(type, callback);
  }
 
  off(type, listener) {
    this.events[type] = this.events[type] || [];
    this.events[type] = this.events[type].filter(
      (callback) => callback !== listener,
    );
  }
}
 
const e = new Event();
 
const callback = (x) => {
  console.log("Click", x.id);
};
e.on("click", callback);
e.on("click", callback);
 
// 只打印一次
const onceCallback = (x) => console.log("Once Click", x.id);
e.once("click", onceCallback);
e.once("click", onceCallback);
 
//=> 3
e.emit("click", { id: 3 });
 
//=> 4
e.emit("click", { id: 4 });

Author 回答者: heretic-G (opens in a new tab)

class Center {
  eventMap = {};
  on(event, fun) {
    this.#add(event, fun, "on");
  }
 
  once(event, fun) {
    this.#add(event, fun, "once");
  }
 
  #add(event, fun, type) {
    if (typeof fun !== "function")
      throw new TypeError(`${fun} is not a function`);
    if (!event) throw new Error(`need type`);
    if (!this.eventMap[event]) {
      this.eventMap[event] = [];
    }
    this.eventMap[event].push({
      event: fun,
      type: type,
    });
  }
 
  emit(event, ...args) {
    if (this.eventMap[event]) {
      this.eventMap[event] = this.eventMap[event].filter((curr) => {
        curr.data(...args);
        return curr.type !== "once";
      });
    }
  }
 
  remove(event, fun) {
    if (this.eventMap[event]) {
      this.eventMap[event] = this.eventMap[event].filter((curr) => {
        return curr.event !== fun;
      });
    }
  }
}

Author 回答者: infjer (opens in a new tab)

之前去B站面试,还追问了循环订阅如何处理。

Author 回答者: shfshanyue (opens in a new tab)

@infjer 何为循环订阅

Author 回答者: leblancy (opens in a new tab)

class Emitter {
  constructor() {
    this.events = {};
  }

  emit(type, ...args) {
    if (this.events[type] && this.events[type].length > 0) {
      this.events[type].forEach(cb => {
        cb.apply(this, args);
      });
    }
  }

  on(type, cb) {
    if (!this.events[type]) {
      this.events[type] = [];
    }
    this.events[type].push(cb);
  }

  once(type, cb) {
    const func = (...args) => {
      this.off(type, func);
      cb.apply(this, args);
    };

    this.on(type, func);
  }

  off(type, cb) {
    if (!cb) {
      this.events[type] = null;
    } else {
      this.events[type].filter(exec => exec !== cb);
    }
  }
}

Author 回答者: Hazel-Lin (opens in a new tab)

// 实现发布订阅模式
// on 订阅
// emit 发布
// off 取消订阅
 
class EventEmitter {
  constructor() {
    this.events = {};
  }
  // 订阅
  on(type, callback) {
    this.events[type] = this.events[type] || [];
    this.events[type].push(callback);
  }
  // 取消订阅
  off(type, callback) {
    if (!this.events[type]) return;
    this.events[type] = this.events[type].filter((event) => event !== callback);
  }
  // 发布
  emit(type, ...args) {
    if (!this.events[type]) return;
    // 遍历事件
    this.events[type].forEach((event) => {
      event(...args);
    });
  }
}
 
const e = new EventEmitter();
const callback2 = (data) => console.log(`${data.name}`);
 
e.on("click", callback2);
 
e.emit("click", { name: "xiaoming" });
 
e.off("click", callback2);
 
e.emit("click", { name: "lili" });