💡 React18合成事件的处理原理
“绝对不是”给当前元素基于addEventListener做的事件绑定,React中的合成事件,都是基于“事件委托”处理的!
- 在React17及以后版本,都是委托给#root这个容器(捕获和冒泡都做了委托)
- 在React17以前,都是委托给document容器的(而且只做了冒泡阶段的委托)
- 对于没有实现事件传播机制的事件,才是单独做的事件绑定(例如,onMouseEnter/onMouseLeave...)
在组件渲染的时候,如果发现JSX元素中有onXxx/OnXxxCapture这样的属性,不会给当前元素直接做事件绑定,只是把绑定的方法赋值给元素的相关属性!
- outer.onClick={() => {console.log("outer 冒泡(合成)");}} //这不是DOM0级事件绑定(这样的才是outer.onclick(小写))
- outer.onClickCapture={() => {console.log("outer 捕获(合成)");}}
- inner.onClick={() => {console.log("inner 冒泡(合成)");}}
- inner.onClickCapture={() => {console.log("inner 捕获(合成)");}}
主要对#root这个容器做了事件绑定(捕获和冒泡都做了)
原因:因为组件中所渲染的内容,最后都会插到#root容器中,这样点击页面中任何一个元素,最后都会把#root的点击行为触发!!而在给#root绑定的方法中,把之前给元素设置的onXxx/onXxxCapture属性,在相应的阶段执行!!
💡 React18合成事件代码示例
我们给React添加对应的onClick,onClickCapture事件,在componentDidMount()周期函数(第一次渲染完毕)中添加原生事件绑定如下:猜猜打印的结果是什么
//React合成事件原理
import React from "react";
class Demosy extends React.Component {
render() {
return (
<div
className="outer"
style={{
width: 200,
height: 200,
background: "lightgreen",
}}
onClick={() => {
console.log("outer 冒泡(合成)");
}}
onClickCapture={() => {
console.log("outer 捕获(合成)");
}}
>
<div
className="inner"
style={{
width: 100,
height: 100,
background: "lightcoral",
}}
onClick={() => {
console.log("inner 冒泡(合成)");
}}
onClickCapture={() => {
console.log("inner 捕获(合成)");
}}
>
888888
</div>
</div>
);
}
componentDidMount() {
//组件渲染完
document.addEventListener(
"click",
() => {
console.log("document捕获");
},
true
);
document.addEventListener(
"click",
() => {
console.log("document冒泡");
},
false
);
document.body.addEventListener(
"click",
() => {
console.log("body捕获");
},
true
);
document.body.addEventListener(
"click",
() => {
console.log("body冒泡");
},
false
);
let root = document.querySelector("#root");
root.addEventListener(
"click",
() => {
console.log("root捕获");
},
true
);
root.addEventListener(
"click",
() => {
console.log("root冒泡");
},
false
);
let outer = document.querySelector(".outer");
outer.addEventListener(
"click",
() => {
console.log("outer捕获(原生)");
},
true
);
outer.addEventListener(
"click",
() => {
console.log("outer冒泡(原生)");
},
false
);
let inner = document.querySelector(".inner");
inner.addEventListener(
"click",
() => {
console.log("inner捕获(原生)");
},
true
);
inner.addEventListener(
"click",
() => {
console.log("inner冒泡(原生)");
},
false
);
}
}
export default Demosy;
运行 代码,点击inner触发事件,打印结果如下
看到这结果是不是跟你想象的不一样,想知道是什么原理嘛,接下来我们来自己简单的实现一下React18的合成事件原理,我们建立一个index.html,代码如下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>合成事件的原理</title>
<style>
* {
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
overflow: hidden;
}
.center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
#root {
width: 300px;
height: 300px;
background: lightblue;
}
#outer {
width: 200px;
height: 200px;
background: lightgreen;
}
#inner {
width: 100px;
height: 100px;
background: lightcoral;
}
</style>
</head>
<body>
<div id="root" class="center">
<div id="outer" class="center">
<div id="inner" class="center"></div>
</div>
</div>
<script>
const root = document.querySelector("#root"),
outer = document.querySelector("#outer"),
inner = document.querySelector("#inner");
//经过视图渲染解析,outer/inner上 都有onXxx/onXxxCapture这样的属性
/* <div
className="outer"
onClick={() => {console.log("outer 冒泡");}}
onClickCapture={() => {console.log("outer 捕获");}}
>
<div
className="inner"
onClick={() => {console.log("inner 冒泡");}}
onClickCapture={() => {console.log("inner 捕获");}}
>
</div>
</div> */
outer.onClick = () => {
console.log("outer 冒泡(合成)");
};
outer.onClickCapture = () => {
console.log("outer 捕获(合成)");
};
inner.onClick = () => {
console.log("inner 冒泡(合成)");
};
inner.onClickCapture = () => {
console.log("inner 捕获(合成)");
};
//给#root做事件绑定(源码)
//捕获
root.addEventListener(
"click",
(ev) => {
let path = ev.composedPath(); //path:[事件源-》...-》window] 所有祖先元素
//深拷贝 捕获阶段倒叙循环
[...path].reverse().forEach((ele) => {
let handle = ele.onClickCapture; //handle是一个函数
if (handle) handle();
});
},
true
);
//冒泡
root.addEventListener(
"click",
(ev) => {
let path = ev.composedPath(); //path:[事件源-》...-》window] 所有祖先元素
// 冒泡不需要倒叙
path.forEach((ele) => {
let handle = ele.onClick;
if (handle) handle(); //handle是一个函数
});
},
false
);
</script>
</body>
</html>
运行代码结果图如下
我们可以通过ev.composedPath()拿到我们需要的对应路径,如上图路径是(事件源->....->window),我们通过事件传播机制知道,拿到我们需要的路径在捕获阶段把它倒叙循环,拿到它onClickCapture的属性(是一个函数),如果存在就执行它,冒泡阶段则不需要倒叙,拿到它onClick的属性(是一个函数),如果存在同样就执行它,此时得到的结果如上图所示
我们来画图深入了解一下
大概了解了一下如何原生实现React18原理,回过头我们在看看上面的题目,再通过画图来过一遍
对比打印结果,是不是对我们React18的合成事件原理秒懂
💡 React18合成事件代码示例
在React16版本中,合成事件的处理机制,不再是把事件委托给#root元素了,而是委托给document元素,并且只做了冒泡阶段的委托,在委托的方法中,把onXxx/onXxxCapture合成事件属性进行执行!
React16中,关于合成事件对象的处理,React内部是基于“事件对象池”,做了一个缓存机制!! React17及有以后,是去掉了这套事件对象池和缓存机制的!
当每一次事件触发的时候,如果传播到了委托的元素上(documnet),在委托的方法中,我们首先会对内置事件对象做统一处理,生成合成事件对象!!
在React16版本中为了防止每一次都是重新创建出新的合成事件对象,它设置了一个事件对象池(缓存池)
-
- 等待本次操作结束,把合成事件对象中的成员信息都清空掉,再放入到事件对象池中
- 本次事件触发,获取到事件操作的相关信息后,我们从事件对象池中获取存储的合成事件对象,把信息赋值给相关的成员
- (特殊使用ev.persist() 就能把合成事件对象中的信息保存下来)
在React18中 并没有事件对象池机制,所以也不存在,创建的事件对象信息清空问题
补充知识点:
ev.stopPropagation(); //合成事件中的“阻止事件传播”:阻止原生的事件转播&&阻止合成事件中的事件传播
ev.nativeEvent.stopPropagation(); //原生事件对象中的“阻止事件传播”:只能阻止原生的事件转播
ev.nativeEvent.stopImmediatePropagation(); //原生事件对象中的“阻止事件传播”:只能阻止原生的事件转播 也能阻止同级#root的冒泡(其它绑定方法执行)
React16合成事件原理图
我们来简单的画个流程图深刻了解一下React16的合成事件原理
代码执行结果如下: