React的合成事件

什么是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合成事件对象
上面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 捕获「合成」');}

上面就是把onClickonClickCapture方法赋值给了元素的属性。

然后对#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)

浏览器输出
#root事件绑定捕获和冒泡
上面当点击innerouter时,就会执行#root上绑定的方法,当执行绑定的方法时,就会把所有规划的路径中,有合成事件属性的都执行即可。
#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内部的合成事件是利用事件委托 (事件传播机制) 完成的!


  转载请注明: 小浩之随笔 React的合成事件

 上一篇
React的Hooks函数useState React的Hooks函数useState
前言 在React开发中,类组件开发是具备状态的,可以使用this.setState来更新状态,使组件重新渲染,但在函数组件中,是不具备状态,也没有实例的概念,调用组件不再是创建类的实例,而是把函数执行,产生一个私有上下文,且在函数组件中不
2022-04-11
下一篇 
React的setState的使用 React的setState的使用
在React类组件开发中,使用setState 用于更新组件的状态。它是一个异步操作,它会将新的状态合并到当前状态中,然后触发组件的重新渲染。那么为什么要使用它来更新状态呢? 为什么使用setState 大家都知道,在开发中我们并不能直接通
2022-04-10
  目录