谈谈react的合成事件与JS事件传播机制
JS事件传播机制
事件传播机制分为三个阶段
以下以click
事件为例。
1. 捕获阶段
当我们触发目标节点
的点击事件时,会从最外层元素向里层元素逐一查找,分析出路径来,此阶段称为捕获阶段
。
2. 目标阶段
目标节点
的点击行为事件触发,此阶段称为目标阶段
3. 冒泡阶段
按照捕获阶段分析出来的路径,从里到外,把每个元素的点击行为事件逐一触发,此阶段称为冒泡阶段
。
图示:
事件委托
事件委托是利用事件的传播机制,实现的一套事件绑定处理方案。
例如:一个容器中有很多元素,都要在点击的时候做一些事情。
- 传统方案:获取需要操作的元素,然后逐一做事件绑定。
- 事件委托:只需要给容器做一个时间绑定「点击内部的任何元素,根据事件的冒泡传播机制,都会让容器的点击事件也触发;然后再根据事件源,做不同的事情就可以了。」
const body = document.body
body.addEventListener('click',(e)=>{
let target = ev.target,
id = target.id
if(id==='wrapper'){
// do something,when wrapper ele click
return;
}
if(id==='outer'){
// do something,when outer ele click
return;
}
if(id==='inner'){
// do something,when inner ele click
return;
}
})
事件委托的优势:
- 提高js代码的运行的性能,并且把处理的逻辑集中在了一起;
- 给动态绑定的元素做事件绑定;
- 某个元素下,存在多个元素有同样的操作。
限制:
- 操作的事件必须支持冒泡传播机制才可以使用事件委托,如:mouseenter、mouseleave 等事件没有冒泡传播机制。
- 若是单独的事件绑定,做了时间传播机制的阻止,那么事件委托中的操作也不会生效!!
react的合成事件
合成事件是围绕浏览器原生事件,充当跨浏览器包装器的对象;它们将不同浏览器的行为合并为一个 API,这样做是为了确保事件在不同浏览器中显示一致的属性!
在react中事件的处理并不是通过对当前元素基于addEventLinstener
进行绑定的,而是通过事件委托处理的
。
基本语法
在JSX元素上,直接基于 onXxx={函数}
进行事件绑定!
浏览器标准事件,在React中大部分都支持。
import React, { Component } from 'react';
export default class App extends Component {
state = {
num: 0,
};
render() {
let { num } = this.state;
return (
<div>
{num}
<br />
<button
onClick={(ev) => {
// 合成事件对象 :SyntheticBaseEvent
console.log(ev);
this.setState({
num: num + 1,
});
}}
>
处理
</button>
</div>
);
}
}
在类组件中需要注意的点:合成事件中的this和传参处理
在类组件中,我们要时刻保证,合成事件绑定的函数中,里面的this是当前类的实例! 也需要保障传参的正常!
import React, { Component } from 'react';
export default class App extends Component {
handler1 = (ev) => {
console.log(ev, this);
};
handler2 = (x, y, ev) => {
console.log(x, y, ev, this);
};
render() {
return (
<div>
<button onClick={this.handler1}>测试1</button>
<button onClick={this.handler2.bind(null, 10, 20)}>测试2</button>
</div>
);
}
}
合成事件的底层机制
总原则:基于事件委托实现。
在react中事件的处理并不是通过对当前元素基于addEventLinstener
进行绑定的,而是通过事件委托处理的
。
react 17及以后,都是委托给#root
容器,捕获和冒泡阶段都做了事件委托。
- 在组件渲染的时候,如果发现JSX元素中有onXxx/onXxxCapture 这样的属性,不会给当前元素直接做事件绑定,只是把绑定的方法赋给元素的相关属性!!
- 然后给
#root
这个容器做了事件绑定【捕获和冒泡都做了】。
1. 因为组件中所渲染的内容,最后都会插入到#root 容器中,这样点击页面中任何一个元素,最后都会把 #root的电机行为触发。
2. 而在给#root绑定的方法中,把之前给元素设置的onXxx/onXxxCapture 属性,在相应的阶段执行。
合成事件原理代码:
const root = document.querySelector("#root"),
outer = document.querySelector(".outer"),
inner = document.querySelector(".inner");
/* 原理 */
const dispatchEvent = function dispatchEvent(ev, isCapture) {
let path = ev.path,
target = ev.target;
if (isCapture) {
[...path].reverse().forEach((elem) => {
let handler = elem.onClickCapture;
if (typeof handler === "function") handler(ev);
});
return;
}
path.forEach((elem) => {
let handler = elem.onClick;
if (typeof handler === "function") handler(ev);
});
};
// 冒泡阶段的委托
root.addEventListener(
"click",
function (ev) {
dispatchEvent(ev, false);
},
false
);
// 捕获阶段的委托
root.addEventListener(
"click",
function (ev) {
dispatchEvent(ev, true);
},
true
);
例子展示
代码示例:
window.addEventListener("click", () => {
console.log("window 冒泡阶段 原生");
});
window.addEventListener(
"click",
() => {
console.log("window 捕获阶段 原生");
},
true
);
document.getElementsByTagName("html")[0].addEventListener("click", () => {
console.log("html 冒泡阶段 原生");
});
document.getElementsByTagName("html")[0].addEventListener(
"click",
() => {
console.log("html 捕获阶段 原生");
},
true
);
export default function HomePage() {
useEffect(() => {
document.getElementById("outer")?.addEventListener("click", () => {
console.log("outer 冒泡阶段 原生");
});
document.getElementById("outer")?.addEventListener(
"click",
() => {
console.log("outer 捕获阶段 原生");
},
true
);
}, []);
return (
<div
className="wrapper"
onClick={() => {
console.log("wrapper 冒泡阶段");
}}
onClickCapture={() => {
console.log("wrapper 捕获阶段");
}}
>
<div
className="outer"
id="outer"
>
<div
className="inner"
onClick={() => {
console.log("inner 冒泡阶段");
}}
onClickCapture={() => {
console.log("inner 捕获阶段");
}}
>
目标元素
</div>
</div>
</div>
);
}
结果输出:
react 16及以前
在16版本-中,合成事件的处理机制,是把事件委托给document元素,并且只做了冒泡阶段的委托;在委托的方法中,把onXxx/onXxxCapture
合成事件属性进行执行。
合成事件的原理代码:
const outer = document.querySelector(".outer"),
inner = document.querySelector(".inner");
/* 原理 */
const dispatchEvent = function dispatchEvent(ev) {
let path = ev.path,
target = ev.target;
[...path].reverse().forEach((elem) => {
let handler = elem.onClickCapture;
if (typeof handler === "function") handler(ev);
});
path.forEach((elem) => {
let handler = elem.onClick;
if (typeof handler === "function") handler(ev);
});
};
// 委托
document.addEventListener(
"click",
function (ev) {
dispatchEvent(ev);
},
false
);
示例代码
window.addEventListener("click", () => {
console.log("window 冒泡阶段 原生");
});
window.addEventListener(
"click",
() => {
console.log("window 捕获阶段 原生");
},
true
);
document.getElementsByTagName("html")[0].addEventListener("click", () => {
console.log("html 冒泡阶段 原生");
});
document.getElementsByTagName("html")[0].addEventListener(
"click",
() => {
console.log("html 捕获阶段 原生");
},
true
);
function IndexPage() {
useEffect(() => {
document.getElementById("outer")?.addEventListener("click", () => {
console.log("outer 冒泡阶段 原生");
});
document.getElementById("outer")?.addEventListener(
"click",
() => {
console.log("outer 捕获阶段 原生");
},
true
);
}, []);
return (
<div
className="wrapper"
onClick={() => {
console.log("wrapper 冒泡阶段");
}}
onClickCapture={() => {
console.log("wrapper 捕获阶段");
}}
>
<div className="outer" id="outer">
<div
className="inner"
onClick={() => {
console.log("inner 冒泡阶段");
}}
onClickCapture={() => {
console.log("inner 捕获阶段");
}}
>
目标元素
</div>
</div>
</div>
);
}
输出结果:
合成事件对象
合成事件对象SyntheticBaseEvent:我们在React合成事件触发的时候,也可以获取到事件对象,只不过此对象是合成事件对象「React内部经过特殊处理,把各个浏览器的事件对象统一化后,构建的一个事件对象」
合成事件对象中,也包含了浏览器内置事件对象中的一些属性和方法。
常用的基本都有:
- clientX/clientY
- pageX/pageY
- target
- type
- preventDefault
- stopPropagation
- ...
- .nativeEvent:基于这个属性,可以获取浏览器内置『原生』的事件对象
react合成事件对象和内置事件对象的不同处:
react合成事件对象:该有的都有。
- 其中对象中
nativeEvent
对象中存放的是内置事件对象中的属性和方法; - 经过bind处理后,ev是最后一个实参。
react 16与react 17+ 的合成事件对象的区别:
react 16 中使用的对象缓存池的机制
在react16中react内部基于事件对象池做了一个缓存机制!!属性都是做了get set劫持,且属性值为null。
react 16 对象缓存池
当每一次事件触发的时候,如果传播到了委托的元素上【document】,在委托的方法中我们首先会对内置的事件对象做统一处理,生成合成事件对象!
为了防止每一次的触发都是重新创建出新的事件合成对象,设置了一个事件对象池【缓存池】,
- 本次事件触发,获取到事件操作的相关信息,我们从事件
事件对象池
中获取存储的合成事件对象,把信息赋给相关的成员。 - 等待本次操作结束,把合成对象中的成员信息都给
清空
掉,再放入到事件对象池
中。这也就是为什么我们在16版本中通过异步方法获取合成事件对象的某个属性值时,得到的是null!!
<div
className="wrapper"
onClick={(ev) => {
console.log(ev.clientX, "同步获取ev.clientX");
setTimeout(() => {
console.log(ev.clientX, "异步获取ev.clientX");
}, 500);
}}
>click me</div>
react 17+ 的合成事件对象:
react17及以后去掉了对象缓存池机制。异步也是可以获取到属性值的
转载自:https://juejin.cn/post/7282693176204034063