自定义事件:打造属于你的消息系统
浏览器事件的局限
浏览器为我们提供了丰富的内置事件:click、input、submit、scroll 等等。这些事件涵盖了用户与页面交互的大部分场景。但在实际开发中,我们常常需要处理更复杂的业务逻辑。
比如,当用户完成了一个多步骤的表单流程,我们希望通知其他组件更新用户的积分显示;当购物车中的商品数量发生变化时,我们需要同时更新导航栏的购物车图标和侧边栏的总价。这些场景都涉及到不同组件之间的通信,而浏览器的内置事件无法直接满足这些需求。
这时候,自定义事件(Custom Events)就派上用场了。它让你能够创建自己的事件类型,定义事件携带的数据,构建起灵活的组件间通信机制。
什么是自定义事件
自定义事件是你自己定义的事件类型,不是浏览器预设的。它们使用与内置事件相同的机制进行触发和监听,具有同样的事件流(捕获和冒泡),可以携带自定义的数据。
自定义事件就像是你为应用程序创建的一套"暗号系统"。不同的模块之间通过这些暗号进行交流,当一个模块发出特定的暗号时,其他监听这个暗号的模块就会做出相应的反应。
// 创建一个自定义事件
const event = new CustomEvent("userLoggedIn", {
detail: {
username: "Sarah",
userId: 12345,
timestamp: Date.now(),
},
});
// 监听这个自定义事件
document.addEventListener("userLoggedIn", (event) => {
console.log(`User ${event.detail.username} logged in`);
console.log("User ID:", event.detail.userId);
});
// 触发这个事件
document.dispatchEvent(event);创建自定义事件
JavaScript 提供了 CustomEvent 构造函数来创建自定义事件。它接收两个参数:事件名称和配置对象。
基本语法
const event = new CustomEvent(eventType, eventOptions);- eventType:事件的名称(字符串),比如
'dataLoaded'、'cartUpdated'、'formValidated' - eventOptions:可选的配置对象,包含以下属性:
detail:任意数据,用于传递事件的额外信息bubbles:布尔值,指定事件是否冒泡(默认false)cancelable:布尔值,指定事件是否可以被取消(默认false)composed:布尔值,指定事件是否会穿过 Shadow DOM 边界(默认false)
简单示例
// 创建一个不冒泡、不携带数据的事件
const simpleEvent = new CustomEvent("taskCompleted");
// 创建一个冒泡的、携带数据的事件
const dataEvent = new CustomEvent("productAdded", {
bubbles: true,
detail: {
productId: "abc123",
productName: "Wireless Mouse",
quantity: 2,
price: 29.99,
},
});
// 创建一个可取消的事件
const cancelableEvent = new CustomEvent("beforeSave", {
bubbles: true,
cancelable: true,
detail: {
data: {
/* ... */
},
},
});触发自定义事件
创建事件后,需要使用 dispatchEvent() 方法来触发它。这个方法可以在任何 DOM 元素或 window、document 对象上调用。
// 在 document 上触发
document.dispatchEvent(event);
// 在特定元素上触发
const button = document.getElementById("submit-btn");
button.dispatchEvent(event);
// 在 window 上触发
window.dispatchEvent(event);dispatchEvent() 会返回一个布尔值:
- 如果事件是可取消的,且有监听器调用了
event.preventDefault(),返回false - 否则返回
true
const event = new CustomEvent("beforeDelete", {
cancelable: true,
detail: { itemId: 123 },
});
element.addEventListener("beforeDelete", (e) => {
if (!confirm("Are you sure?")) {
e.preventDefault(); // 阻止默认行为
}
});
const wasNotCancelled = element.dispatchEvent(event);
if (wasNotCancelled) {
// 事件没有被取消,执行删除操作
deleteItem(123);
} else {
// 事件被取消,不执行删除
console.log("Delete cancelled");
}监听自定义事件
监听自定义事件与监听内置事件完全一样,使用 addEventListener() 方法。
// 监听自定义事件
element.addEventListener("customEventName", (event) => {
// 访问事件携带的数据
console.log(event.detail);
});
// 也可以使用第三个参数控制捕获/冒泡
element.addEventListener("customEventName", handler, {
capture: true, // 在捕获阶段监听
once: true, // 只监听一次
passive: true, // 被动监听器
});传递数据:detail 属性
detail 属性是自定义事件最强大的特性之一。它可以携带任何类型的数据:对象、数组、字符串、数字,甚至函数。
// 传递对象
const event1 = new CustomEvent("userUpdated", {
detail: {
userId: 123,
changes: {
email: "[email protected]",
phone: "+1-555-0123",
},
timestamp: new Date(),
},
});
// 传递数组
const event2 = new CustomEvent("itemsSelected", {
detail: {
items: [1, 5, 8, 12],
selectAll: false,
},
});
// 传递复杂数据结构
const event3 = new CustomEvent("chartDataReady", {
detail: {
datasets: [
{ label: "Revenue", data: [10, 20, 30, 40] },
{ label: "Expenses", data: [5, 15, 25, 35] },
],
labels: ["Jan", "Feb", "Mar", "Apr"],
options: {
responsive: true,
maintainAspectRatio: false,
},
},
});
// 监听并使用数据
document.addEventListener("chartDataReady", (event) => {
const { datasets, labels, options } = event.detail;
renderChart(datasets, labels, options);
});实际应用场景
场景 1:购物车系统
在电商网站中,购物车的变化需要通知多个组件更新。
// 购物车模块
class ShoppingCart {
constructor() {
this.items = [];
}
addItem(product, quantity) {
this.items.push({ product, quantity });
// 触发自定义事件
const event = new CustomEvent("cartUpdated", {
bubbles: true,
detail: {
action: "add",
product: product,
quantity: quantity,
totalItems: this.getTotalItems(),
totalPrice: this.getTotalPrice(),
},
});
document.dispatchEvent(event);
}
removeItem(productId) {
const index = this.items.findIndex((item) => item.product.id === productId);
if (index > -1) {
const removed = this.items.splice(index, 1)[0];
const event = new CustomEvent("cartUpdated", {
bubbles: true,
detail: {
action: "remove",
product: removed.product,
totalItems: this.getTotalItems(),
totalPrice: this.getTotalPrice(),
},
});
document.dispatchEvent(event);
}
}
getTotalItems() {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}
getTotalPrice() {
return this.items.reduce(
(sum, item) => sum + item.product.price * item.quantity,
0
);
}
}
// 导航栏购物车图标组件
class CartIcon {
constructor() {
this.badge = document.getElementById("cart-badge");
this.setupEventListeners();
}
setupEventListeners() {
document.addEventListener("cartUpdated", (event) => {
this.updateBadge(event.detail.totalItems);
});
}
updateBadge(count) {
this.badge.textContent = count;
this.badge.style.display = count > 0 ? "block" : "none";
}
}
// 购物车侧边栏组件
class CartSidebar {
constructor() {
this.sidebar = document.getElementById("cart-sidebar");
this.totalElement = document.getElementById("cart-total");
this.setupEventListeners();
}
setupEventListeners() {
document.addEventListener("cartUpdated", (event) => {
const { action, product, totalPrice } = event.detail;
if (action === "add") {
this.addItemToList(product);
} else if (action === "remove") {
this.removeItemFromList(product.id);
}
this.updateTotal(totalPrice);
});
}
addItemToList(product) {
// 添加商品到侧边栏列表
const item = document.createElement("div");
item.className = "cart-item";
item.dataset.productId = product.id;
item.innerHTML = `
<span>${product.name}</span>
<span>$${product.price}</span>
`;
this.sidebar.appendChild(item);
}
removeItemFromList(productId) {
const item = this.sidebar.querySelector(`[data-product-id="${productId}"]`);
if (item) item.remove();
}
updateTotal(total) {
this.totalElement.textContent = `$${total.toFixed(2)}`;
}
}
// 使用
const cart = new ShoppingCart();
const cartIcon = new CartIcon();
const cartSidebar = new CartSidebar();
// 添加商品会自动更新所有相关组件
cart.addItem({ id: 1, name: "Laptop", price: 999.99 }, 1);
cart.addItem({ id: 2, name: "Mouse", price: 29.99 }, 2);场景 2:表单验证系统
多步骤表单中,每一步完成时触发事件,协调整体流程。
// 表单步骤管理器
class FormWizard {
constructor() {
this.currentStep = 0;
this.steps = document.querySelectorAll(".form-step");
this.setupEventListeners();
}
setupEventListeners() {
// 监听步骤完成事件
document.addEventListener("stepCompleted", (event) => {
const { stepNumber, isValid, data } = event.detail;
if (isValid) {
this.saveStepData(stepNumber, data);
this.goToNextStep();
}
});
// 监听整个表单完成事件
document.addEventListener("formCompleted", (event) => {
this.submitForm(event.detail.allData);
});
}
goToNextStep() {
this.steps[this.currentStep].classList.remove("active");
this.currentStep++;
if (this.currentStep < this.steps.length) {
this.steps[this.currentStep].classList.add("active");
} else {
// 所有步骤完成
const event = new CustomEvent("formCompleted", {
detail: {
allData: this.getAllData(),
},
});
document.dispatchEvent(event);
}
}
saveStepData(stepNumber, data) {
sessionStorage.setItem(`step_${stepNumber}`, JSON.stringify(data));
}
getAllData() {
const allData = {};
for (let i = 0; i < this.steps.length; i++) {
const stepData = sessionStorage.getItem(`step_${i}`);
if (stepData) {
Object.assign(allData, JSON.parse(stepData));
}
}
return allData;
}
submitForm(data) {
console.log("Submitting form:", data);
// 发送到服务器
}
}
// 单个表单步骤
class FormStep {
constructor(element, stepNumber) {
this.element = element;
this.stepNumber = stepNumber;
this.form = element.querySelector("form");
this.setupEventListeners();
}
setupEventListeners() {
this.form.addEventListener("submit", (e) => {
e.preventDefault();
this.validateAndComplete();
});
}
validateAndComplete() {
const formData = new FormData(this.form);
const data = Object.fromEntries(formData);
// 验证数据
const isValid = this.validate(data);
// 触发步骤完成事件
const event = new CustomEvent("stepCompleted", {
bubbles: true,
detail: {
stepNumber: this.stepNumber,
isValid: isValid,
data: data,
},
});
this.element.dispatchEvent(event);
}
validate(data) {
// 验证逻辑
return true;
}
}
// 进度指示器
class ProgressIndicator {
constructor() {
this.indicator = document.getElementById("progress-indicator");
this.setupEventListeners();
}
setupEventListeners() {
document.addEventListener("stepCompleted", (event) => {
if (event.detail.isValid) {
this.updateProgress(event.detail.stepNumber + 1);
}
});
}
updateProgress(completedSteps) {
const totalSteps = 4;
const percentage = (completedSteps / totalSteps) * 100;
this.indicator.style.width = `${percentage}%`;
}
}场景 3:实时通知系统
// 通知管理器
class NotificationManager {
constructor() {
this.container = document.getElementById("notifications");
this.setupEventListeners();
}
setupEventListeners() {
// 监听各种通知事件
document.addEventListener("notification:success", (e) => {
this.show(e.detail.message, "success");
});
document.addEventListener("notification:error", (e) => {
this.show(e.detail.message, "error");
});
document.addEventListener("notification:warning", (e) => {
this.show(e.detail.message, "warning");
});
document.addEventListener("notification:info", (e) => {
this.show(e.detail.message, "info");
});
}
show(message, type) {
const notification = document.createElement("div");
notification.className = `notification notification-${type}`;
notification.textContent = message;
this.container.appendChild(notification);
// 动画显示
setTimeout(() => notification.classList.add("show"), 10);
// 自动移除
setTimeout(() => {
notification.classList.remove("show");
setTimeout(() => notification.remove(), 300);
}, 3000);
}
}
// 工具函数:触发通知
function showNotification(message, type = "info") {
const event = new CustomEvent(`notification:${type}`, {
detail: { message },
});
document.dispatchEvent(event);
}
// 在应用的任何地方使用
function saveUserProfile(data) {
fetch("/api/profile", {
method: "POST",
body: JSON.stringify(data),
})
.then((response) => {
if (response.ok) {
showNotification("Profile saved successfully!", "success");
} else {
showNotification("Failed to save profile", "error");
}
})
.catch(() => {
showNotification("Network error occurred", "error");
});
}场景 4:插件系统
自定义事件可以用来构建插件系统,让第三方代码能够扩展应用功能。
// 主应用
class Application {
constructor() {
this.plugins = [];
}
registerPlugin(plugin) {
this.plugins.push(plugin);
plugin.init(this);
}
render() {
// 渲染前触发事件
const beforeRenderEvent = new CustomEvent("app:beforeRender", {
cancelable: true,
detail: { app: this },
});
const shouldContinue = document.dispatchEvent(beforeRenderEvent);
if (!shouldContinue) return;
// 执行渲染
this.doRender();
// 渲染后触发事件
const afterRenderEvent = new CustomEvent("app:afterRender", {
detail: { app: this },
});
document.dispatchEvent(afterRenderEvent);
}
doRender() {
console.log("Rendering application...");
}
}
// 分析插件
class AnalyticsPlugin {
init(app) {
// 监听应用事件
document.addEventListener("app:afterRender", () => {
this.trackPageView();
});
document.addEventListener("userLoggedIn", (e) => {
this.trackUser(e.detail.userId);
});
}
trackPageView() {
console.log("Analytics: Page view tracked");
}
trackUser(userId) {
console.log(`Analytics: User ${userId} tracked`);
}
}
// 日志插件
class LoggerPlugin {
init(app) {
document.addEventListener("app:beforeRender", (e) => {
console.log("[Logger] App is about to render");
});
document.addEventListener("app:afterRender", (e) => {
console.log("[Logger] App has finished rendering");
});
}
}
// 使用
const app = new Application();
app.registerPlugin(new AnalyticsPlugin());
app.registerPlugin(new LoggerPlugin());
app.render();自定义事件 vs 回调函数
你可能会问:为什么要使用自定义事件?直接传递回调函数不是更简单吗?
// 使用回调函数
function saveData(data, onSuccess, onError) {
fetch("/api/save", { method: "POST", body: JSON.stringify(data) })
.then(() => onSuccess())
.catch(() => onError());
}
// 使用自定义事件
function saveData(data) {
fetch("/api/save", { method: "POST", body: JSON.stringify(data) })
.then(() => {
document.dispatchEvent(
new CustomEvent("dataSaved", { detail: { data } })
);
})
.catch(() => {
document.dispatchEvent(
new CustomEvent("saveFailed", { detail: { data } })
);
});
}自定义事件的优势在于:
1. 解耦性更强
使用回调函数时,调用者和被调用者之间有明确的依赖关系。而自定义事件完全解耦,触发事件的代码不需要知道谁在监听,监听者也不需要知道谁触发了事件。
// 回调:紧耦合
class DataService {
constructor(ui, logger, analytics) {
this.ui = ui;
this.logger = logger;
this.analytics = analytics;
}
save(data) {
// 需要知道所有依赖
this.ui.showLoading();
this.logger.log("Saving...");
this.analytics.track("save_started");
// ...保存逻辑
}
}
// 事件:完全解耦
class DataService {
save(data) {
document.dispatchEvent(
new CustomEvent("saveStarted", { detail: { data } })
);
// 不需要知道谁在监听
// ...保存逻辑
}
}
// 各个模块独立监听
document.addEventListener("saveStarted", (e) => ui.showLoading());
document.addEventListener("saveStarted", (e) => logger.log("Saving..."));
document.addEventListener("saveStarted", (e) =>
analytics.track("save_started")
);2. 多个监听者
一个事件可以有多个监听者,而回调函数只能调用一次。
// 回调:只能通知一个对象
button.onclick = handleClick; // 只有一个处理函数
// 事件:可以有多个监听者
button.addEventListener("click", handleClick1);
button.addEventListener("click", handleClick2);
button.addEventListener("click", handleClick3);3. 利用事件冒泡
自定义事件可以利用 DOM 的事件冒泡机制,实现事件委托。
// 在父容器上监听所有子元素的自定义事件
const container = document.getElementById("container");
container.addEventListener("itemSelected", (e) => {
console.log("Item selected:", e.detail.itemId);
});
// 任何子元素都可以触发,事件会冒泡到容器
const item = document.getElementById("item-123");
item.dispatchEvent(
new CustomEvent("itemSelected", {
bubbles: true,
detail: { itemId: 123 },
})
);##常见问题与最佳实践
1. 事件命名规范
使用清晰、描述性的名称,采用一致的命名约定。
// ✅ 好的命名
new CustomEvent("userLoggedIn");
new CustomEvent("cart:itemAdded");
new CustomEvent("form:validated");
new CustomEvent("data:loaded");
// ❌ 不好的命名
new CustomEvent("event1");
new CustomEvent("update");
new CustomEvent("done");推荐使用命名空间(如 cart:itemAdded)来避免命名冲突。
2. 何时使用 bubbles
如果你希望事件能够被父元素捕获(比如使用事件委托),设置 bubbles: true。
// 需要冒泡
const event = new CustomEvent("notification:show", {
bubbles: true, // 允许事件冒泡
detail: { message: "Hello" },
});
element.dispatchEvent(event);3. 避免在 detail 中传递 DOM 元素
detail 中的数据应该是可序列化的,避免传递 DOM 元素或函数。
// ❌ 避免这样做
new CustomEvent("itemClicked", {
detail: {
element: document.getElementById("item"), // DOM 元素
callback: () => console.log("clicked"), // 函数
},
});
// ✅ 推荐做法
new CustomEvent("itemClicked", {
detail: {
elementId: "item", // 传递 ID
itemData: {
/* ... */
}, // 传递数据
},
});4. 清理事件监听器
记得在不需要时移除事件监听器,避免内存泄漏。
class Component {
constructor() {
this.handleDataUpdate = this.handleDataUpdate.bind(this);
}
init() {
document.addEventListener("dataUpdated", this.handleDataUpdate);
}
destroy() {
// 组件销毁时移除监听器
document.removeEventListener("dataUpdated", this.handleDataUpdate);
}
handleDataUpdate(event) {
console.log("Data updated:", event.detail);
}
}5. 使用类型检查(TypeScript)
如果使用 TypeScript,可以为自定义事件定义类型。
// 定义事件详情类型
interface CartUpdatedDetail {
action: "add" | "remove";
productId: string;
totalItems: number;
totalPrice: number;
}
// 创建类型安全的自定义事件
const event = new CustomEvent<CartUpdatedDetail>("cartUpdated", {
detail: {
action: "add",
productId: "abc123",
totalItems: 5,
totalPrice: 99.99,
},
});
// 监听器也有类型提示
document.addEventListener(
"cartUpdated",
(e: CustomEvent<CartUpdatedDetail>) => {
console.log(e.detail.productId); // TypeScript 知道这个属性存在
}
);总结
自定义事件是构建模块化、可扩展应用的强大工具。它们让你能够:
- 解耦组件:模块之间不需要直接引用,通过事件进行松耦合的通信
- 扩展功能:轻松添加新功能,只需监听相应的事件
- 构建插件系统:允许第三方代码通过事件钩子扩展应用
- 统一通信模式:使用与内置事件相同的 API,保持代码一致性
关键要点:
- 使用
CustomEvent构造函数创建事件 - 通过
detail属性传递数据 - 使用
dispatchEvent()触发事件 - 设置
bubbles: true让事件能够冒泡 - 使用清晰的命名约定
- 及时清理不再需要的监听器