React事件机制

概述

React 基于浏览器的事件机制实现了一套自身的事件机制,它符合W3C规范,包括事件触发、事件冒泡、事件捕获、事件合成和事件派发等

原生事件机制

在原生Web应用开发时,执行事件动作的回调函数通常是绑定在要监听的 DOM 节点上的。 DOM 节点与 DOM 节点之间存在包含关系时,如果一个 DOM 节点被点击,它的父节点、祖先结点实际上是都被点击了的,绑定在上面的点击事件监听函数均应该被执行,那么各个事件监听函数的执行顺序应该是什么样的呢?

DOM 事件标准描述了事件传播的 3 个阶段:

  • 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素
  • 目标阶段(Target phase)—— 事件到达目标元素
  • 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡

React 事件机制

为了在底层磨平不同浏览器的差异,React 实现了统一的事件机制,开发者不再需要处理浏览器事件机制方面的兼容问题,在上层面向开发者暴露出稳定、统一的、与原生事件相同的事件接口

事件池

React 事件池仅支持在 React 16 及更早版本中,在 React 17 已经不使用事件池

在React 16中,为了减少频繁创建和销毁事件对象,提高React的性能,引入了事件池的概念

事件池内的对象是复用的,在一个事件对象使用完毕后,并不会将这个事件对象销毁,而是将其重置(各个属性均设置为null)之后放回事件池

在React 16中,事件对象不能直接在异步执行的代码中调用,需要先执行事件对象的SyntheticEvent.pertsist方法对事件对象进行持久化,移除事件池不再复用之后才能在异步执行的代码块中使用事件对象

事件池对性能带来提升在现代浏览器中非必要,所以在React 17中,已经移除了事件池的概念,事件对象不会再复用,虽然事件对象仍然有pertsist方法,但是该方法执行之后并不会有什么影响

事件代理机制

原生事件绑定中,事件是通过dom.addEventListener直接绑定到对应的节点上的

React 中的事件机制与原生的事件机制有较大的区别:React 内部自己实现了一套事件绑定机制,并没有直接将事件的回调函数绑定到对应的节点上,而是利用了浏览器的事件冒泡和捕获机制,在React的root节点(React v16版本为document对象) 上添加了所有浏览器原生冒泡事件和捕获事件的代理,当root节点内部某个事件被触发:

  • 事件被 React 的 root 节点捕获之后,就会执行 React 的捕获事件代理,合成事件对象并模拟浏览器的捕获事件机制将合成事件对象分发给对应的React捕获事件监听执行
  • 事件被冒泡到 root 节点之后,就会执行 React 的冒泡事件代理,将合成事件对象分发给对应的React冒泡事件监听执行

React 17 与 React 16 不同之处如下:

简单描述一下v17事件系统的改版:

  • 事件统一绑定container上,ReactDOM.render(app, container);而不是document上,这样好处是有利于微前端的,微前端一个前端系统中可能有多个应用,如果继续采取全部绑定在document上,那么可能多应用下会出现问题
  • 对齐原生浏览器事件
    • React 17 中终于支持了原生捕获事件的支持, 对齐了浏览器原生标准
    • onScroll 事件不再进行事件冒泡
    • onFocus 和 onBlur 使用原生 focusin, focusout 合成
  • 取消事件池

事件代理机制

React 在 root 节点上添加了各个委托事件的监听器,分别监听对应事件的捕获和冒泡事件,然后将监听到的捕获或者冒泡事件通过dispatchEvent等函数将浏览器原生事件合成为SyntheicEvent,并收集事件的触发路径,然后分别模拟浏览器原生事件的捕获和冒泡,按照一定的顺序执行对应节点的事件回调函数

  • 事件合成
    React 在构造 SyntheticEvent 对象时,会将浏览器的原生事件对象保存在SyntheticEvent.nativeEvent属性中,在需要时可以通过该属性获取浏览器原生的事件对象来进行一些操作

  • 事件链路收集
    简而言之就是收集从触发事件的节点开始向上直到最外层所有的需要执行的事件回调函数

    以点击click事件为例,React会根据当前事件的target属性,也就是触发click事件的原生dom节点,找到 最近的fiber节点,然后沿着这个fiber节点逐层向上,收集每一层上对应的事件回调,click事件时就是onClick事件回调,将其push到 listeners 数组中,直到最外层的root节点为止,然后将listeners数组以及合成事件对象组成的对象push到 dispatchQueue队列

  • 事件回调函数执行
    收集到事件的执行路径之后,接下来就是要模拟浏览器原生事件冒泡/捕获,按照一定的顺序执行 dispatchQueue队列 中的回调函数,React收集fiber节点上绑定的回调函数的顺序是从内层到外层的,与事件冒泡的顺序类似,所以要模拟事件冒泡过程就是将 dispatchQueue 中的 listeners 正序遍历执行,模拟事件捕获就是按照相反的顺序,将 dispatchQueue 中的 listeners 倒序遍历执行

    如果某个事件回调函数内部调用了event对象的 stopPropagation() 方法,事件的链路就会中断执行,后面的事件回调将不在被触发

合成事件与原生事件区别

  • 事件名称命名方式不同

原生事件命名为纯小写(onclick, onblur),而 React 事件命名采用小驼峰式(camelCase),如 onClick 等

1
2
3
4
5
// 原生事件绑定方式
<button onclick="handleClick()">button</button>

// React 合成事件绑定方式
const button = <button onClick={handleClick}>button</button>
  • 事件处理函数写法不同

原生事件中事件处理函数为字符串,在 React JSX 语法中,传入一个函数作为事件处理函数

1
2
3
4
5
// 原生事件 事件处理函数写法
<button onclick="handleClick()">button</button>

// React 合成事件 事件处理函数写法
const button = <button onClick={handleClick}>button</button>
  • 阻止默认行为方式不同

在原生事件中,可以通过返回 false 方式来阻止默认行为,但是在 React 中,需要显式使用 preventDefault() 方法来阻止

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 原生事件阻止默认行为方式
<a href="https://www.baidu.com"
onclick="console.log('阻止原生事件~'); return false"
>
button
</a>

// React 事件阻止默认行为方式
const handleClick = e => {
e.preventDefault();
console.log('阻止原生事件~');
}
const clickElement = <a href="https://www.baidu.com" onClick={handleClick}>
button
</a>