1. 什么是合成事件(SyntheticEvent)?
- React自己实现了一套事件,在原生事件基础上做了很大改进,减少了内存消耗,解决了IE等浏览器的兼容问题
- React把事件委托到 document身上(react 17以前),在React17以后再次改进了合成事件,委托对象变成了容器对象
- 当真实dom元素触发事件,先处理原生事件,然后会冒泡到document或容器对象上,再处理react事件
2. 事件捕获,事件冒泡,事件委托
DOM2级事件规定的事件流包含3个阶段,事件捕获阶段、处于目标阶段、事件冒泡阶段
首先发生的事件捕获为截获事件提供机会,然后是实际的目标接收事件,最后一个阶段是事件冒泡阶段,可以在这个阶段对事件做出响应
- 事件捕获: 事件捕获会从外向内进行捕获(addEventListener 第三个参数设置为 true可以让事件在 捕获阶段触发)
- 事件冒泡: 事件冒泡会从内向外冒泡
- 事件委托: 利用事件冒泡,将事件挂载在外层父级上,当触发子级时会向上冒泡触发父级的方法,再通过 e.target 拿到子级对象
3. React16与17版本合成事件的区别
- react16时事件委托的对象是 document,react17时事件委托的对象是容器组件
- react16时原生事件与react事件执行时,冒泡阶段与捕获阶段没有区分开(捕获-> 冒泡 -> 捕获);react17时优化了合成事件的执行,当与原生事件一起调用时,捕获阶段总是先于冒泡阶段(捕获 -> 冒泡)
- react17废弃了事件池
4. React合成事件伪代码分析
首先,构造一段dom结构,再分别打印原生事件和模拟的react事件的触发顺序
<body>
<div id="root">
<div class="parent">
<div class="child">
点击
</div>
</div>
</div>
</body>
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");
}
5. react16合成事件伪代码模拟
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.getElementById('document').addEventListener('click',dispatchEvent);
react16时以上代码的打印顺序是:
原生捕获:parent
原生捕获:child
原生冒泡:child
原生冒泡:parent
react捕获:parent
react捕获:child
react冒泡:child
react冒泡:parent
6. React17合成事件伪代码模拟
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));
react17时以上代码的打印顺序是:
react捕获:parent
react捕获:child
原生捕获:parent
原生捕获:child
原生冒泡:child
原生冒泡:parent
react冒泡:child
react冒泡:parent
7. 结语
React合成事件应用了事件委托,并且在16版本与17版本合成事件有很多差异
实际上,react的合成事件涉及到很多内容,实现非常复杂,这里只是简单模拟,以对合成事件有一些了解