JavaScript中的自定义事件
目录
创建自定义事件
事件构造器
dispatchEvent
冒泡示例
MouseEvent,KeyboardEvent 及其他
自定义事件
event.preventDefault()
事件中的事件是同步的
实现 replaceState && pushState 监听
总结
创建自定义事件
我们不仅可以分配事件处理程序,还可以从 JavaScript 生成事件。
自定义事件可用于创建图形组件。例如,在基于JavaScript的应用中,默认情况下根元素会响应open(打开菜单)和select(一项被选中)等事件以通知菜单的状态变化。另一个代码段可能会监听这些事件并根据具体情况作出相应反应以维护系统的一致性
除了能够自动生成外,我们还能够创建诸如 click 和 mousedown 这样的内置事件,这可能有助于提高自动化测试的效果
事件构造器
构建一个事件类的层次结构(hierarchy),类似于DOM元素类。根节点是内置的Event对象。
我们可以像这样创建 Event 对象:
letevent=newEvent``(type,[options]``)``;
参数:
type —— 事件标识符(即标识符),可为如"click"所示的字符串形式或自定义如"my-event"名称的形式。
options —— 具有两个可选属性的对象:
bubbles属性可取true或false值—— 当其设置为true时,则表示该事件会触发冒泡行为。cancelable属性可取true或false值—— 当其设置为true时,则表示该事件的执行将被取消。后续章节中我们将探讨这一特性在自定义事件中的具体意义。
默认情况下,以上两者都为 false:{bubbles: false, cancelable: false}。
当事件对象被创建后发生时
然后,在这种情况下,该处理程序会在其中作出相应的回应,并将其视为一个典型的浏览器事件。如果该事件是由 bubbles 标志创建的,则这个处理程序会将此事件进行处理或响应。
在下面这个示例中,在click 事件中使用的是JavaScript语言进行初始化创建。系统的处理流程与手动点击按钮的操作逻辑是一致的:
<button id="elem" onclick="alert('Click!');">Autoclick</button>
<script>
let event = new Event("click");
elem.dispatchEvent(event);
</script>
一种识别机制能够有效地区分真实用户行为与脚本诱导的行为。
在实际应用中,在检测到的真实用户行为中,“event.isTrusted”属性被标记为true;而当数据源是由脚本创建时,“event.isTrusted”属性则被标记为false。
冒泡示例](https://zh.javascript.info/dispatch-events#mao-pao-shi-li "冒泡示例")
我们可以创建一个名为 "hello" 的冒泡事件,并在 document 上捕获它。
我们需要做的就是将 bubbles 设置为 true:
<h1 id="elem">Hello from the script!</h1>
<script>
// 在 document 上捕获...
document.addEventListener("hello", function(event) { // (1)
alert("Hello from " + event.target.tagName); // Hello from H1
});
// ...在 elem 上 dispatch!
let event = new Event("hello", {bubbles: true}); // (2)
elem.dispatchEvent(event);
// 在 document 上的处理程序将被激活,并显示消息。
</script>

注意:
建议我们对自定义事件进行处理时应使用addEventListener命令。
因为对于内建事件来说on<event>属性是存在的;
而像document.onhello这种非标准属性则无法直接引用。
built-in events (click) and customized events (hello) share the same bubble mechanism. Customized events also feature a capturing phase and a bubble stage.
JavaScript中的事件机制:包括鼠标事件(MouseEvent)、键盘事件(KeyboardEvent)等
https://zh.javascript.info/dispatch-events#mouseeventkeyboardevent-ji-qi-ta
这是一个源自UI 事件规范的一个简明扼要的 UI 类别清单:
UIEventFocusEventMouseEventWheelEventKeyboardEvent- …
为了实现创建此类事件的目标, 我们应该避免使用 new Event 而采用其他方法. 比如点击一个按钮的动作可以通过 new MouseEvent("click") 来表示.
正确的构造器允许为该类型的事件指定标准属性。
就像鼠标事件的 clientX/clientY 一样:
let event = new MouseEvent("click", {
bubbles: true,
cancelable: true,
clientX: 100,
clientY: 100
});
alert(event.clientX); // 100
请注意:通用的 Event 构造器不允许这样做。
让我们试试:
let event = new Event("click", {
bubbles: true, // 构造器 Event 中只有 bubbles 和 cancelable 可以工作
cancelable: true,
clientX: 100,
clientY: 100
});
alert(event.clientX); // undefined,未知的属性被忽略了!
在技术层面而言,在创建之后直接将其设置为 event.clientX=100 是一种有效的解决方案以解决该问题。这表明该问题可以通过以下方式得以解决:通过这种方式不仅实现了目标而且符合相应的规定标准。所有由浏览器触发的行为都属于正确的行为类别
规范列举了各种用户界面(UI)事件及其属性的详细清单。例如,在HTML文档中定义了一个MouseEvent对象,并将其引用为MouseEvent
自定义事件
对于此类新增的事件类型, 如 hello, 我们建议在代码中引入 new CustomEvent 类型。从技术角度来看, CustomEvent 和 Event 类型具有相似性, 尽管如此, 在某些方面存在细微差别.
对于传递与事件相关的自定义信息而言,在第二个参数(对象)中我们可以为用户提供一个新增属性 detail
例如:
<h1 id="elem">Hello for John!</h1>
<script>
// 事件附带给处理程序的其他详细信息
elem.addEventListener("hello", function(event) {
alert(event.detail.name);
});
elem.dispatchEvent(new CustomEvent("hello", {
detail: { name: "John" }
}));
</script>

该 detail 属性允许存储任何形式的数据。从技术上讲,在常规新事件对象中我们无需为每个属性分配空间(因为我们可以在创建之后将任何属性分配给它)。然而 CustomEvent 为此提供了一个独特的 detail 字段(以避免与其他事件属性的冲突)。
此外,在对一个对象进行分类时我们可以将其归类为特定的行为模式。例如如果我们希望将某个对象指定为执行某个特定任务的行为模式则可以选择将该对象与相应的行为模式关联起来
event.preventDefault()
许多浏览器事件都有“默认行为”,例如,导航到链接,开始一个选择,等。
对于新增或自定义的事件,在浏览器中不会自动执行任何行为;然而,在处理此类事件时编写的代码可能会有自己的逻辑安排。当这些事件被触发后应采取什么操作。
当执行 event.preventDefault() 时,事件处理程序会响应这一行为,并指示这些行为应当被撤销。
在这样的情况下,在处理 elem 的 dispatch(event) 时会返回 false。因此,在这种情况下处理完事件后就不会再执行后续操作。
让我们看一个实际的例子 —— 一只隐藏的兔子(可以是关闭菜单或者其他)。
在下方可以看到一个在其上分配了‘hide’事件的#rabbit标记和hidden()方法(即hide()函数),以便让所有关注者都能知道这只兔子正在隐藏。
所有执行流程均可通过 rabbit.addEventListener('hide',...) 置入对这一事件的响应,并在必要时调用 event.preventDefault() 以终止此操作。这样一来,兔子就不会再隐藏起来了:
<pre id="rabbit">
|\ /|
\|_|/
/. .\
=\_Y_/=
{>o<}
</pre>
<button onclick="hide()">Hide()</button>
<script>
function hide() {
let event = new CustomEvent("hide", {
cancelable: true // 没有这个标志,preventDefault 将不起作用
});
if (!rabbit.dispatchEvent(event)) {
alert('The action was prevented by a handler');
} else {
rabbit.hidden = true;
}
}
rabbit.addEventListener('hide', function(event) {
if (confirm("Call preventDefault?")) {
event.preventDefault();
}
});
</script>

请特别注意:此事件必须配置为 cancelable: true ,如果不设置 cancelable: true ,则 event.preventDefault() 调用将不会被执行。
嵌套在其中的事件被协调一致地发生
通常情况下,事件会被排入队列进行处理。换而言之:每当浏览器执行onclick时发生了一个新的事件(例如鼠标移动),那么它的处理程序就会被安排进队列中相应的等待位置,并且在原有事件(如onclick)得到处理后会依次调用相关操作(如mousemove)
值得注意的是这种情况发生时通常是由另一个相关事件触发的。此类场景通常通过调用 dispatchEvent 来实现。当一个新的事件处理器被激活后,在随后执行现有事件处理器之前这些新增加的任务将立即被执行。
例如,在下面的代码中,在 onclick 事件发生时会被捕获的布尔值属性 menu-open 被设置为 true。
它会被立即执行,而不必等待 onclick 处理程序结束:
<button id="menu">Menu (click me)</button>
<script>
menu.onclick = function() {
alert(1);
menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
}));
alert(2);
};
// 在 1 和 2 之间触发
document.addEventListener('menu-open', () => alert('nested'));
</script>

输出顺序为:1 → nested → 2。
请特别注意嵌套事件 menu-open 会在 document 上被捕获。其传播和处理过程最先完成,并随后才会返回到外部代码块(如onclick事件处理函数)。
它不仅涉及 dispatchEvent 而言,在实际应用中还可能遇到其他情况。当一个事件处理器调用触发其他事件的函数时,在这种情况下这些方法将按照嵌套的方式进行同步处理。
有时会出现不符合预期的情况。我们希望确保onclick能够不受嵌套事件影响,并且能够优先执行。
我们可以将 dispatchEvent(或另一个触发事件的调用)放置在 onclick 末尾,并且最好是封装到一个零延迟的 setTimeout 中:
<button id="menu">Menu (click me)</button>
<script>
menu.onclick = function() {
alert(1);
setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
})));
alert(2);
};
document.addEventListener('menu-open', () => alert('nested'));
</script>

在当前情况下,在执行完所有代码后进行异步操作,并包含 menu.onclick 事件触发这一过程,请问事件处理程序之间具有高度的独立性吗?
输出顺序变成:1 → 2 → nested。
实现 replaceState && pushState 监听
浏览器内置于实现 onpopstate 事件时仅在以下两种情况下发生:当浏览器执行前进操作(如 history.go())、后退操作(如 history.back())或者基于时间编排的历史操作(如 history.forward())时会触发该事件;而对于历史上的推特状态(pushState)和替代状态(replaceState)则不会触发该事件。
现将对这两个方法进行改写与优化以使其能够对应地引发相应的 popstate 事件。
function bindHistoryEvent(method) {
const originMethod = history[method];
if (!originMethod) {
throw new Error("history has not this method named " + method);
}
// 闭包处理
return function () {
let result = null;
try {
originMethod.apply(this, arguments);
//这里也可以把事件名称写死,后面做对应的监听即可
const evt = new Event(method);
evt.arguments = arguments;
//分发事件
window.dispatchEvent(evt);
originMethod.apply(this, arguments);
} catch (error) {
throw new Error("执行出错");
}
return result;
};
}
function init() {
history.pushState = bindHistoryEvent("pushState");
history.replaceState = bindHistoryEvent("replaceState");
}
function test() {
init();
window.addEventListener("pushState", function (e) {
console.log("pushState触发", e);
});
window.addEventListener("replaceState", function (e) {
console.log("replaceState触发", e);
});
history.pushState({ name: "pushState" }, "", "pageA");
history.replaceState({ name: "replaceState" }, "", "pageB");
history.back();
}
test();

总结
要从代码生成一个事件,我们首先需要创建一个事件对象。
通用的 Event(name, options) 构造器支持任意类型的事件名称,并能够处理包含两个属性的对象:在构造器中接受任意事件名称和具有两个属性的对象,并作为构造器使用时灵活配置相关参数。
- 如果某个事件应该被触发后处理,则设置
$bubbles = true$.- 如果
$event.preventDefault()存在,则设置$cancelable = true$.
- 如果
其他原生事件构造器(如 MouseEvent 和 KeyboardEvent)均支持类型相关的属性来指定事件行为。例如,在鼠标事件中通常会使用 clientX 属性来表示相对于屏幕的位置。
当处理自定义事件时,建议采用 CustomEvent 构造器。其带有名为 detail 的附加属性,请将其赋值以存储特定数据。随后,所有的处理程序可以通过访问 event.detail 来获取所需数据。
然而技术上能够制造一系列如 click 和 keydown 的浏览器事件 为了避免潜在的安全风险 我们仍需审慎加以运用
我们不建议触发浏览器事件,因为这是一种非同寻常且笨拙的方式去处理程序逻辑.大多数时候,这种做法都是一个糟糕的设计架构.
可以生成原生事件:
- 如果第三方程序库缺乏其他交互途径,则使用这种间接手段将是使其正常运作的基础。
- 对于自动化测试而言,在脚本中执行操作并检查接口是否得到预期的响应是基本流程。
我们可以自定义命名的事件通常是为了架构目的而生成的,在此过程中会指示菜单(menu),滑块(slider),轮播(carousel)等内部操作发生了什么情况。
