极客时间返利平台,你可以在上边通过山月的链接购买课程,并添加我的微信 (shanyue94) 领取返现。
山月训练营之面试直通车 服务上线了,从准备简历、八股文准备、项目经历准备、面试、面经、面经解答、主观问题答复、谈薪再到入职的一条龙服务。

# 如何使用 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 new window)

一个简单的订阅发布模式实现如下,主要有两个核心 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 new window)

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 });
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 new window)

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

@infjer 何为循环订阅

Author

回答者: leblancy (opens new window)

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);
    }
  }
}
Last Updated: 11/27/2021, 6:11:48 PM