1、React合成事件是什么?
React 合成事件(SyntheticEvent)是 React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器。它根据 W3C 规范 来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口。
2、为什么会有合成事件?
- 将事件绑定在
document - v16
/容器元素 - v17
统一管理,防止很多事件直接绑定在原生的dom
元素上。造成一些不可控的情况
-
React
想实现一个全浏览器的框架, 为了实现这种目标就需要提供全浏览器一致性的事件系统,以此抹平不同浏览器的差异。
3、深入合成事件
3.1、事件工作流回顾
3.1.1、事件流
DOM2 Events 规范规定事件流分为 3 个阶段:事件捕获、到达目标和事件冒泡。事件捕获最先发生,为提前拦截事件提供了可能。然后,实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。以前面那个简单的 HTML 为例,点击
元素会以如图 17-3 所示的顺序触发事件。
在 DOM 事件流中,实际的目标(
元素)在捕获阶段不会接收到事件。这是因为捕获阶段从
document 到再到就结束了。下一阶段,即会在
元素上触发事件的“到达目标”
阶段,通常在事件处理时被认为是冒泡阶段的一部分。然后,冒泡阶段开始,事件反向传播至文档。
3.1.2、事件委托
因为事件处理程序在现代 Web 应用中可以实现交互,所以很多开发者会错误地在页面中大量使用它们。在创建 GUI 的语言如 C#中,通常会给 GUI 上的每个按钮设置一个 onclick 事件处理程序。这样做不会有什么性能损耗。在 JavaScript 中,页面中事件处理程序的数量与页面整体性能直接相关。原因有很多。首先,每个函数都是对象,都占用内存空间,对象越多,性能越差。其次,为指定事件处理程序所需访问 DOM 的次数会先期造成整个页面交互的延迟。只要在使用事件处理程序时多注意一些方法,就可以改善页面性能。
“过多事件处理程序”的解决方案是使用事件委托。事件委托利用事件冒泡,可以只使用一个事件
处理程序来管理一种类型的事件。
3.2、事件差异
3.2.1、合成事件与原生事件的差异
-
语法不同
-
阻止默认行为方式不同
- 原声可以通过
return false
/ e.preventDefault()
阻止默认行为
- 合成事件需要使用
e.preventDefault()
,return false
无效 (addEventListener)
3.2.2、React16和17合成事件的差异
- React16时事件委托的对象是 document,React17时事件委托的对象是容器组件
- 这样做是有利于微前端的,微前端一个前端系统中可能有多个应用,如果继续采取全部绑定在
document
上,那么可能多应用下会出现问题。
- React16时原生事件与React事件执行时,冒泡阶段与捕获阶段没有区分开(原声捕获-> 原声冒泡 -> React捕获 -> React冒泡);React17时优化了合成事件的执行,当与原生事件一起调用时,捕获阶段总是先于冒泡阶段(React捕获 ->原声捕获 -> 原声冒泡 -> React冒泡)
- react17废弃了事件池
3.3、简单实现合成事件
3.3.1、React17以前
-
简单模拟
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div class="root">
<div class="parent">
<div class="child">
点击
</div>
</div>
</div>
</body>
</html>
<script>
document.getElementsByClassName('parent')[0].addEventListener('click',()=>{
console.log("原生事件捕获:parent")
},true)
document.getElementsByClassName('child')[0].addEventListener('click',()=>{
console.log("原生事件捕获:child")
},true)
document.getElementsByClassName('parent')[0].addEventListener('click',()=>{
console.log("原生事件冒泡:parent")
})
document.getElementsByClassName('child')[0].addEventListener('click',()=>{
console.log("原生事件冒泡:child")
})
document.getElementsByClassName('parent')[0].onClick = function(){
console.log("react合成事件冒泡:parent")
}
document.getElementsByClassName('child')[0].onClick = function(){
console.log("react合成事件冒泡:child");
}
document.getElementsByClassName('parent')[0].onClickCapture = function(){
console.log("react合成事件捕获:parent")
}
document.getElementsByClassName('child')[0].onClickCapture = function(){
console.log("react合成事件捕获:child");
}
const dispatchEvent = (e)=>{
let paths = []
let current = e.target;
while(current){
paths.push(current);
current = current.parentNode;
}
// 模拟捕获与冒泡
for(let i= paths.length - 1;i>=0;i--){
let handler = paths[i].onClickCapture;
handler && handler();
}
for (var i = 0; i < paths.length; i++) {
let handler = paths[i].onClick;
handler && handler();
}
}
// 绑定合成事件
document.addEventListener('click',dispatchEvent);
</script>
-
效果
3.3.2、React17以后
-
简单模拟
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="root">
<div class="parent">
<div class="child">
点击
</div>
</div>
</div>
</body>
</html>
<script>
document.getElementsByClassName('parent')[0].addEventListener('click',()=>{
console.log("原生事件捕获:parent")
},true)
document.getElementsByClassName('child')[0].addEventListener('click',()=>{
console.log("原生事件捕获:child")
},true)
document.getElementsByClassName('parent')[0].addEventListener('click',()=>{
console.log("原生事件冒泡:parent")
})
document.getElementsByClassName('child')[0].addEventListener('click',()=>{
console.log("原生事件冒泡:child")
})
document.getElementsByClassName('parent')[0].onClick = function(){
console.log("react合成事件冒泡:parent")
}
document.getElementsByClassName('child')[0].onClick = function(){
console.log("react合成事件冒泡:child");
}
document.getElementsByClassName('parent')[0].onClickCapture = function(){
console.log("react合成事件捕获:parent")
}
document.getElementsByClassName('child')[0].onClickCapture = function(){
console.log("react合成事件捕获:child");
}
const dispatchEvent = (e,useCapture)=>{
let paths = []
let current = e.target;
while(current){
paths.push(current);
current = current.parentNode;
}
// 模拟捕获与冒泡
if(useCapture){
for(let i= paths.length - 1;i>=0;i--){
let handler = paths[i].onClickCapture;
handler && handler();
}
}else{
for (var i = 0; i < paths.length; i++) {
let handler = paths[i].onClick;
handler && handler();
}
}
}
// 绑定合成事件
document.getElementById('root').addEventListener('click',(e)=>dispatchEvent(e,true),true);
document.getElementById('root').addEventListener('click',(e)=>dispatchEvent(e,false));
</script>
-
效果
4、结语
- 实际上React合成事件涉及到很多内容,实现非常复杂,我们这里只是简单实现了下。
- 参考链接
- https://juejin.cn/post/6955636911214067720#heading-26
- https://www.bilibili.com/video/BV1tK4y1R7Kt/?spm_id_from=333.999.0.0
- 参考书籍