# React专栏 - lesson2 / 浅谈react内置事件原理

啥是react的内置事件?

onClick, onMouseEnter这类事件就是react给开发者提供的内置事件, 我们来看看一些比较常见的应用

// Test组件
import React from 'react';

export default class Test extends React.PureComponent {
    divClick = e => {
        console.log('react事件: div被点击了')
    }
    buttonClick = e => {
        console.log('react事件: button被点击了');
    }
    render() {
        return (
            <div style={ {
                width: '200px',
                height: '200px',
                backgroundColor: 'green',
                margin: '100px auto',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center'
            } } onClick={ this.divClick }>
                <button onClick={ this.buttonClick }>按钮</button>
            </div>
        )
    }
}

该Test组件渲染进页面, 然后我们点击button按钮效果如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aPsAlq5Q-1592562660139)(./react事件.gif)]

其实结果如我们所料, 事件肯定会从button按钮冒泡上div按钮, 同时我们如果不想冒泡, 调用事件源对象上的stopPropagation就可以坐到了, 一切看起来都是这么的顺滑, 跟原生js简直是一模一样

但是, 真的是一模一样吗?

我们来做一些其他的操作, 在Test组件下(或者你想写在任何地方都行, 但是不是js中就不行的哈), 我们加上这样一行代码

// Test.js
...
export default class Test extends React.PureComponent {
    ...
}

// 既然看起来是一样的, 那我们就给原生的dom也绑定个事件看看效果
document.getElementById('root').onclick = () => {
    console.log('native事件: id为root的根结点被点击了')
}

当我们点击button按钮以后效果如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U8iDDl5z-1592562660140)(./加上原生事件.gif)]

id为root的dom元素作为所有react元素的容器, 我们的预期结果是从button冒泡到div最终冒泡到id为root的dom元素, 但是情况好像挺惊悚的, 因为我们发现id为root的dom元素的点击事件竟然是最先触发的?

可能有同学会想到是不是捕获了? 那你仔细看看buttondiv的执行顺序, 你还觉得是捕获的原因吗?

这个问题咱们先放这, 再来看一个现象, 我们在id为root的dom元素的点击事件中加上一行代码

// Test.js
...
export default class Test extends React.PureComponent {
    ...
}

document.getElementById('root').onclick = e => {
   ...
   e.stopPropagation(); // 没错我们阻止了事件冒泡
}

当我们点击button按钮以后效果如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ifEuHAhu-1592562660142)(./在原生事件里阻止事件冒泡.gif)]

效果是相当的惊悚, 我们发现button事件和div事件双双都失效了???

还不够, 我们再来看一个现象, 我们在button和在root的点击事件中分别这样操作

 buttonClick = e => {
    console.log('react事件: button被点击了');
    console.log('我是button绑定事件中输出的e.target', e.target);
    setTimeout(() => {
        console.log('我是button绑定事件中在setTimeout输出的e.target', e.target);
    }, 0)
}
...

document.getElementById('root').onclick = () => {
    console.log('native事件: id为root的根结点被点击了');
    console.log('我是root绑定事件中输出的e.target', e.target);
    setTimeout(() => {
        console.log('我是root绑定事件中在setTimeout输出的e.target', e.target);
    }, 0)
}

我们点击button后效果又如下

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ABB31Jz6-1592562660143)(./异步操作react事件.gif)]

这下好了, 不仅报错, 而且在buttonsetTimeout中输出的e.target竟然是null

看到这里, 笔者相信你心里也清楚了, react一定有着自己的事件机制且跟原生的事件机制完全不一样, 所以在这篇博客我们要探究的就是react的事件到底是什么机制, 不涉及源码, 也不写的过深, 但是接下来的这些总结, 笔者相信你看了以后对你的帮助会非常的大

其实是这样的, React几乎将所有的事件都交给了document进行委托, 也就是说, react希望所有的事件最终都冒泡到document身上, 然后从document往下面去找触发回调函数并依次执行

关于上面的两个问题

  1. 为什么root的点击事件先于buttondiv触发
    答: 因为当我们点击button的时候, 这个时候button的点击事件确实已经执行了, 并且开始冒泡, 从button冒泡到div再冒泡到root, 但是由于buttondiv绑定的是React事件, 即使这个按钮被真实的点击了, 系统也无法给出相应的反馈, 而root绑定的是真实的dom事件, 所以当冒泡到他的时候, 系统知道, 哦你要打印东西, 于是root的点击事件执行了, 然后继续冒泡, 冒泡到document, document在一开始就已经被react偷偷的绑定了点击事件, 这个时候document的点击事件触发, 通过事件源对象去找到dom元素原来是button, 这个时候react就去找这个button上绑定的React事件执行, 当button执行完毕以后, 就找虚拟dom树中button的父节点, 如果父节点有绑定点击事件且无其他干扰因素就执行, 然后继续往上找, 直到找到虚拟dom树的中没有绑定事件的父级为止, 其实你更应该理解为, 不是button的点击事件没有执行, 而是点击事件发生以后点击事件的回调函数被react延迟执行了
  1. 为什么我们在root中阻止了事件冒泡以后, buttondiv的事件都不处理了?
    答: 有了前面的铺垫, 这个问题的答案就非常的简单了, 既然react将所有的事件都委托给了真实的document的dom事件中, 那么在root的点击事件中取消了事件冒泡, 那么document上的事件就不能被执行, 既然不能被执行, 那么怎么去找虚拟dom呢?
  1. 为什么在reactsetTimeout中取的e.targetnull而且报错呢?
    答: 这是因为React出于对性能的考虑, 设定了一个事件池, 这么说把, 就是react他会重用事件源对象这个e, 就比如我们在button的点击事件里拿到的e是一个对象对吧, 在div中拿到的e也是一个对象是吧, 其实这两个对象是一个, react会在button的点击事件回调函数的最后一行同步代码完成以后, 将buttone对象中的每一个属性赋值为null, 然后在div的点击事件回调执行之前, 将这个e中的每一个属性又重新赋值为div的各项事件源对象属性, 所以当我们异步操作react元素的事件源对象的时候, 往往得到的都是null, 所以我们如果用一个全局变量保存了buttone的话, 在div的点击回调函数中我们是发现这两个值是全等的

我们可以来看看这个报错信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TZXY4Aru-1592562660144)(./报错信息.png)]

如果你英语好, 相信你已经看懂了, 没看懂也没关系, 这一段话就是react告诉你, 事件源对象是会被重用的, 而且会在重用之前将属性置空为null, 如果你真的想在异步代码中也要操作事件源对象, 那么你必须调用event.persist方法, 这个方法调用以后, react将不再重用该事件源对象, 而是会持久化它, 你随时可以使用

可能有同学注意到了笔者上面说react几乎将所有的事件都注册在了document上, 那意思是有例外喽? 没错还真的有例外

比如focus, input, 这些表单事件, 你说怎么冒泡给document, 所以这些事件react是直接在元素本身上监听了

如果你觉得上面的文字看着实在是头疼, 笔者帮你总结好了, 你可以很清晰的知道react的事件原理, 当然, 如果你想彻底理解, 上面的文字是你无法避免的

我们讨论的事件: 指的是react内置dom组件中的事件

  1. react在内部实际上是将**几乎**所有元素的点击事件通过事件委托的方式绑定在了document

  2. document的事件处理中, react会根据虚拟dom树来一步一步完成事件处理函数的调用

  3. 如果给真实的dom注册事件回调, 那么事件会先于任何一个react事件执行

  4. 如果给真实的dom注册事件回调, 回调中阻止了事件冒泡, 那么所有的react同类型事件一个都执行不了, 因为如果事件无法冒泡到document, react事件还怎么执行?

  5. 如果在跟其他的库进行联合开发的时候, 要避免document上的事件冲突, 可以调用事件源对象中的e.nativeEvent.stopImmediatePropagation来阻止其他库给document注册事件

    <button onclick={ e => {
        e.nativeEvent.stopImmediatePropagation(); // 这哥们一用, 其他库就没办法给document注册同类型事件了
    } }></button>
    
  6. react中的事件参数, 并非原生的dom事件参数, 而是react自行合成的一个对象, 该对象长得很像原生dom的事件参数, 在react事件中:

      1. e.stopPropagation, 可以阻止元素在虚拟dom树中冒泡, 但是它并不能阻止真实dom树的冒泡, 你想啊, react元素的事件都要等到真实dom的document执行了react元素事件才会被执行, 那你怎么阻止真实dom的冒泡呢
      1. e.nativeEvent可以拿到正儿八经的原生事件源对象
      1. 为了提高执行效率, react会重用事件源对象, 所以我们不要在异步操作中去使用事件源对象, 如果我们一定要使用, 请在使用前调用e.per sist()来阻止react重用该事件源对象
      <button onclick={ e => {
          e.persist();
          console.log(e.target); // button真实dom元素
          setTimeout(() => {
              console.log(e.target); // button真实dom元素
          }, 1000) 
      } }></button>
      
  7. 并不是所有的事件都在document上进行委托, 像input, focus这些表单事件, react还是选择在元素本身上进行监听, 因为他们根本无法冒泡, 你可以仔细想想 focus这种聚焦事件怎么冒泡, 所以也意味着, 即使我们在真实dom中阻止某类事件冒泡, 比如阻止click事件冒泡, react所有click失效, 阻止mouseenter, react所有mouseenter失效, 但是如果阻止focus, 不好意思, react的focus事件依旧生效


ok, 关于react的事件原理, 笔者已经基本写完了, 未来会再从源码层面真正的剖析它的事件池等机制, 希望这篇博客能够帮助到你, 也希望能够让你在读源码的时候对事件机制更加的得心应手

猜你喜欢

转载自blog.csdn.net/weixin_44238796/article/details/106861902
今日推荐