React的合成事件
React 16/18合成事件
合成事件对象SyntheticBaseEvent
基于React内部的处理 如果我们给合成事件绑定一个普通函数 当事件触发 绑定的函数执行 方法中的this会是undefined(这样是不好的)
- 解决方案 是将this -> 实例
- 我们可以基于JS中的bind方法:预先处理函数中的this和实参
- 当然更推荐的方法是:将绑定的函数设置为”箭头函数“,让其使用上下文中的this(也就是所说的实例)
合成事件对象SyntheticBaseEvent:React内部经过特殊处理 把各个浏览器的事件对象统一化后 的一个事件对象
我们在React合成事件对象触发的时候 我们也可以获取到事件对象 只不过此对象是合成事件对象
合成事件对象中 也包含了浏览器内置事件对象中的一些属性和方法(常用的基本都有)例如clientX/clientY pageX/pageY target type preventDefault stopPropagation…
nativeEvent:这个属性可以获取浏览器内置(原生)的事件对象
注意:经过bind处理改变this指向 不管有没有预传参数 最后一个实参都是合成对象
事件委托机制
我们先来复习一下事件委托
事件委托:利用事件的传播机制 实现一套事件绑定处理方案
例如:一个容器中 有很多元素都要在点击的时候做一些事情
- 传统方案:首先获取需要操作的元素 然后逐一做事件绑定
- 事件委托:只需要给容器做一个事件绑定(点击内部的任何元素 根据事件的冒泡传播机制 都会让容器的点击事件也触发 我们在这里 根据事件源 做不同的事情就可以了)
优势: - 提高JS代码运行的性能 并且把处理的逻辑都集中在一起
- 某些需求必须基于事件委托处理 例如:动态增加的元素也需要有点击
- 给动态绑定的元素做事件绑定
限制: - 当前操作的事件必须支持冒泡传播机制才可以
- 例如:mouseenter/mouseleave等事件是没有冒泡传播机制的
- 如果单独做的事件绑定 做了事件传播机制的阻止 呢么事件委托中的操作也不会生效
事件具备传播机制
第一步:从最外层向最里层逐一查找 (捕获阶段:分析出路径)
第二部:把事件源(点击的这个元素) 的点击行为触发(目标阶段)
第三步:按照捕获阶段分析出来的路径 从里到外 把每一个元素的点击行为触发(冒泡阶段)
事件和事件绑定
- 事件是浏览器赋予元素的默认行为
- 事件绑定给这个行为绑定一个方法
即使我们没有给body的点击事件绑定方法 当我们点击body的时候 其点击行为也会被触发 只不过啥事都不做而已
e.stopPropagation 阻止事件的传播 (包含捕获与冒泡)
e.stopImmediatePropagation() 阻止事件传播 只不过它可以把当前元素绑定的其他方法(同级也会阻止) 如果还未执行 也不会在执行了
合成事件底层实现原理
所谓合成事件绑定 其实并没有给元素自身做事件绑定 而是给元素设置 onXxx/onXxxCapture这样的合成事件属性
当事件行为触发 根据原生事件传播的机制 都会传播到#root容器上 React内部给#root容器做了事件绑定(捕获与冒泡)
当React内部绑定的方法执行的时候 会依据ev.composedPath()中分析的路径 依次把对应阶段的onXxx/onXxxCapture 等事件合成属性触发执行
=>合成事件是利用事件委托(事件传播机制完成的)
因为最后执行的handle方法 从此可以知道为什么 如果不经过处理 方法中的this是undefined (如果绑定的方法是箭头函数 则找函数上级上下文中的this)
<!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>合成事件原理</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.onClick = () => {
console.log("outer 冒泡 合成");
};
outer.onClickCapture = () => {
console.log("outer 捕获 合成");
};
inner.onClick = () => {
console.log("inner 冒泡 合成");
};
inner.onClickCapture = () => {
console.log("inner 捕获 合成");
};
//给#root做事件绑定 捕获 冒泡 #root上绑定的方法执行把所有规划的路径中 有合成事件属性的都执行即可
root.addEventListener(
"click",
(ev) => {
let path = ev.composedPath(); //path: [事件源-> ... -> window] 所有祖先元素
[...path].reverse().forEach((ele) => {
let handle = ele.onClickCapture; // 获得祖先元素的onClickCapture
if (handle) {
handle();
}
});
},
true
);
root.addEventListener(
"click",
(ev) => {
let path = ev.composedPath(); //path: [事件源-> ... -> window] 所有祖先元素
path.forEach((ele) => {
let handle = ele.onClick; // 获得祖先元素的onClick
if (handle) {
//这里把绑定的合成事件方法执行 如果不经过处理 方法中的this是undefined (如果绑定的方法是箭头函数 则找函数上级上下文中的this)
//在执行这些方法之前 把原生的事件对象ev做特殊处理 返回合成事件对象
handle();
}
});
},
false
);
</script>
</body>
</html>
React合成事件
React中合成事件的处理原理
- ”绝对不是“给当前元素基于addEventListener单独做的事件绑定 React的合成事件 都是基于事件委托处理的
- 在React17及以后版本 都是委托给#root这个容器(捕获和冒泡都做了委托 并捕获与冒泡分开)
- 但是在16版本 都是委托给document容器的(而且只做了冒泡阶段的委托 捕获与冒泡没分开)
- 在16版本 关于合成事件对象的处理 React内部是基于”事件对象池“ 做了一个缓存机制 React17之后是 去掉了这套事件对象缓存池和缓存机制 当每一次事件触发的时候 如果传播到了委托的元素上(document/#root) 在委托的方法中 我们首先会对内置事件对象做统一处理 生成合成事件对象
- 在React16版本中 为了防止每一次都是重新创建出新的合成事件对象 它设置了一个事件对象池(缓存池) 本次事件触发 获取到事件操作的相关信息后 我们从事件对象池中 获取存储的合成事件对象 把信息赋值给相关的成员 等待本次操作结束 把合成事件对象中的成员信息都清空掉 在放到事件对象池中 容易出现创建的对象信息清空的问题(如设置5秒后在用这些信息 会获取不到) 使用ev.persist() 可以把合成事件中的信息保留下来
- 对于没有实现事件传播机制的事件 才是单独做的事件绑定(例如: onMouseEnter/onMouseLeave…)
在组件渲染的时候 如果发现jsx元素属性中有 onXxxx/onXxxxCapture这样的属性 不会给当前元素直接做事件绑定 只是把绑定的方法
赋值给元素的相关属性!!
例如:
- outer.onClick = () => {console.log(‘outer 冒泡 合成’)} //着不是DOM0级事件绑定(这样的才是 outer.onclick)
- outer.onClickCapture = () => {console.log(‘outer 捕获 合成’)}
然后对#root这个容器做了事件绑定(捕获和冒泡都做了)
原因:因为组件中所渲染的内容 最后都会插入到#root容器中 这样点击页面中任何一个元素 最后都会把#root的点击行为触发
而在给#root绑定的方法中 把之前给元素设置的onXxxx/onXxxCapture属性 在相应的阶段执行!!!
React17及以后的
React16