发布订阅模式:
- 定义对象间一对多的依赖关系,使得每当一个对象改变状态,则所有依赖于它的对象都会得到通知并被自动更新
- 事件总线是对发布订阅模式的一种实现,也是一种集中式事件处理机制,允许不同组件之间相互通信,而不需要相互依赖,以此来解耦
- 应用场景:公众号消息,短信提醒等等
传统的事件总线和发布订阅的实现
事件总线
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
,根本是EventTarget
,window
、document
和元素节点都是继承于EventTarget
-
XMLHttpRequest
、WebSocket
也继承于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()
自定义事件三种方式
-
document.createEvent()
(废弃) -
new Event()
-
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;
}
})();
这篇就写完了,大家如果觉得好的话可以多多点赞,赠人玫瑰,手有余香。我会继续努力奉献更高质量的文章的。