1. react事件绑定在哪?
我们知道,原生dom事件就绑定在对应的元素上,但是这点在react中有所不同:
在react中,假设给一个button绑定了点击事件,会发现事件并没有绑定在button上,button上只绑定了一个空函数,而真正的事件绑定到了document上。
本文先讲述react17之前事件版本,也就是事件绑定在document上。
也有一些事件不冒泡,如input的focus事件,video的onplay,不是在document元素上监听,而是在元素本身上监听
这里做一个小拓展,事件到底被存到了哪里?
我们知道jsx代码会被babel转化成React.createElement的形式,最终被转化成fiber对象,所以事件最终其实被保存到了fiber的memoizedProps和pendingProps中。
2. 合成事件
2.1 概念
合成事件,是react创造的模拟原生dom事件所有能力的一个事件对象。
比如上面给button绑定的点击事件,react中是'onClick',而这并不是原生事件,而是react的合成事件。
又比如说一个好用的合成事件'onChange',其实是很多原生事件的综合,比如blur、change、focus、input等。
2.2 为什么要使用合成事件?
主要有这些原因:
- 增强兼容性: 每个浏览器的内核都不相同,而React通过顶层事件代理机制,保证冒泡的统一性,抹平不同浏览器事件对象之间的差异,将不同平台的事件进行模拟成合成事件,使其能够跨浏览器执行
- 统一管理所有事件: 原生事件系统中,所有事件都被绑定在对应的dom上,如果页面复杂,会产生非常多的绑定事件;而react将所有事件都放在document上,便于统一管理
- 避免垃圾回收: 假设给原生事件系统和react事件系统中的输入框都绑定onChange,那么原生事件绑定onchange对应的就是change,而React会被处理为很多的监听器。在实际中,我们的事件会被频繁的创建和回收,这样会影响其性能,为了解决这个问题,React引入事件池,通过事件池来获取和释放事件。
事件池在react17中被删除
2.3 对比原生事件和合成事件
2.3.1 执行顺序对比
import React, { useEffect, useRef } from "react";
export default function App(props) {
const ref = useRef(null)
const ref1 = useRef(null)
useEffect(() => {
const div = document.querySelector("div")
const button = document.querySelector("button")
div.addEventListener("click", () => console.log("原生:div元素"))
button.addEventListener("click", () => console.log("原生:button元素"))
document.addEventListener("click", () => console.log("原生:document元素"))
}, [])
return (
<div onClick={() => console.log('React:div元素')}>
<button
onClick={() => console.log('React:按钮元素')}
>
执行顺序
</button>
</div>
);
}
可以看出,当dom元素事件被触发后,会先执行原生事件,再处理react事件,最后真正执行document上挂载的事件。
2.3.2 和原生事件的区别?
(1) 事件名不同
原生事件,是以纯小写来命名,如:onclick
合成事件,是以小驼峰式来命名,如:onClick
(2) 接收的参数不同
原生事件接收字符串,如
onClick = 'Click()'
合成事件接收函数,如
onClick = {() => Click()}
(3) 事件源不同
在React中,我们的所有事件都可以说是虚拟的,并不是原生的事件。
我们在React中拿到的事件源(e)也并非是真正的事件e,而是经过React单独处理的e。
(4) 阻止默认事件方式不同
原生事件中,可以通过e.preventDefault()和return false 来阻止默认事件
合成事件中,通过e.preventDefault()阻止默认事件
注意原生事件和合成事件的e.preventDefault()并非是同一个函数,React的事件源e是单独创立的
拓展:合成事件中的阻止冒泡:
- e.stopPropagation() :阻止虚拟dom中合成事件的冒泡,但不会阻止document上的事件
- e.nativeEvent.stopImmediatePropagation() :与上一种相反,只能阻止绑定在document上的事件冒泡
react通过e.nativeEvent可以得到真实dom的对象e
2.3.3 合成事件的冒泡和捕获
在React中,所有的绑定事件(如:onClick、onChange)都是冒泡阶段执行。
所有的捕获阶段统一加Capture,如onClickCapture、onChangeCapture
<button
onClick={() => {console.log('冒泡')}}
onClickCapture={() => {console.log("捕获")}} >
点击
</button>
2.4 两种事件是否可以混用?
先举个例子
import React, { useEffect, useRef } from "react";
export default function App(props) {
useEffect(() => {
const button = document.querySelector("button")
button.addEventListener("click", (e) => {
e.stopPropagation();
console.log("原生button阻止冒泡");
})
document.addEventListener("click", () => {
console.log("原生document元素");
});
}, [])
return (
<button
onClick={() => {
console.log('按钮事件')
}}
>
混用
</button>
);
}
最终只输出“原生button阻止冒泡”。
原因是原生事件会先执行,并且执行了阻止冒泡语句后,button的点击事件不会冒泡到document,最终导致无法触发合成按钮事件。
而反过来,给合成事件阻止冒泡,是无法阻止真实dom事件的执行的。