三行代码实现发布订阅,系统掌握 DOM 必备的自定义事件

1,546 阅读3分钟

发布订阅模式:

  • 定义对象间一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新
  • 事件总线是对发布订阅模式的一种实现,也是一种集中式事件处理机制,允许不同组件之间相互通信,而不需要相互依赖,以此来解耦
  • 应用场景:公众号消息,短信提醒等等

传统的事件总线和发布订阅的实现

事件总线

const eventBus = {
  // 保存类型和回调容器
  callbacks: {
    // login: [fn1, fn2]
  }
};

// 绑定事件
eventBus.on = function (eventName, callback) {
  // 判断有该类型事件
  if (this.callbacks[eventName]) {
    // 推入
    this.callbacks[eventName].push(callback);
  } else {
    // 构造该类型数组
    this.callbacks[eventName] = [callback];
  }
};

// 触发事件
eventBus.emit = function (eventName, data) {
  // 判断有该类型事件
  if (this.callbacks[eventName]?.length > 0) {
    // 遍历数组里函数执行传入数据
    this.callbacks[eventName].forEach(event => event(data));
  }
};

eventBus.off = function (eventName) {
  // 如果传入事件名
  if (eventName) {
    // 删除指定事件
    delete this.callbacks[eventName];
  } else {
    // 清空
    this.callbacks = {};
  }
};

// 测试
eventBus.on("login", data => {
  console.log(`用户已经登陆,数据${data}`);
});

eventBus.on("logout", data => {
  console.log(`用户已经退出,数据${data}`);
});

setTimeout(() => {
  eventBus.emit("login", "云牧");
  eventBus.emit("logout", "云牧");
}, 1000);

// eventBus.off("login");
// eventBus.off();

发布订阅

const PubSub = {
  id: 1,
  callbacks: {
    // pay:{
    //   // token_1: fn1,
    //   // token_2: fn2,
    // }
  }
};

PubSub.subscribe = function (channel, callback) {
  // 唯一的编号
  let token = "token_" + this.id++;
  // 判断callbacks是否存在channel
  if (this.callbacks[channel]) {
    // 存入
    this.callbacks[channel][token] = callback;
  } else {
    // 构造出对象存入
    this.callbacks[channel] = {
      [token]: callback
    };
  }
  return token;
};

// 订阅频道
PubSub.publish = function (channel, data) {
  // 获取当前频道所有的回调 遍历执行
  if (this.callbacks[channel]) {
    Object.values(this.callbacks[channel]).forEach(callback => callback(data));
  }
};

// 取消订阅
PubSub.unsubscribe = function (flag) {
  // 没有传则全部清空
  if (!flag) {
    this.callbacks = {};
    // 判断
  } else if (typeof flag === "string") {
    // 如果包含token_
    if (flag.includes("token_")) {
      // 遍历对象找到对应token
      const callbackobj = Object.values(this.callbacks).find(obj => obj.hasOwnProperty(flag));
      if (callbackobj) {
        delete callbackobj[flag];
      }
    } else {
      // 删除该订阅下所有回调
      delete this.callbacks[flag];
    }
  }
};

// 测试
const id1 = PubSub.subscribe("pay", data => {
  console.log("商家接受到了订单", data);
});
const id2 = PubSub.subscribe("pay", data => {
  console.log("骑手接受到了订单", data);
});
const id3 = PubSub.subscribe("cancel", data => {
  console.log("买家取消了订单", data);
});

// 取消了id1,商家无法接到订单
PubSub.unsubscribe(id1);

PubSub.publish("pay", {
  title: "鱼香肉丝",
  price: 20,
  address: "xxx"
});
PubSub.publish("cancel", {
  title: "鱼香肉丝",
  price: 20,
  address: "xxx"
});

四行实现发布订阅

<script>
  window.on = window.addEventListener;
  
  window.off = window.removeEventListener;
  
  window.emit = (type, data) => window.dispatchEvent(new CustomEvent(type, { detail: data }));
  // once绑定,多次 emit 事件只会被触发一次
  window.once = (type, listener) => window.addEventListener(type, listener, { once: true, capture: true });
  
  function handleEvent(e) {
    console.log("收到数据:", e.detail); // 收到数据: {message: 'i love you'}
  }
  
  // 绑定
  window.on("my-event", handleEvent);
  // 触发
  window.emit("my-event", { message: "i love you" });
  // 解绑
  window.off("my-event", handleEvent);
  // 此时触发失效
  window.emit("my-event", { message: "i love you" });
</script>

原理:

  • 表面是 window,根本是 EventTargetwindowdocument 和元素节点都是继承于 EventTarget

  • XMLHttpRequestWebSocket 也继承于 EventTarget

  • 继承于它,就可以实现事件中心,可以 EventTarget.addEventListener()EventTarget.removeEventListener()EventTarget.dispatchEvent

简化三行

<script>
  (window._on = window.addEventListener), (window._off = window.removeEventListener);
  
  window._emit = (type, data) => window.dispatchEvent(new CustomEvent(type, { detail: data }));
  
  window._once = (type, listener) => window.addEventListener(type, listener, { once: true, capture: true });
</script>

升级八行

  • 上面三行实现有诸多问题,比如不能多个实例、不能传递多参数、参数从 e.detail 获取不合理等

改进如下:

<script>
  class EventEmitter extends EventTarget {
    on = (type, listener, options) =>
      this.addEventListener(
        type,
        function wrap(e) {
          return (listener.__wrap__ = wrap), listener.apply(this, e.detail || []);
        },
        options
      );

    off = (type, listener) => this.removeEventListener(type, listener.__wrap__);

    emit = (type, ...args) => this.dispatchEvent(new CustomEvent(type, { detail: args }));

    once = (type, listener) => this.on(type, listener, { once: true, capture: true });
  }

  const emitter = new EventEmitter();

  function handleEvent(data) {
    console.log("收到数据:", data); // 收到数据: {message: 'i love you'}
  }
  // 绑定
  emitter.on("my-event", handleEvent);
  // 触发
  emitter.emit("my-event", { message: "i love you" });
  // 解绑
  emitter.off("my-event", handleEvent);
  // 此时触发失效
  emitter.emit("my-event", { message: "i love you" });

  // once 绑定触发
  emitter.once("my-once-event", handleEvent);
  emitter.emit("my-once-event", { message: "i love you once" });
  emitter.emit("my-once-event", { message: "i love you once" });
</script>

自定义事件

内置事件类型

  • 点击按钮,这是 click 事件
  • 输入框失焦,这是 blur 事件
  • 鼠标滚动,这是 wheel 事件

触发内置事件

  • element[eventType] 直接调用
  • new [Event] + dispatchEvent
document.createElement("a").click();

// 自定义事件触发
const event = new MouseEvent("click");

document.createElement("a").dispatchEvent(event);

前端快捷生成 uuid

URL.createObjectURL(new Blob([""])).split("/").pop()

自定义事件三种方式

  1. document.createEvent()(废弃)

  2. new Event()

  3. new CustomEvent()

document.createEvent()

  • const event = document.createEvent(type);

new Event

  • event = new Event(type, eventInit);

<body>
  <button type="button" id="btnTrigger">触发事件</button>

  <script>
    btnTrigger.addEventListener("myEvent", function () {
      console.log("myEvent trigger");
    });

    const event = new MouseEvent("myEvent");

    btnTrigger.dispatchEvent(event);
  </script>
</body>

可使用 new Event 通信,处理流程,达到解耦

<body>
  <button id="btn">开始吧</button>

  <div>
    <div id="step1"></div>
    <div id="step2"></div>
  </div>

  <script>
    function dispatchCustomEvent(target, type) {
      const event = new Event(type);
      target.dispatchEvent(event);
    }

    btn.addEventListener("click", () => {
      setTimeout(() => {
        dispatchCustomEvent(step1, "step-1");
      }, 1000);
    });

    step1.addEventListener("step-1", () => {
      // 显示当前流程文字
      step1.textContent = "流程1进行中...";
      // 继续下一个流程触发
      setTimeout(() => {
        dispatchCustomEvent(step2, "step-2");
      }, 1000);
    });

    step2.addEventListener("step-2", () => {
      step2.textContent = "流程2进行中...";
      setTimeout(() => {
        dispatchCustomEvent(window, "finished");
      }, 1000);
    });

    window.addEventListener("finished", () => {
      alert("task finished");
    });
  </script>
</body>

new CustomEvent

  • event = new Event(type, eventInit);

相比之前方式,它可以携带更多的参数了:

<body>
  <button id="btn">开始吧</button>

  <div>
    <div id="step1"></div>
    <div id="step2"></div>
  </div>

  <script>
    function dispatchCustomEvent(target, type, data) {
      const event = new CustomEvent(type, {
        detail: data
      });
      target.dispatchEvent(event);
    }

    btn.addEventListener("click", () => {
      setTimeout(() => {
        dispatchCustomEvent(step1, "step-1", { params: "step1参数" });
      }, 1000);
    });

    step1.addEventListener("step-1", e => {
      // 显示当前流程文字
      step1.textContent = "流程1进行中..." + e.detail.params;
      // 继续下一个流程触发
      setTimeout(() => {
        dispatchCustomEvent(step2, "step-2", { params: "step2参数" });
      }, 1000);
    });

    step2.addEventListener("step-2", e => {
      // 显示当前流程文字
      step2.textContent = "流程2进行中..." + e.detail.params;
      // 继续下一个流程触发
      setTimeout(() => {
        dispatchCustomEvent(window, "finished", "完成结果");
      }, 1000);
    });

    window.addEventListener("finished", e => {
      alert("task finished" + e.detail);
    });
  </script>
</body>

兼容垫片(如果浏览器不支持 CustomEvent 的话):

(function () {
  if (typeof CustomEvent !== "function") {
    var CustomEvent = function (event, params) {
      params = params || { bubbles: false, cancelable: false, detail: undefined };

      var evt = document.createEvent("CustomEvent");

      evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);

      return evt;
    };

    CustomEvent.prototype = window.Event.prototype;

    window.CustomEvent = CustomEvent;
  }
})();

这篇就写完了,大家如果觉得好的话可以多多点赞,赠人玫瑰,手有余香。我会继续努力奉献更高质量的文章的。