React 合成事件和 DOM 原生事件

Respect to React合成事件和DOM原生事件混用须知

React 的事件处理机制可以分为两个阶段:初始化渲染时在 root 节点上注册原生事件;原生事件触发时模拟捕获、目标和冒泡阶段派发合成事件。通过这种机制,冒泡的原生事件类型最多在 root 节点上注册一次,节省内存开销。且 React 为不同类型的事件定义了不同的处理优先级,从而让用户代码及时响应高优先级的用户交互,提升用户体验。

React 的事件机制中依赖合成事件这个核心概念。合成事件在符合 W3C 规范定义的前提下,抹平浏览器之间的差异化表现。并且简化事件逻辑,对关联事件进行合成。如每当表单类型组件的值发生改变时,都会触发 onChange 事件,而 onChange 事件由 change、click、input、keydown、keyup 等原生事件组成。

SyntheticEvent

React17版本开始,对事件系统的两个重要变更:

  • React17以前将事件(包括捕获及冒泡)委托到document的冒泡阶段触发。React17开始将冒泡事件委托到容器root的冒泡阶段触发,将捕获事件委托到容器root的捕获阶段触发
  • 移除事件池

注意: react的事件体系, 不是全部都通过事件委托来实现的. 有一些特殊情况, 是直接绑定到对应 DOM 元素上的(如:scroll, load), 它们都通过listenToNonDelegatedEvent函数进行绑定.

jsx
1const handleClick = (e) => {
2    setState(e.target.value) 
3    // 在 react 16 中, 因为事件复用 setState(异步) 时 e 的引用被清空。 
4    // react 17 中 no event pooling。 可以直接在异步中使用 event.
5    // 从 v17 开始,e.persist() 将不再生效,因为 SyntheticEvent 不再放入事件池中。
6} 
7
8<button onClick={handleClick}>
9  Activate Lasers
10</button>

响应顺序上合成事件晚于原生事件
NativeEvent stopPropagation 会阻止 SyntheticEvent.
SyntheticEvent stopPropagation 不会阻止 NativeEvent.

What are passive event listeners?

javascript
1element.addEventListener(event, handler[, options]);

event
事件名,例如:"click"

handler
处理程序。

options
具有以下属性的附加可选对象:

  • once:如果为 true,那么会在被触发后自动删除监听器。
  • capture:事件处理的阶段,由于历史原因,options 也可以是 false/true,它与 {capture: false/true} 相同。
  • passive:如果为 true,那么处理程序将不会调用 preventDefault()

https://stackoverflow.com/a/37721906/12583084

Problem: All modern browsers have a threaded scrolling feature to permit scrolling to run smoothly even when expensive JavaScript is running, but this optimization is partially defeated by the need to wait for the results of any touchstart and touchmove handlers, which may prevent the scroll entirely by calling preventDefault() on the event.

Solution: {passive: true}

By marking a touch or wheel listener as passive, the developer is promising the handler won't call preventDefault to disable scrolling. This frees the browser up to respond to scrolling immediately without waiting for JavaScript, thus ensuring a reliably smooth scrolling experience for the user.

js
1document.addEventListener("touchstart", function(e) {
2    console.log(e.defaultPrevented);  // will be false
3    e.preventDefault();   // does nothing since the listener is passive
4    console.log(e.defaultPrevented);  // still false
5}, Modernizr.passiveeventlisteners ? {passive: true} : false);

事件委托

捕获和冒泡允许我们实现最强大的事件处理模式之一,即 事件委托 模式。
这个想法是,如果我们有许多以类似方式处理的元素,那么就不必为每个元素分配一个处理程序 —— 而是将单个处理程序放在它们的共同祖先上。
在处理程序中,我们获取 event.target 以查看事件实际发生的位置并进行处理。

好处:

  • 简化初始化并节省内存:无需添加许多处理程序。
  • 更少的代码:添加或移除元素时,无需添加/移除处理程序。
  • DOM 修改 :我们可以使用 innerHTML 等,来批量添加/移除元素。

事件委托也有其局限性:

  • 首先,事件必须冒泡。而有些事件不会冒泡。此外,低级别的处理程序不应该使用 event.stopPropagation()
  • 其次,委托可能会增加 CPU 负载,因为容器级别的处理程序会对容器中任意位置的事件做出反应,而不管我们是否对该事件感兴趣。但是,通常负载可以忽略不计,所以我们不考虑它。

自定义事件

事件构造器

内建事件类形成一个层次结构(hierarchy),类似于 DOM 元素类。根是内建的 Event 类。

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

参数:

  • type —— 事件类型,可以是像这样 "click" 的字符串,或者我们自己的像这样 "my-event" 的参数。
  • options —— 具有两个可选属性的对象:
    • bubbles: true/false —— 如果为 true,那么事件会冒泡。
    • cancelable: true/false —— 如果为 true,那么“默认行为”就会被阻止。稍后我们会看到对于自定义事件,它意味着什么。
      默认情况下,以上两者都为 false:{bubbles: false, cancelable: false}

dispatchEvent

事件对象被创建后,我们应该使用 elem.dispatchEvent(event) 调用在元素上“运行”它。
然后,处理程序会对它做出反应,就好像它是一个常规的浏览器事件一样。如果事件是用 bubbles 标志创建的,那么它会冒泡。

在下面这个示例中,click 事件是用 JavaScript 初始化创建的。处理程序工作方式和点击按钮的方式相同:

html
1<button id="elem" onclick="alert('Click!');">Autoclick</button>
2
3<script>
4  let event = new Event("click");
5  elem.dispatchEvent(event);
6</script>

event.isTrusted
有一种方法可以区分“真实”用户事件和通过脚本生成的事件。
对于来自真实用户操作的事件,event.isTrusted 属性为 true,对于脚本生成的事件,event.isTrusted 属性为 false
我们应该对我们的自定义事件使用 addEventListener,因为 on<event> 仅存在于内建事件中,document.onhello 则无法运行。

event.preventDefault()

通过调用 event.preventDefault(),事件处理程序可以发出一个信号,指出这些行为应该被取消。
在这种情况下,elem.dispatchEvent(event) 的调用会返回 false。那么分派(dispatch)该事件的代码就会知道不应该再继续。

html
1<pre id="rabbit">
2  |\   /|
3   \|_|/
4   /. .\
5  =\_Y_/=
6   {>o<}
7</pre>
8<button onclick="hide()">Hide()</button>
9
10<script>
11  function hide() {
12    let event = new CustomEvent("hide", {
13      cancelable: true // 没有这个标志,preventDefault 将不起作用
14    });
15    if (!rabbit.dispatchEvent(event)) {
16      alert('The action was prevented by a handler');
17    } else {
18      rabbit.hidden = true;
19    }
20  }
21
22  rabbit.addEventListener('hide', function(event) {
23    if (confirm("Call preventDefault?")) {
24      event.preventDefault();
25    }
26  });
27</script>

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

事件中的事件是同步的

addEventListener => handler 是同步执行的。

浏览器默认行为

许多事件会自动触发浏览器执行某些行为。
例如:

  • 点击一个链接 —— 触发导航(navigation)到该 URL。
  • 点击表单的提交按钮 —— 触发提交到服务器的行为。
  • 在文本上按下鼠标按钮并移动 —— 选中文本。

阻止浏览器行为

有两种方式来告诉浏览器我们不希望它执行默认行为:

  • 主流的方式是使用 event 对象。有一个 event.preventDefault() 方法。
  • 如果处理程序是使用 on<event>(而不是 addEventListener)分配的,那返回 false 也同样有效。
html
1<a href="/" onclick="return false">Click here</a>
2or
3<a href="/" onclick="event.preventDefault()">here</a>

想要阻止默认行为 —— 可以使用 event.preventDefault()return false。第二个方法只适用于通过 on<event> 分配的处理程序。
addEventListenerpassive: true 选项告诉浏览器该行为不会被阻止。这对于某些移动端的事件(像 touchstarttouchmove)很有用,用以告诉浏览器在滚动之前不应等待所有处理程序完成。
如果默认行为被阻止,event.defaultPrevented 的值会变成 true,否则为 false