什么是React的合成事件
React 合成事件(SyntheticEvent)是 React 框架提供的一种事件对象,它封装了不同浏览器的事件对象,使得开发者可以编写跨浏览器兼容的代码。合成事件对象中,也包含了浏览器内置事件对象中的一些属性和方法。
在下面的代码中,当用户点击button
元素时,会触发 handleClick
方法,并将 React 合成事件对象作为参数传递给该方法。在 handleClick
方法中,可以访问事件对象的属性和方法,
class Parent extends React.Component{
handleClick = (event) =>{
console.log(event)
}
render(){
return <div>
<h2>React的合成事件</h2>
<div>
<button onClick={this.handleClick}>Click me!</button>
</div>
</div>
}
}
浏览器输出合成事件对象
可以看到在这个合成事件对象中包含了很多常用的属性和方法,比如:
- clientX/clientY
- pageX/pageY
- target
- type
- preventDefault
- stopPropagation
- nativeEvent:基于这个属性,可以获取浏览器内置原生的事件对象
合成事件的this
当事件行为触发,绑定的函数执行,方法中的this
会是undefined
。
class Parent extends React.Component{
handleClick(){
console.log(this)//undefined
}
render(){
return <div>
<h2>React的合成事件</h2>
<div>
<button onClick={this.handleClick}>Click me!</button>
</div>
</div>
}
}
上面this
打印出来是undefined
,解决这个问题有三种方案:
1、可以基于JS中的bind
方法: 预先处理函数中的this
和实参。
class Parent extends React.Component{
handleClick(...args){
console.log(this)
console.log(args)
}
render(){
return <div>
<h2>React的合成事件</h2>
<div>
<button onClick={this.handleClick.bind(this,10,20,30)}>Click me!</button>
</div>
</div>
}
}
浏览器输出
上面bind
第一个参数就是this
,如果绑定的方法里面用不到this
,可以传null
,第二个参数及后面就是传给函数的参数了,而函数参数最后一个默认传的都是合成事件对象。
2、可以把绑定的函数设置为“箭头函数”,让其使用上下文中的this
也就是我们的实例。
class Parent extends React.Component{
handleClick = (...args) => {
console.log(this)
console.log(args)
}
render(){
return <div>
<h2>React的合成事件</h2>
<div>
<button onClick={this.handleClick}>Click me!</button>
</div>
</div>
}
}
上面就把会触发的函数handleClick
写为了箭头函数,里面的this
就是我们的实例。
3、可以在把事件写成函数的形式也是OK的。
class Parent extends React.Component{
handleClick(...args){
console.log(this)
console.log(args)
}
render(){
return <div>
<h2>React的合成事件</h2>
<div>
<button onClick={(event)=>this.handleClick(10, 20, event)}>Click me!</button>
</div>
</div>
}
}
上面把事件写成了函数,在函数内调用handleClick
方法,这样写在函数内拿到的this
就是触发这个函数的父级调用(实例),在函数内如果需要使用合成事件,需要在函数内传个event
事件参数。
合成事件的原理
在React开发中,当给元素添加事件时,并不是基于addEventListener
单独给真实DOM节点上做的事件绑定,React中的合成事件,都是基于“事件委托”处理的!
class Parent extends React.Component{
handleOuterClick = () =>{
console.log('outer-冒泡合成')
}
handleOuterCapture = () =>{
console.log('outer-捕获合成')
}
handleInnerClick = () =>{
console.log('inner-冒泡合成')
}
handleInnerCapture = () =>{
console.log('inner-捕获合成')
}
componentDidMount(){
const root = document.querySelector('#root')
root.addEventListener('click',(ev) => {
console.log('root 捕获')
},true)
root.addEventListener('click',(ev) => {
console.log('root 冒泡')
},false)
}
render(){
return <div>
<h2>React的合成事件</h2>
<div id="outer" className='outer' onClick={this.handleOuterClick} onClickCapture={this.handleOuterCapture}>
<div id="inner" className='inner' onClick={this.handleInnerClick} onClickCapture={this.handleInnerCapture}></div>
</div>
</div>
}
}
在组件染的时候,如果发现JSX元素属性中有onXxx/onXxxCapture
这样的属性,不会给当前元素直接做事件绑定,只是把绑定的方法赋值给元素的相关属性! 比如:
const outer = document.querySelector('#outer')
const inner = document.querySelector('#inner')
outer.onClick=() => {console.log('outer 冒泡「合成」');}
outer.onClickCapture=() => {console.log('outer 捕获「合成」');}
inner.onClick=() => {console.log('inner 冒泡「合成」');}
inner.onClickCapture=() => {console.log('inner 捕获「合成」');}
上面就是把onClick
和onClickCapture
方法赋值给了元素的属性。
然后对#root
容器做了事件绑定捕获和冒泡
const root = document.querySelector('#root')
const outer = document.querySelector('#outer')
const 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.addEventListener('click',(ev) => {
console.log('root 捕获',ev.composedPath())
let path = ev.composedPath()//事件源->触发Dom事件的元素一路冒泡到window的所有祖先元素
const doms = [...path].reverse()
doms.forEach(ele => {
let handle = ele.onClickCapture;
if(handle)handle()
})
},true)
root.addEventListener('click',(ev) => {
console.log('root 冒泡')
let path = ev.composedPath()
path.forEach(ele => {
let handle = ele.onClick;
if(handle)handle()
})
},false)
浏览器输出
上面当点击inner
或outer
时,就会执行#root
上绑定的方法,当执行绑定的方法时,就会把所有规划的路径中,有合成事件属性的都执行即可。
因为组件中所渲染的内容,最后都会插入到#root
容器中,这样点击页面中任何一个元素,最后都会把#root
的点击行为触发!而在给#root
绑定的方法中,把之前给元素设置的onXxx/onXxxCapture
属性,在相应的阶段执行!
原生事件和合成事件的执行顺序
下面是测试原生事件和合成事件的执行顺序的完整代码段:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>react合成事件原理</title>
<style lang="scss">
.flex{
display: flex;
justify-content: center;
align-items: center;
}
#root{
width:300px;
height:300px;
background-color: red;
#outer{
width: 200px;
height: 200px;
background-color: yellow;
}
#inner{
width: 100px;
height: 100px;
background-color: blue;
}
}
</style>
</head>
<body>
<div id="root" class="flex">
<div id='outer' class="flex">
<div id='inner'></div>
</div>
</div>
<script>
const html = document.documentElement,
body = document.body,
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 捕获「合成」');}
html.addEventListener('click',(ev) =>{
console.log('html 捕获')
},true)
html.addEventListener('click',(ev) =>{
console.log('html 冒泡')
},false)
body.addEventListener('click',(ev) =>{
console.log('body 捕获')
},true)
body.addEventListener('click',(ev) =>{
console.log('body 冒泡')
},false)
outer.addEventListener('click',(ev) =>{
console.log('outer 原生捕获')
},true)
outer.addEventListener('click',(ev) =>{
console.log('outer 原生冒泡')
},false)
inner.addEventListener('click',(ev) =>{
console.log('inner 原生捕获')
},true)
inner.addEventListener('click',(ev) =>{
console.log('inner 原生冒泡')
},false)
root.addEventListener('click',(ev) => {
console.log('root 捕获')
let path = ev.composedPath()//事件源->触发Dom事件的元素一路冒泡到window的所有祖先元素
const doms = [...path].reverse()
doms.forEach(ele => {
let handle = ele.onClickCapture;
if(handle)handle()
})
},true)
root.addEventListener('click',(ev) => {
console.log('root 冒泡')
let path = ev.composedPath()
path.forEach(ele => {
let handle = ele.onClick;
if(handle)handle()
})
},false)
</script>
</body>
</html>
浏览器效果
综合上面执行顺序可以画出原生事件和合成事件的执行流程展示对比如下图
event.stopPropagation()
合成事件对象中的“阻止事件传播”,阻止原生的事件传播,阻止合成事件中的事件传播。
event.nativeEvent.stopPropagation()
原生事件对象中的“阻止事件传播”,只能阻止原生事件的传播。
event.nativeEvent.stopImmediatePropagation()
原生事件对象的阻止事件传播,只不过可以阻止#root
上其它绑定的方法执行。
总结
首先所谓的合成事件绑定,其实并没有给元素本身做事件绑定,而是给元素设置 onXxx/onXxxCapture
这样的合成事件属性,当事件行为触发,根据原生事件传播的机制,都会传播到#root
容器上,React内部给#root
容器做了事件绑定捕获&冒泡。
然后当React内部绑定的方法执行的时候,会依据ev.composedPath()
中分析的路径依次把对应阶段的 onXxx/onXxxCapture
等事件合成属性触发执行!
最后React内部的合成事件是利用事件委托 (事件传播机制) 完成的!