Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 2-ui/2-events/04-default-browser-action/article.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ menu.onclick = function(event) {
- `mousedown` —— 开始选择(移动鼠标进行选择)。
- 在 `<input type="checkbox">` 上的 `click` —— 选中/取消选中的 `input`。
- `submit` —— 点击 `<input type="submit">` 或者在表单字段中按下 `key:Enter` 键会触发该事件,之后浏览器将提交表单。
- `keydown` —— 按下一个按键可能会导致将字符添加到字段,或者触发其他行为。
- `keydown` —— 按下一个按键会导致将字符添加到字段,或者触发其他行为。
- `contextmenu` —— 事件发生在鼠标右键单击时,触发的行为是显示浏览器上下文菜单。
- ……还有更多……

Expand Down
173 changes: 89 additions & 84 deletions 2-ui/2-events/05-dispatch-events/article.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,37 @@
# 生成自定义事件
# 创建自定义事件

我们不仅可以分发事件,还可以从 JavaScript 中生成事件
我们不仅可以分配事件处理程序,还可以从 JavaScript 生成事件

自定义事件可以用于创建“图形组件”。例如,菜单组件的根元素可以通过触发 `open`(打开菜单)`select`(有一项被选中)等事件告诉菜单发生了什么
自定义事件可用于创建“图形组件”。例如,我们自己的基于 JavaScript 的菜单的根元素可能会触发 `open`(打开菜单)`select`(有一项被选中)等事件来告诉菜单发生了什么。另一个代码可能会监听事件,并观察菜单发生了什么

我们也可以生成一些像 `click``mousedown` 此类的内置事件,这些都有利于测试
我们不仅可以生成出于自身目的而创建的全新事件,还可以生成例如 `click``mousedown` 等内建事件。这可能会有助于自动化测试

## 事件构造器

事件会像 DOM 元素类一样形成层次结构。事件的底层是内置的 [Event](http://www.w3.org/TR/dom/#event) 类。
内建事件类形成一个层次结构,类似于 DOM 元素类。根是内建的 [Event](http://www.w3.org/TR/dom/#event) 类。

我们可以像这样创建 `Event` 对象:

```js
let event = new Event(event type[, options]);
let event = new Event(type[, options]);
```

参数:

- **event type** —— 可以是任何字符串,比如 `"click"` 或者我们自己喜欢的 `"hey-ho!"`
- **type** —— 事件类型,可以是像这样 `"click"` 的字符串,或者我们自己的像这样 `"my-event"` 的参数
- **options** —— 具有两个可选属性的对象:
- `bubbles: true/false` —— 如果是 `true`,那么事件冒泡
- `cancelable: true/false` —— 如果 `true`,那么“默认动作”就会被阻止。之后我们会看到对于自定义事件,这些意味着什么
- `bubbles: true/false` —— 如果为 `true`,那么事件会冒泡
- `cancelable: true/false` —— 如果为 `true`,那么“默认行为”就会被阻止。稍后我们会看到对于自定义事件,它意味着什么

默认情况下,它们都是 false:`{bubbles: false, cancelable: false}`。
默认情况下,以上两者都为 false:`{bubbles: false, cancelable: false}`。

## dispatchEvent

事件对象被创建后,我们应该调用 `elem.dispatchEvent(event)` 在元素上“运行”它。
事件对象被创建后,我们应该使用 `elem.dispatchEvent(event)` 调用在元素上“运行”它。

然后处理器对其作出反应,就好像它是一个正常的内置事件。如果事件是使用 `bubbles` 标志创建的,那么它就会冒泡
然后,处理程序会对它做出反应,就好像它是一个常规的浏览器事件一样。如果事件是用 `bubbles` 标志创建的,那么它会冒泡

在下面示例中,`click` 事件是用 JavaScript 初始化生成的。处理器执行效果和单击按钮的效果一样
在下面这个示例中,`click` 事件是用 JavaScript 初始化创建的。处理程序工作方式和点击按钮的方式相同

```html run no-beautify
<button id="elem" onclick="alert('Click!');">Autoclick</button>
Expand All @@ -43,9 +43,9 @@ let event = new Event(event type[, options]);
```

```smart header="event.isTrusted"
有一个可以区分 “真实”用户事件和 script 生成事件的方法
有一种方法可以区分“真实”用户事件和通过脚本生成的事件

`event.isTrusted` 属性为 `true`,则事件来自真实用户的动作,为 `false` ,则说明事件由脚本生成
对于来自真实用户操作的事件,`event.isTrusted` 属性为 `true`,对于脚本生成的事件,`event.isTrusted` 属性为 `false`。
```

## 冒泡示例
Expand All @@ -58,40 +58,44 @@ let event = new Event(event type[, options]);
<h1 id="elem">Hello from the script!</h1>

<script>
// catch on document...
// document 上捕获...
document.addEventListener("hello", function(event) { // (1)
alert("Hello from " + event.target.tagName); // Hello from H1
});

// ...dispatch on elem!
// ...elem 上 dispatch!
let event = new Event("hello", {bubbles: true}); // (2)
elem.dispatchEvent(event);

// 在 document 上的处理程序将被激活,并显示消息。

</script>
```


注意:

1. 我们应该使用 `addEventListener` 定义我们的事件,因为 `on<event>` 仅存在于内置事件中,`document.onhello` 则无法运行。
1. 我们应该对我们的自定义事件使用 `addEventListener`,因为 `on<event>` 仅存在于内建事件中,`document.onhello` 则无法运行。
2. 必须设置 `bubbles:true`,否则事件不会向上冒泡。

对于内置 (`click`) 和自定义 (`hello`) 的事件,冒泡机制是一样的。也有捕获和冒泡阶段
内建事件(`click`)和自定义事件(`hello`)的冒泡机制相同。自定义事件也有捕获阶段和冒泡阶段

## 鼠标事件,键盘事件和其他
## MouseEvent,KeyboardEvent 及其他

这里有一个在 [UI Event specification](https://www.w3.org/TR/uievents) 上的 UI 事件类短列表
这是一个摘自于 [UI 事件规范](https://www.w3.org/TR/uievents) 的一个简短的 UI 事件类列表

- `UIEvent`(UI 事件)
- `FocusEvent`(焦点事件)
- `MouseEvent`(鼠标事件)
- `WheelEvent`(滚轮事件)
- `KeyboardEvent`(键盘事件)
- `UIEvent`
- `FocusEvent`
- `MouseEvent`
- `WheelEvent`
- `KeyboardEvent`
- ...

如果我们想要创建这样的事件,我们应该使用它们而不是“新事件”。例如,`new MouseEvent("click")`。
如果我们想要创建这样的事件,我们应该使用它们而不是 `new Event`。例如,`new MouseEvent("click")`。

正确的构造函数允许为该类型的事件指定标准属性
正确的构造器允许为该类型的事件指定标准属性

就像鼠标事件 `clientX/clientY` 一样:
就像鼠标事件的 `clientX/clientY` 一样:

```js run
let event = new MouseEvent("click", {
Expand All @@ -106,41 +110,40 @@ alert(event.clientX); // 100
*/!*
```

请注意:通用 `Event` 构造器不允许这样做。
请注意:通用的 `Event` 构造器不允许这样做。

让我们试试:

```js run
let event = new Event("click", {
bubbles: true, // only bubbles and cancelable
cancelable: true, // work in the Event constructor
bubbles: true, // 构造器 Event 中只有 bubbles cancelable 可以工作
cancelable: true,
clientX: 100,
clientY: 100
});

*!*
alert(event.clientX); // undefined, the unknown property is ignored!
alert(event.clientX); // undefined,未知的属性被忽略了!
*/!*
```

从技术上讲,我们可以通过在创建后直接分发 `event.clientX=100` 来解决这个问题。所以这是一个方便和遵守规则的问题。浏览器生成的事件总是具有正确的类型
从技术上讲,我们可以通过在创建后直接分配 `event.clientX=100` 来解决这个问题。所以,这是一个方便和遵守规则的问题。浏览器生成的事件始终具有正确的类型

不同 UI 事件的所有属性列表在说明书中,例如 [MouseEvent](https://www.w3.org/TR/uievents/#mouseevent)。
规范中提供了不同 UI 事件的属性的完整列表,例如 [MouseEvent](https://www.w3.org/TR/uievents/#mouseevent)。

## 自定义事件

对于我们自己的自定义事件,像 `"hello"`,我们应该使用 `new CustomEvent`。从技术上来说,[CustomEvent](https://dom.spec.whatwg.org/#customevent) 和 `Event` 一样。除了一点不同之外。

在第二个参数(对象)中,我们可以在事件中为我们想要传递的任何自定义信息添加一个附加的属性 `detail`。
对于我们自己的全新事件类型,例如 `"hello"`,我们应该使用 `new CustomEvent`。从技术上讲,[CustomEvent](https://dom.spec.whatwg.org/#customevent) 和 `Event` 一样。除了一点不同。

在第二个参数(对象)中,我们可以为我们想要与事件一起传递的任何自定义信息添加一个附加的属性 `detail`。

例如:

```html run refresh
<h1 id="elem">Hello for John!</h1>

<script>
// additional details come with the event to the handler
// 事件附带给处理程序的其他详细信息
elem.addEventListener("hello", function(event) {
alert(*!*event.detail.name*/!*);
});
Expand All @@ -149,47 +152,48 @@ alert(event.clientX); // undefined, the unknown property is ignored!
*!*
detail: { name: "John" }
*/!*
});
}));
</script>
```

`detail` 属性可以有任何数据。从技术上讲,我们可以不用,因为我们可以在创建后将任何属性分配到常规的 `new Event` 对象中。但是 `CustomEvent` 为它提供了特殊的 `detail` 字段,以避免与其他事件属性的冲突。
`detail` 属性可以有任何数据。从技术上讲,我们可以不用,因为我们可以在创建后将任何属性分配给常规的 `new Event` 对象中。但是 `CustomEvent` 提供了特殊的 `detail` 字段,以避免与其他事件属性的冲突。

事件类告诉一些关于“是什么类型的事件”的信息,如果事件是自定义的,那么我们应该使用 `CustomEvent` 来明确它是什么。
此外,事件类描述了它是“什么类型的事件”,如果事件是自定义的,那么我们应该使用 `CustomEvent` 来明确它是什么。

## event.preventDefault()

如果 `cancelable:true` 被指定,那么我们可以在脚本生成的事件上调用 `event.preventDefault()`
许多浏览器事件都有“默认行为”,例如,导航到链接,开始一个选择,等

当然,如果事件有一个非标准的名称,那么浏览器就不知道它,而且它也没有“默认浏览器动作”
对于新的,自定义的事件,绝对没有默认的浏览器行为,但是分派(dispatch)此类事件的代码可能有自己的计划,触发该事件之后应该做什么

但是事件生成代码可能会在 `dispatchEvent` 之后安排一些动作
通过调用 `event.preventDefault()`,事件处理程序可以发出一个信号,指出这些行为应该被取消

调用 `event.preventDefault()` 是处理器发送不应该执行这些操作的信号的一种方法
在这种情况下,`elem.dispatchEvent(event)` 的调用会返回 `false`。那么分派(dispatch)该事件的代码就会知道不应该再继续

在这种情况下,`elem.dispatchEvent(event)` 会返回 `false`。而且事件生成代码知道处理器不应该继续
让我们看一个实际的例子 —— 一只隐藏的兔子(可以是关闭菜单或者其他)

例如,在下面的示例中有一个 `hide()` 函数。它在元素 `#rabbit` 上生成 `"hide"` 事件。通知所有相关联部分兔子将要隐藏起来了
在下面,你可以看到一个在其上分派了 `"hide"` 事件的 `#rabbit` 和 `hide()` 函数,以使所有感兴趣的各方面都知道这只兔子要隐藏起来

`rabbit.addEventListener('hide',...)` 设置的处理器将会知道这些,并且如果需要,可以通过调用 `event.preventDefault()` 来阻止该操作。然后兔子就不会隐藏了
任何处理程序都可以使用 `rabbit.addEventListener('hide',...)` 来监听该事件,并在需要时使用 `event.preventDefault()` 来取消该行为。然后兔子就不会藏起来了

```html run refresh
```html run refresh autorun
<pre id="rabbit">
|\ /|
\|_|/
/. .\
=\_Y_/=
{>o<}
</pre>
<button onclick="hide()">Hide()</button>

<script>
// hide() will be called automatically in 2 seconds
// hide() 将在 2 秒后被自动调用
function hide() {
let event = new CustomEvent("hide", {
cancelable: true // without that flag preventDefault doesn't work
cancelable: true // 没有这个标志,preventDefault 将不起作用
});
if (!rabbit.dispatchEvent(event)) {
alert('the action was prevented by a handler');
alert('The action was prevented by a handler');
} else {
rabbit.hidden = true;
}
Expand All @@ -200,29 +204,25 @@ alert(event.clientX); // undefined, the unknown property is ignored!
event.preventDefault();
}
});

// hide in 2 seconds
setTimeout(hide, 2000);

</script>
```

请注意:该事件必须具有 `cancelable: true` 标志,否则 `event.preventDefault()` 调用将会被忽略。

## Events-in-events 同步
## 事件中的事件是同步的

事件通常都是同步处理的。也就是说:如果浏览器正在处理 `onclick`,而且在处理过程中发生了一个新的事件,那么它将等待,直到 `onclick` 处理完成。
通常事件是被异步处理的。也就是说:如果浏览器正在处理 `onclick`,在此处理过程中发生了一个新的事件,那么它将等待,直到 `onclick` 处理完成。

异常情况是一个事件从另一个事件中启动
唯一的例外就是,一个事件是在另一个事件中发起的

然后控制器会跳到嵌套事件处理器中,并且(执行完成)之后返回
然后,程序执行的控制流会跳转到嵌套的事件的处理程序,执行完成后返回

例如,这里的 `menu-open` 嵌套事件在 `onclick` 期间被同步处理:
例如,这里的嵌套事件 `menu-open` `onclick` 期间被同步处理:

```html run
```html run autorun
<button id="menu">Menu (click me)</button>

<script>
// 1 -> nested -> 2
menu.onclick = function() {
alert(1);

Expand All @@ -234,55 +234,60 @@ alert(event.clientX); // undefined, the unknown property is ignored!
alert(2);
};

document.addEventListener('menu-open', () => alert('nested'))
document.addEventListener('menu-open', () => alert('nested'));
</script>
```
```

输出顺序为:1 -> nested -> 2。

请注意 `menu-open` 嵌套事件会冒泡,而且是在 `document` 被处理。嵌套事件的传播是在处理返回到外部代码 (`onclick`) 之前就已经全部完成的
请注意,嵌套事件 `menu-open` 会完全冒泡,并在 `document` 上被处理。嵌套事件的传播(propagation)和处理必须完全完成,然后处理过程才会返回到外部代码(`onclick`

这不仅仅是 `dispatchEvent`,还有其他案例。JavaScript 在事件处理时可以调用导致其他事件的方法 —— 它们也是被同步处理的。
这不仅与 `dispatchEvent` 有关,还有其他情况。事件处理程序中的 JavaScript 可以调用会引发其他事件的方法 —— 它们也是被同步处理的。

如果我们不喜欢,可以将 `dispatchEvent`(或者其他事件触发器调用)放在 `onclick` 结束,或者如果不方便,可以将其包装在 `setTimeout(...,0)` 中:
如果我们不喜欢它,可以将 `dispatchEvent`(或者其他触发事件的调用)放在 `onclick` 末尾,或者最好将其包装到零延迟的 `setTimeout` 中:

```html run
<button id="menu">Menu (click me)</button>

<script>
// 1 -> 2 -> nested
menu.onclick = function() {
alert(1);

// alert(2)
setTimeout(() => menu.dispatchEvent(new CustomEvent("menu-open", {
bubbles: true
})), 0);
})));

alert(2);
};

document.addEventListener('menu-open', () => alert('nested'))
document.addEventListener('menu-open', () => alert('nested'));
</script>
```
```

现在,`dispatchEvent` 在当前代码执行完成之后异步运行,包括 `mouse.onclick`,因此,事件处理程序是完全独立的。

输出顺序变成:1 -> 2 -> nested。

## 总结

要生成一个事件,我们首先需要创建一个事件对象。
要从代码生成一个事件,我们首先需要创建一个事件对象。

泛型 `Event(name, options)` 构造器接受任意事件名,`options` 对象具有两个属性
- `bubbles: true` ,如果事件应该冒泡的话
- `cancelable: true` 则 `event.preventDefault()` 应该有效。
通用的 `Event(name, options)` 构造器接受任意事件名称和具有两个属性的 `options` 对象
- 如果事件应该冒泡,则 `bubbles: true`。
- 如果 `event.preventDefault()` 应该有效,则 `cancelable: true`

其他像 `MouseEvent``KeyboardEvent` 这样的原生事件构造器,接受特定于该事件类型的属性。例如,鼠标事件的 `clientX`。
其他像 `MouseEvent``KeyboardEvent` 这样的原生事件的构造器,都接受特定于该事件类型的属性。例如,鼠标事件的 `clientX`。

对于自定义事件我们应该使用 `CustomEvent` 构造器。它有一个名为 `detail` 的附加选项,我们应该将特定事件的数据指派给它。然后处理器可以以 `event.detail` 的形式访问它
对于自定义事件,我们应该使用 `CustomEvent` 构造器。它有一个名为 `detail` 的附加选项,我们应该将事件特定的数据分配给它。然后,所有处理程序可以以 `event.detail` 的形式来访问它

尽管技术上有可能产生像 `click` 或者 `keydown` 这样的浏览器事件,但我们还是该谨慎使用
尽管技术上有可能生成像 `click` `keydown` 这样的浏览器事件,但我们还是应谨慎使用

我们不应该生成浏览器事件,因为这是运行处理器的一种 hacky 方式。大多数来说,这都是一种糟糕的架构。
我们不应该生成浏览器事件,因为这是运行处理程序的一种怪异(hacky方式。大多数时候,这都是一种糟糕的架构。

可以生成原生事件:

- 如果他们不提供其他的交互方式,脏黑客行为可以制作第三方库操作所需的方式
- 对于自动化测试,要在脚本中“单击按钮”并查看接口是否正确反应
- 如果第三方程序库不提供其他交互方式,那么这是使第三方程序库工作所需的一种肮脏手段
- 对于自动化测试,要在脚本中“点击按钮”并查看接口是否正确响应

使用我们自己的名字来自定义的事件通常是为架构目的产生的,用来指示菜单、滑块、轮播等内部发生什么
使用我们自己的名称的自定义事件通常是出于架构的目的而创建的,以指示发生在菜单(menu),滑块(slider),轮播(carousel)等内部发生了什么