文章目录
- React事件处理机制
- 原生事件的事件机制
- 事件代理(事件委托)
- 合成事件
- 使用合成事件目的
- 合成事件原生事件区别
- 事件池
- 原生事件和React事件的执行顺序
- e.stopPropagation()
- React17事件机制的修改
React事件处理机制
react 事件机制基本理解:react 基于浏览器的事件机制,自身实现了一套自己的事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等。
react 的所有事件并没有绑定到具体的dom节点上而是绑定在了 document 上,然后由统一的事件处理程序来处理,同时也是基于浏览器的事件机制(冒泡),所有节点的事件都会在 document 上触发。
React 的事件处理机制可以分为两个阶段:
- 初始化渲染时在 root 节点上注册原生事件;
- 原生事件触发时模拟、目标和冒泡阶段派发合成事件。
通过这种机制,冒泡的原生事件类型最多在 root 节点上注册一次,节省内存开销。且 React 为不同类型的事件定义了不同的处理优先级,从而让用户代码及时响应高优先级的用户交互,提升用户体验。
合成事件是 React 模拟原生 DOM 事件所有能力的一个事件对象,它可以兼容所有浏览器,也拥有和浏览器原生事件相同的接口。
原生事件的事件机制
事件流向分为三个阶段:捕获阶段、目标阶段、冒泡阶段。
捕获阶段是指事件响应从最外层的Window开始逐层向内层推进,直到具体的目标元素。
目标阶段是触发事件的目标元素。
冒泡阶段与捕获阶段相反,事件的响应是从最底层开始一层层向外传递到最外层的Window。
我们可以通过element.addEventListener()设置一个元素的事件模型为冒泡事件还是捕获事件。
element.addEventListener(type, listener, useCapture)
type:监听的事件类型(例’click’)
listener:事件的回调函数
useCapture:默认值为false,表示元素事件模型为事件冒泡,设为true时为事件捕获。
事件代理(事件委托)
事件代理(也称为事件委托)是一种利用DOM事件机制,通过将事件处理程序添加到父元素而不是每个子元素上来提高性能的技术。当子元素上的事件触发时,事件会冒泡/捕获到父元素,然后由父元素上的事件处理程序进行处理。
例对于列表元素,我们希望将用户点击了哪个item打印出来,通常我们可以给每个item注册点击事件监听器,但通过事件代理,可以将多个事件监听器减少为一个,这样可以减少重复工作,也可以减少内存。还可以使新增元素动态绑定事件。
var items = document.getElementById('item-list');
//事件捕获实现事件代理
items.addEventListener('click', (e) => {console.log('捕获:click ',e.target.innerHTML)}, true);
//事件冒泡实现事件代理
items.addEventListener('click', (e) => {console.log('冒泡:click ',e.target.innerHTML)}, false);
事件代理既可以通过事件冒泡来实现,也可以通过事件捕获来实现。
合成事件
React 合成事件(SyntheticEvent)是 React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器。它根据 W3C 规范来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口。
即在react中,我们绑定的事件onClick等,并不是原生事件,而是由原生事件合成的React事件,比如 click 事件合成为 onClick 事件。比如 blur, change, input, keydown, keyup, 合成为 onChange。可以通过 e.nativeEvent 属性获取原生事件。
使用合成事件目的
- 方便事件统一管理
- 进行浏览器兼容,实现更好的跨平台
- 避免垃圾回收:事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或释放事件对象。即 React 事件对象不会被释放掉,而是存放进一个数组中,当事件触发,就从这个数组中弹出,避免频繁地去创建和销毁(垃圾回收) 。
合成事件原生事件区别
命名方式不同:原生事件命名为纯小写,合成事件命名用小驼峰。
事件处理函数写法不同:原生事件中函数为字符串,合成事件里为函数。
阻止默认行为方式不同:原生事件中可以返回 false 来阻止默认行为,合成事件里需要显式调用 e.preventDefault() 来阻止。
事件池
合成事件对象池,是 React 事件系统提供的一种性能优化方式。合成事件对象在事件池统一管理,不同类型的合成事件具有不同的事件池。
React 事件池仅支持在 React16 及更早版本中,在 React17 已经不使用事件池。
SyntheticEvent 对象会被放入池中统一管理。这意味着 SyntheticEvent 对象可以被复用,当所有事件处理函数被调用之后,其所有属性都会被置空。例如,以下代码是无效的:
function handleChange(e) {
// This won't work because the event object gets reused.
setTimeout(() => {
console.log(e.target.value); // Too late!
}, 100);
}
https://zh-hans.legacy.reactjs.org/docs/legacy-event-pooling.html
原生事件和React事件的执行顺序
React 所有事件都挂载在 document 对象上。当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件。
所以会先执行原生事件,然后处理 React 事件,最后真正执行 document 上挂载的事件。
原生事件(阻止冒泡)会阻止合成事件的执行;合成事件(阻止冒泡)不会阻止原生事件的执行。
e.stopPropagation()
e.stopPropagation() 可以阻止合成事件在组件树中的冒泡传播。事件本身还都是在 document 上执行。不能阻止原生事件的冒泡传播。
原生事件中如果执行了 stopPropagation 方法,则会导致其他 React 事件失效。因为所有元素的事件将无法冒泡到document上。
React17事件机制的修改
- 事件统一绑定container上,ReactDOM.render(app, container);而不是document上,这样好处是有利于微前端的,微前端一个前端系统中可能有多个应用,如果继续采取全部绑定在document上,那么可能多应用下会出现问题。比如如果你在一个 React 子应用的 React 事件中调用了 e.stopPropagation(),无法阻止事件冒泡到外部树,因为真实的事件早已传播到 document。
- React17 对齐了浏览器原生标准。比如 onScroll 事件不再进行事件冒泡。onFocus 和 onBlur 使用原生 focusin, focusout 合成。
- React17 取消事件池复用。“它并不会提高现代浏览器的性能,甚至还会使经验丰富的开发者一头雾水” -> React 在旧浏览器中重用了不同事件的事件对象,以提高性能,并将所有事件字段在它们之前设置为 null。在 React 16 及更早版本中,使用者必须调用 e.persist() 才能正确的使用该事件,或者正确读取需要的属性。
https://zh-hans.legacy.reactjs.org/blog/2020/08/10/react-v17-rc.html