「React 深入」React事件系统与原生事件系统究竟有何区别?
大家好,我是小杜杜,我们知道React自身提供了一套虚拟的事件系统,这套系统与原生系统到底有何区别?今天一起来详细看看
作为一个前端,我们会有很多的事件处理,所以学好React事件系统是非常有必要的
关于
React事件系统将会分三个方面去讲解,分别是:React事件系统与原生事件系统、深入React v16事件系统、对比Reactv16~v18事件系统 三个模块,有感兴趣的可以关注下新的专栏:React 深入进阶,一起进阶学习~
在正式开始前,让我们一起来看看以下几个问题:
React为什么要单独整一套虚拟的事件系统,这样做有什么好处?React的事件系统,与原生的事件系统有什么区别?- 什么是合成事件?
React中是如何模拟冒泡和捕获阶段的?- 所有的事件真的绑定在真实
DOM上吗?如果不是,那么绑定在哪里? React中的阻止冒泡与原生事件系统的阻止冒泡一样吗,为什么?- ...
如果你能耐心的看完,相信一定能帮你更好的了解事件系统,先附上一张知识图,供大家更好的观看,还请各位小伙伴多多支持~

原生DOM事件
在讲React的事件系统前,我们先复习一下原生DOM事件的概念,来帮助我们更好的理解
注册事件
注册事件:通过给元素添加点击(滚动等)事件,称作注册事件,也叫绑定事件
注册事件共有两种方式,分别是:传统注册方式和监听注册方式
传统注册方式
传统注册方式:是以on开头的方式,完成注册方式
如:
// 第一种
<button onclick="console.log(1)">点击</button>
// 第二种
<button id="btn">点击</button>
const btn = document.querySelector('#btn')
btn.onclick = function () {}
需要注意的是:我们注册的事件都具备唯一性,也就是同一个元素只能设置一个处理的函数,如果有多个,则会进行覆盖
监听注册方式
监听注册方式:是以addEventListener方法来监听元素事件
addEventListener方法并不支持IE 9以下的浏览器,当有需要的时候可以使用attachEvent方法,这个方法支持IE 10以下的浏览器,但此方法建并非标准
如:
<button id="btn">点击</button>
const btn = document.querySelector('#btn')
btn.addEventListener('click', () => {})
addEventListener 与传统的方式不同,支持多次绑定事件,但要比传统方式等级低
事件流
DOM事件流共分为三个阶段:事件捕获、目标和事件冒泡三个阶段
事件捕获:由 DOM 最顶层节点开始,然后逐级向下传播到最具体的元素接收的过程
事件冒泡:事件开始时由最具体的元素接收,然后逐级向上传播到 DOM 最顶层节点的过程
特别注意:
- JS代码中,只能执行
捕获或冒泡其中的一个阶段 addEventListener的第三个参数为false代表冒泡(默认),为true代表捕获- 在真实的情况下,我们更多的关注是在
冒泡上,可以利用冒泡做一些很巧妙的事情,但有时又会带来不必要的麻烦,应该合理的去利用 - 并不是所有的事件都有冒泡,有些事件并不存在冒泡事件,如:
onblur、onfocus、onmouseenter事件等 - 合理的利用
e.stopPropagation()、e.stopImmediatePropagation()来阻止冒泡
扩展:阻止冒泡
这里简单介绍一下e.stopPropagation()和e.stopImmediatePropagation()的区别,方便大家更好的理解,
举个栗子🌰:
<div id="id">
<button id="btn">点击</button>
</div>
const div = document.querySelector('#id')
const btn = document.querySelector('#btn')
document.addEventListener('click', (e) => {
console.log(1)
})
div.addEventListener('click', (e) => {
console.log(2)
})
div.addEventListener('click', (e) => {
console.log(3)
})
div.addEventListener('click', (e) => {
console.log(4)
})
btn.addEventListener('click', () => {
console.log(5)
})
当我们点击按钮的时候,执行顺序是:5 > 2 > 3 > 4 > 1,原因是执行了冒泡,会从最底层的btn开始执行,然后是div,最后才是顶层document, 然后根据Js的执行顺序,分别是 2、3 、4
那么我们在console.log(3)上加入e.stopPropagation()看看,结果是什么?

可以看到执行顺序变成了:5 > 2 > 3 > 4
同样的,我们换成e.stopImmediatePropagation()来看看结果:
此时结果变成了:5 > 2 > 3
结论:e.stopImmediatePropagation() 相当于e.stopPropagation()的增强版,不但可以阻止向上的冒泡,还能够阻止同级的扩散
扩展:获取事件流的阶段
我们如果想知道触发事件的元素属于三个阶段的哪个阶段时,可以通过e.eventPhase来获取
当e.eventPhase为1时,代表捕获阶段,为2时,代表目标阶段,为3时,代表冒泡阶段
事件委托
事件委托:也称事件代理,在JQ中称事件委派,也就是利用事件冒泡,将子级的事件委托给父级加载
也就是说,我们可以通过将监听节点设置在父级上,然后利用冒泡来影响子集,如:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
const ul = document.querySelector('ul')
const lis = ul.children
ul.addEventListener('click', (e) => {
for(let i = 0; i < lis.length; i++) {
lis[i].style.background = ""
}
e.target.style.background = "red"
})
我们点击对应的节点,点击的节点需要高亮,如果设置在子级上,就需要监听所有子级节点,就非常麻烦,此时我们可以监听父级,来实现效果

初探React事件系统
点击事件究竟去了哪?
我们先来看看原生DOM事件,用传统/监听注册方式下,事件绑定在何处?

可以看出,原生DOM事件就绑定在对应的button上,那么React中,也是如此吗?

我们发现在React中,button 这个元素并没有绑定事件,并且对应的点击事件中有button和document两个事件
先来看看button事件,绑定的方法为nonp

然而绑定的 nonp只是一个空函数,也就说,真正的事件绑定到了document上
点击事件究竟存储到了哪?
之前在「React深入」一文吃透虚拟DOM和diff算法中讲过,我们编写的jsx代码首先会被babel转化为React.createElement,最终被转化为fiber对象
接下来我们逐步看看上述的代码转化后的样子:
React.createElement形式:

fiber对象形式(可以选中当前元素,然后输入console.dir($0)查看当前元素):

可以发现,事件最终保存在fiber中的memoizedProps 和 pendingProps中
什么是合成事件?
合成事件(SyntheticEvent):是React模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器
简单的来讲,我们在上述的例子中,使用onClick点击事件,而onClick并不是原生事件,而是由原生事件合成的React事件
为了更好的理解,我们来看看input的onChang事件
export default function App(props) {
return (
<div>
<input onChange={(e) => console.log(e)}></input>
</div>
);
}
此时在doncument上的事件为

也就是说,我们绑定的onChange事件最终被处理成了很多事件的监听器,如:blur、change、focus、input等
为什么要单独整一套呢?
React为什么要采取事件合成的模式呢?这样做有什么好处呢?
兼容性,跨平台
我们知道有多种浏览器,每个浏览器的内核都不相同,而React通过顶层事件代理机制,保证冒泡的统一性,抹平不同浏览器事件对象之间的差异,将不同平台的事件进行模拟成合成事件,使其能够跨浏览器执行
将所有事件统一管理
在原生事件中,所有的事件都绑定在对应的Dom上,如果页面复杂,绑定的事件会非常多,这样就会造成一些不可控的问题
而React将所有的事件都放在了document上,这样就可以对事件进行统一管理,从而避免一些不必要的麻烦
避免垃圾回收
我们来看看React和原生事件中的input都绑定onChange事件是什么样子?

可以看出,原生事件绑定onchange对应的就是change,而React会被处理为很多的监听器
在实际中,我们的事件会被频繁的创建和回收,这样会影响其性能,为了解决这个问题,React引入事件池,通过事件池来获取和释放事件。
也就是说,所有的事件并不会被释放,而是存入到一个数组中,如果这个事件触发,则直接在这个数组中弹出即可,这样就避免了频繁创建和销毁
浅谈合成事件与原生事件
执行顺序对比
我们先模拟下React中的合成事件和原生中的事件顺序,如:
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(button)元素触发后,会先执行原生事件,再处理React时间,最后真正执行document上挂载的事件

与原生事件有何不同?
事件名不同
- 原生事件,是以
纯小写来命名,如:onclick - 合成事件,是以
小驼峰式来命名,如:onClick

接受的参数不同
- 原生事件,接受的参数是字符串,如:
Click() - 合成事件,接受的参数是函数,如:
Click()
事件源不同,阻止默认事件的方式不同
在React中,我们的所有事件都可以说是虚拟的,并不是原生的事件
我们在React中拿到的事件源(e) 也并非是真正的事件e,而是经过React 单独处理的e
- 原生事件中,可以通过
e.preventDefault()和return false来阻止默认事件 - 合成事件中,通过
e.preventDefault()阻止默认事件
特别注意,原生事件和合成事件的
e.preventDefault()并非是同一个函数,React的事件源e是单独创立的,所以两者的方法也不相同,同时return false也在React中无效
扩展:对比 e.stopPropagation() 和 e.nativeEvent.stopImmediatePropagation
来扩展下阻止冒泡的方法:
为了更好的说明,我们分别使用e.stopPropagation() 和 e.nativeEvent.stopImmediatePropagation有什么效果:
正常触发:

e.stopPropagation()触发:

e.nativeEvent.stopImmediatePropagation触发:

从上图可知:
- e.stopPropagation():可以阻止当前
DOM事件的冒泡,但事实上,e.stopPropagation()只能阻止合成事件的冒泡,即不会阻止顶流document上 - e.nativeEvent.stopImmediatePropagation:于
e.stopPropagation()正好相反,只能阻止绑定在document上的监听事件
扩展:冒泡和捕获阶段
在React中,所有的绑定事件(如:onClick、onChange)都是冒泡阶段执行。
所有的捕获阶段统一加Capture,如onClickCapture、onChangeCapture
举个小例子:
<button
onClick={() => {console.log('冒泡')}}
onClickCapture={() => {console.log("捕获")}} >
点击
</button>
是否可以混用?
我们通过对比原生事件和合成事件后,提出一个疑问,原生事件和合成事件是否可以一起使用?
先来举个栗子🌰,一起看看
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>
);
}
结果:

可以发现只执行了原生事件,并没有执行合成事件,这是因为原生事件的执行顺序在合成事件之前,所以导致合成事件没有办法进行触发。
所以两者建议不要进行混用,否则会跳过React的事件机制
End
参考
相关文章
- 「React深入」一文吃透React v16事件系统
- 「React 深入」畅聊React 事件系统(v17、v18版本)
- 花一个小时,迅速掌握Jest的全部知识点~
- 「React 深入」一文玩转React Hooks的单元测试
结语
本文通过对比React 事件系统和原生事件系统,详细的了解两者的区别,实际上React上的事件都绑定在了document上,就连事件源也并非是原生中的事件源
那么,React究竟如何绑定事件的,又是如何触发事件的?为什么我们必须通过this去绑定对应的事件?又是如何处理批量更新的?... 都是一些我们值得探讨的问题。
感兴趣的可以关注下这个专栏,这个专栏会以进阶为目的,详细讲解React相关的原理、源码、实战,有感兴趣的可以关注下,一起学习,一起进步~
转载自:https://juejin.cn/post/7158786701249216519