|
| 1 | +在前端开发世界中,JavaScript 和 HTML 之间往往通过 **事件** 来实现交互。其中多数为内置事件,本文主要介绍 JS**自定义事件概念和实现方式**,并结合案例详细分析自定义事件的原理、功能、应用及注意事项。 |
| 2 | + |
| 3 | +# 📚一、什么是自定义事件 |
| 4 | +在日常开发中,我们习惯监听页面许多事件,诸如:点击事件( `click` )、鼠标移动事件( `mousemove` )、元素失去焦点事件( `blur` )等等。<br /> |
| 5 | +<br />事件本质是一种通信方式,是一种消息,只有在多对象多模块时,才有可能需要使用事件进行通信。在多模块化开发时,可以使用**自定义事件**进行模块间通信。<br /> |
| 6 | +<br />当某些基础事件无法满足我们业务,就可以尝试 **自定义事件**来解决。 |
| 7 | + |
| 8 | +# 📚二、实现方式介绍 |
| 9 | + |
| 10 | +目前实现**自定义事件**的两种主要方式是 JS 原生的 `Event()` 构造函数和 `CustomEvent()` 构造函数来创建。<br /> |
| 11 | + |
| 12 | +## 1. Event() |
| 13 | +`Event()` 构造函数, 创建一个新的事件对象 `Event`。 |
| 14 | + |
| 15 | +### 1.1 语法 |
| 16 | +```javascript |
| 17 | +let myEvent = new Event(typeArg, eventInit); |
| 18 | +``` |
| 19 | + |
| 20 | +### 1.2 参数 |
| 21 | +`typeArg` : `DOMString` 类型,表示创建事件的名称;<br />`eventInit` :可选配置项,包括: |
| 22 | + |
| 23 | +| 字段名称 | 说明 | 是否可选 | 类型 | 默认值 | |
| 24 | +| :---: | :---: | :---: | :---: | :---: | |
| 25 | +| `bubbles` | 表示该事件**是否冒泡**。 | 可选 | `Boolean` | false | |
| 26 | +| `cancelable` | 表示该事件**能否被取消**。 | 可选 | `Boolean` | false | |
| 27 | +| `composed` | 指示事件是否会在**影子DOM根节点之外**触发侦听器。 | 可选 | `Boolean` | false | |
| 28 | + |
| 29 | + |
| 30 | +### 1.3 演示示例 |
| 31 | +```javascript |
| 32 | +// 创建一个支持冒泡且不能被取消的 pingan 事件 |
| 33 | +let myEvent = new Event("pingan", {"bubbles":true, "cancelable":false}); |
| 34 | +document.dispatchEvent(myEvent); |
| 35 | + |
| 36 | +// 事件可以在任何元素触发,不仅仅是document |
| 37 | +testDOM.dispatchEvent(myEvent); |
| 38 | +``` |
| 39 | + |
| 40 | +### 1.4 兼容性 |
| 41 | +<br />图片来源:[https://caniuse.com/](https://caniuse.com/) |
| 42 | + |
| 43 | +## 2. CustomEvent() |
| 44 | +`CustomEvent()` 构造函数, 创建一个新的事件对象 `CustomEvent`。 |
| 45 | + |
| 46 | +### 2.1 语法 |
| 47 | + |
| 48 | +```javascript |
| 49 | +let myEvent = new CustomEvent(typeArg, eventInit); |
| 50 | +``` |
| 51 | + |
| 52 | +### 2.2 参数 |
| 53 | +`typeArg` : `DOMString` 类型,表示创建事件的名称;<br />`eventInit` :可选配置项,包括: |
| 54 | + |
| 55 | +| 字段名称 | 说明 | 是否可选 | 类型 | 默认值 | |
| 56 | +| :---: | :---: | :---: | :---: | :---: | |
| 57 | +| `detail` | 表示该事件中需要被传递的数据,在 `EventListener` 获取。 | 可选 | `Any` | null | |
| 58 | +| `bubbles` | 表示该事件**是否冒泡**。 | 可选 | `Boolean` | false | |
| 59 | +| `cancelable` | 表示该事件**能否被取消**。 | 可选 | `Boolean` | false | |
| 60 | + |
| 61 | +### 2.3 演示示例 |
| 62 | +```javascript |
| 63 | +// 创建事件 |
| 64 | +let myEvent = new CustomEvent("pingan", { |
| 65 | + detail: { name: "wangpingan" } |
| 66 | +}); |
| 67 | + |
| 68 | +// 添加适当的事件监听器 |
| 69 | +window.addEventListener("pingan", e => { |
| 70 | + alert(`pingan事件触发,是 ${e.detail.name} 触发。`); |
| 71 | +}); |
| 72 | +document.getElementById("leo2").addEventListener( |
| 73 | + "click", function () { |
| 74 | + // 派发事件 |
| 75 | + window.dispatchEvent(pingan2Event); |
| 76 | + } |
| 77 | +) |
| 78 | +``` |
| 79 | +<br /> |
| 80 | +<br />我们也可以给自定义事件添加属性: |
| 81 | + |
| 82 | +```javascript |
| 83 | +myEvent.age = 18; |
| 84 | +``` |
| 85 | + |
| 86 | +### 2.4 兼容性 |
| 87 | +<br />图片来源:[https://caniuse.com/](https://caniuse.com/) |
| 88 | + |
| 89 | +### 2.5 IE8 兼容 |
| 90 | +分发事件时,需要使用 `dispatchEvent` 事件触发,它在 IE8 及以下版本中需要进行使用 `fireEvent` 方法兼容: |
| 91 | + |
| 92 | +```javascript |
| 93 | +if(window.dispatchEvent) { |
| 94 | + window.dispatchEvent(myEvent); |
| 95 | +} else { |
| 96 | + window.fireEvent(myEvent); |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +## 3. Event() 与 CustomEvent() 区别 |
| 101 | +从两者支持的参数中,可以看出:<br />`Event()` 适合创建简单的自定义事件,而 `CustomEvent()` 支持参数传递的自定义事件,它支持 `detail` 参数,作为事件中**需要被传递的数据**,并在 `EventListener` 获取。 |
| 102 | + |
| 103 | +**注意:**<br />当一个事件触发时,若相应的元素及其上级元素没有进行事件监听,则不会有回调操作执行。 <br />当需要对于子元素进行监听,可以在其父元素进行事件托管,让事件在事件冒泡阶段被监听器捕获并执行。此时可以使用 `event.target` 获取到具体触发事件的元素。 |
| 104 | + |
| 105 | +# 📚三、使用场景 |
| 106 | +**事件本质是一种消息**,事件模式本质上是**观察者模式**的实现,即能用**观察者模式**的地方,自然也能用**事件模式**。<br /> |
| 107 | + |
| 108 | +## 1.场景介绍 |
| 109 | +比如这两种场景:<br /> |
| 110 | + |
| 111 | +- **场景1:单个目标对象发生改变,需要通知多个观察者一同改变。** |
| 112 | + |
| 113 | +如:当微博列表中点击“关注”,此时会同时发生很多事:推荐更多类似微博,个人关注数增加...<br /> |
| 114 | + |
| 115 | +- **场景2:解耦多模块开协作。** |
| 116 | + |
| 117 | +如:小王负责A模块开发,小陈负责B模块开发,模块B需要模块A正常运行之后才能执行。 |
| 118 | + |
| 119 | +## 2. 代码实现 |
| 120 | + |
| 121 | +### 2.1 场景1实现 |
| 122 | +**场景1:单个目标对象发生改变,需要通知多个观察者一同改变。**<br />本例子模拟三个页面进行演示:<br />1.微博列表页(Weibo.js)<br />2.粉丝列表页(User.js)<br />3.微博首页(Home.js) |
| 123 | + |
| 124 | +在**微博列表页(Weibo.js)**中,我们导入其他两个页面,并且监听【关注微博】按钮的点击事件,在回调事件中,创建一个自定义事件 `focusUser`,并在 `document` 上使用 `dispatchEvent` 方法派发自定义事件。 |
| 125 | +```javascript |
| 126 | +// Weibo.js |
| 127 | +import UserModule from "./User.js"; |
| 128 | +import HomeModule from "./Home.js"; |
| 129 | +const eventButton = document.getElementById("eventButton"); |
| 130 | +eventButton.addEventListener("click", event => { |
| 131 | + const focusUser = new Event("focusUser"); |
| 132 | + document.dispatchEvent(focusUser); |
| 133 | +}) |
| 134 | +``` |
| 135 | + |
| 136 | +接下来两个页面实现的代码基本一致,这里为了方便观察,设置了两者不同输出日志。 |
| 137 | + |
| 138 | +```javascript |
| 139 | +// User.js |
| 140 | +const eventButton = document.getElementById("eventButton"); |
| 141 | +document.addEventListener("focusUser", event => { |
| 142 | + console.log("【粉丝列表页】监听到自定义事件触发,event:",event); |
| 143 | +}) |
| 144 | + |
| 145 | +// Home.js |
| 146 | +const eventButton = document.getElementById("eventButton"); |
| 147 | +document.addEventListener("focusUser", event => { |
| 148 | + console.log("【微博首页】监听到自定义事件触发,event:",event); |
| 149 | +}) |
| 150 | +``` |
| 151 | + |
| 152 | +点击【关注微博】按钮后,看到控制台输出如下日志信息: |
| 153 | + |
| 154 | + |
| 155 | + |
| 156 | +最终实现了,在 **微博列表页(Weibo.js)**组件负责派发事件,其他组价负责监听事件,这样三个组件之间耦合度非常低,完全不用关系对方,互相不影响。<br />**其实这也是实现了观察者模式。** |
| 157 | + |
| 158 | +### 2.2 场景2实现 |
| 159 | +**场景2:解耦多模块开协作。**<br />举个更直观的例子,当微博需要加入【**一键三连**】新功能,需要产品原型和UI设计完后,程序员才能开发。<br />本例子模拟四个模块:<br />1.流程控制(Index.js)<br />2.产品设计(Production.js)<br />3.UI设计(Design.js)<br />4.程序员开发(Develop.js) |
| 160 | + |
| 161 | + |
| 162 | + |
| 163 | +在**流程控制(Index.js)模块**中,我们需要将其他三个流程的模块都导入进来,然后监听【开始任务】按钮的点击事件,在回调事件中,创建一个自定义事件 `startTask`,并在 `document` 上使用 `dispatchEvent` 方法派发自定义事件。 |
| 164 | + |
| 165 | +```javascript |
| 166 | +// Index.js |
| 167 | +import ProductionModule from "./Production.js"; |
| 168 | +import DesignModule from "./Design.js"; |
| 169 | +import DevelopModule from "./Develop.js"; |
| 170 | + |
| 171 | +const start = document.getElementById("start"); |
| 172 | +start.addEventListener("click", event => { |
| 173 | + console.log("开始执行任务") |
| 174 | + const startTask = new Event("startTask"); |
| 175 | + document.dispatchEvent(startTask); |
| 176 | +}) |
| 177 | +``` |
| 178 | + |
| 179 | +在 Production 产品设计模块中,监听任务开始事件 `startTask` 后,模拟1秒后原型设计完成,并派发一个新的事件 `productionSuccess` ,开始接下来的UI稿设计。 |
| 180 | +```javascript |
| 181 | +// Production.js |
| 182 | +document.addEventListener("startTask", () => { |
| 183 | + console.log("产品开始设计..."); |
| 184 | + setTimeout(() => { |
| 185 | + console.log("产品原型设计完成"); |
| 186 | + console.log("--------------"); |
| 187 | + document.dispatchEvent(new Event("productionSuccess")); |
| 188 | + }, 1000); |
| 189 | +}); |
| 190 | +``` |
| 191 | + |
| 192 | +在UI稿设计和程序开发模块,其实也类似,代码实现: |
| 193 | +```javascript |
| 194 | +// Dedign.js |
| 195 | +document.addEventListener("productionSuccess", () => { |
| 196 | + console.log("UI稿开始设计..."); |
| 197 | + setTimeout(() => { |
| 198 | + console.log("UI稿设计完成"); |
| 199 | + console.log("--------------"); |
| 200 | + document.dispatchEvent(new Event("designSuccess")); |
| 201 | + }, 1000); |
| 202 | +}); |
| 203 | + |
| 204 | +// Production.js |
| 205 | +document.addEventListener("designSuccess", function (e) { |
| 206 | + console.log("开始开发功能..."); |
| 207 | + setTimeout(function () { |
| 208 | + console.log("【一键三连】开发完成"); |
| 209 | + }, 2000) |
| 210 | +}); |
| 211 | +``` |
| 212 | + |
| 213 | +开发完成后,我们点击【开始任务】按钮后,看到控制台输出如下日志信息: |
| 214 | + |
| 215 | + |
| 216 | + |
| 217 | +最终实现了在 **流程控制(Index.js)模块**负责派发事件,其他组件负责监听事件,按流程完成其他任务。<br />**可以看出,原型设计、UI稿设计和程序开发任务,互不影响,易于任务拓展。** |
| 218 | + |
| 219 | +# 📚四、总结 |
| 220 | +本文详细介绍 JS**自定义事件概念和实现方式**,并结合两个实际场景进行代码演示。细心的小伙伴会发现,这两个实际场景都是用 `Event()` 构造函数实现,当然也是可以使用 `CustomEvent` 构造函数来代替。<br />另外本文也详细介绍两种实现方式,包括其区别和兼容性。<br />最后也希望大家能在实际开发中,多思考代码解耦,适当使用**自定义事件**来提高代码质量。 |
| 221 | + |
| 222 | +如有错误,欢迎指点。 |
| 223 | + |
| 224 | +# 📚五、参考文章 |
| 225 | + |
| 226 | +- 《[javascript自定义事件功能与用法实例分析](https://m.jb51.net/article/127776.htm)》 |
| 227 | +- 《[Event - MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Event/Event)》 |
| 228 | +- 《[CustomEvent - MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/CustomEvent)》 |
| 229 | + |
| 230 | + |
| 231 | + |
| 232 | +|Author|王平安| |
| 233 | +|---|---| |
| 234 | + |
| 235 | +|博 客|www.pingan8787.com| |
| 236 | +|微 信|pingan8787| |
| 237 | +|每日文章推荐|https://github.com/pingan8787/Leo_Reading/issues| |
| 238 | +|ES小册|js.pingan8787.com| |
| 239 | + |
| 240 | +## 微信公众号 |
| 241 | + |
0 commit comments