「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