最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 写给自己看的React源码解析(四):React事件系统的实现原理

    正文概述 掘金(zouwowo)   2021-01-28   942

    前言

    React有着独特的事件机制-合成事件,React的初学者肯定碰到过这种问题,使用event.stopPropagation();,却还是无法禁止当前组件的事件冒泡,这就是React的事件机制的原因,它并不与DOM事件相同。

    本文将从源码的角度来解析,React的事件系统到底是如何实现的?

    DOM事件流

    DOM事件流属于比较基础的知识点,本文不会详细的再叙述,只列出需要一些关键点。W3C标准约定了一个事件的传播过程要经过以下 3 个阶段:

    • 1.事件捕获阶段
    • 2.目标阶段
    • 3.事件冒泡阶段

    通过DOM事件流,我们经常会用到一直常见的性能优化思路:事件委托

    React事件系统也是基于事件委托这个特性实现的。

    React事件系统

    React中,除了一些不可冒泡的事件外,其它的事件都不会被绑定在具体的元素上,而是统一被绑定到document上(17版本之后修改为绑定到React的根DOM组件上),当事件在具体的DOM节点上被触发后,最终都会冒泡到document上,React根组件上所绑定的统一事件处理程序会将事件分发到具体的组件实例。

    在分发事件之前,React首先会对事件进行包装,把原生DOM事件包装成合成事件。

    React合成事件

    合成事件是React自定义的事件对象,它在底层抹平了不同浏览器的差异,在上层面向开发者暴露统一的、稳定的、与DOM原生事件相同的事件接口。

    虽然合成事件并不是原生DOM事件,但它保存了原生DOM事件的引用。当你需要访问原生DOM事件对象时,可以通过合成事件对象的e.nativeEvent属性获取到原生DOM事件。

    React事件的绑定

    事件的绑定是在组件的首次渲染链路的completeWork方法中完成的。关于首次渲染的流程,可以看我之前的文章

    写给自己看的React源码解析(一):你的React代码是怎么渲染成DOM的?

    completeWork主要做了三件事情:

    • 创建 DOM 节点(createInstance)
    • 将 DOM 节点插入到 DOM 树中(appendAllChildren)
    • 为 DOM 节点设置属性(finalizeInitialChildren)。

    finalizeInitialChildren方法中,会遍历节点的props。当遍历到事件相关的props时,就会触发事件的注册链路。

    本文基于16.13版本的React源码。注意:17版本的事件系统在源码上的改动比较大,源码链路上跟本文并不一致。

    写给自己看的React源码解析(四):React事件系统的实现原理

    ensureListeningTo中,会获取当前DOM中的document对象,然后通过调用legacyListenToEvent,将统一的事件监听函数注册到document上面。

    legacyListenToEvent中,实际上是通过调用legacyListenToTopLevelEvent来处理事件和document之间的关系的。 legacyListenToTopLevelEvent直译过来是“监听顶层的事件”,这里的“顶层”就可以理解为事件委托的最上层,也就是document节点。

    注意:在17版本中,流程中不会存在ensureListeningTolegacyListenToEvent方法,React会在finalizeInitialChildren方法下的setInitialProperties根据节点的tag类型,传入不同的参数并调用listenToNonDelegatedEvent方法。在这个方法里,会直接调用addTrappedEventListener添加事件到React的根组件DOM元素上。

    listenToNonDelegatedEvent源码地址

    我们接着来看,最终注册到document上的并不是某一个DOM节点上对应的具体回调逻辑,而是一个统一的事件分发函数listener,它的本体是一个dispatchEvent

    React事件的触发

    事件的触发其实就是对于dispatchEvent函数的调用。

    写给自己看的React源码解析(四):React事件系统的实现原理

    我们根据下面的这个demo来走流程

    import React from 'react';
    import { useState } from 'react'
    function App() {
      const [state, setState] = useState(0);
      return (
        <div className="App">
          <div
          	className="container"
            onClickCapture={() => console.log('捕获经过 div')}
            onClick={() => console.log('冒泡经过 div')} 
          >
            <p>{state}</p>
            <button onClick={() => { setState(state + 1) }}>点击+1</button>
          </div>
        </div>
      );
    }
    export default App;
    

    这个demo的功能很简单,每次点击按钮都会给state加1。并给container这个div上添加了两个点击事件,一个捕获事件,一个冒泡事件。下图是这个demo的fiber树结构。

    写给自己看的React源码解析(四):React事件系统的实现原理

    收集的逻辑过程在traverseTwoPhase函数

    function traverseTwoPhase(inst, fn, arg) {
      // 定义一个 path 数组
      var path = [];
    
      while (inst) {
        // 将当前节点收集进 path 数组
        path.push(inst);
        // 向上收集 tag===HostComponent 的父节点
        inst = getParent(inst);
      }
      var i;
      // 从后往前,收集 path 数组中会参与捕获过程的节点与对应回调
      for (i = path.length; i-- > 0;) {
        fn(path[i], 'captured', arg);
      }
    
      // 从前往后,收集 path 数组中会参与冒泡过程的节点与对应回调
      for (i = 0; i < path.length; i++) {
        fn(path[i], 'bubbled', arg);
      }
    }
    

    traverseTwoPhase函数做了以下三件事情。

    1. 循环收集符合条件的父节点,存进 path 数组中
    2. 模拟事件在捕获阶段的传播顺序,收集捕获阶段相关的节点实例与回调函数
    3. 模拟事件在冒泡阶段的传播顺序,收集冒泡阶段相关的节点实例与回调函数

    收集父节点

    traverseTwoPhase会以触发事件的目标节点为起点,通过getParent方法,不断向上寻找tag===HostComponent的父节点,并将这些节点按顺序收集进path数组中。tag===HostComponent的节点是DOM元素对应的的fiber节点类型,也就是说只收集DOM元素对应的节点。

    按照demo中的fiber树来说,最后收集到的节点为div#containerdiv.Appbutton节点。

    写给自己看的React源码解析(四):React事件系统的实现原理

    模拟捕获顺序,收集节点实例与回调函数

    for (i = path.length; i-- > 0;) {
        fn(path[i], 'captured', arg);
      }
    

    path数组是从子节点出发,向上收集得来的。所以,模拟事件的捕获顺序,需要从后往前遍历path数组。在遍历的过程中,fn函数检测每个节点的事件回调,若该节点上对应当前事件的捕获回调不为空,那么节点fiber实例会被收集到合成事件的SyntheticEvent._dispatchInstances中,事件回调则会被收集到合成事件的SyntheticEvent._dispatchListeners属性。

    模拟冒泡顺序,收集节点实例与回调函数

     for (i = 0; i < path.length; i++) {
        fn(path[i], 'bubbled', arg);
      }
    

    这里功能跟上一步一致,区别只是从前往后来遍历path数组。

    最后,我们来看下SyntheticEvent对象上的_dispatchInstances_dispatchListeners写给自己看的React源码解析(四):React事件系统的实现原理

    我们只要按顺序调用执行回调函数,就能够模拟出DOM事件流,也就是 “捕获-目标-冒泡”这三个阶段。

    react17中对于事件系统的更新

    上文的源码解析是基于16.13.x版本的,17版本之后的事件系统,有了挺大的区别。

    • 1.事件系统改为挂载到React的根组件dom上
    • 2.onScroll事件不再冒泡
    • 3.onFocusonBlur事件已在底层切换为原生的focusinfocusout事件
    • 4.onClickCapture现在使用的是实际浏览器中的捕获监听器(合成事件只会存在listenToNonDelegatedEvent添加的冒泡事件)
    • 5.事件池SyntheticEvent不再复用,在点击事件中使用异步方法也将可以获取到点击事件。不需要再使用e.persist()方法

    感谢

    如果本文对你有所帮助,请帮忙点个赞,感谢!


    起源地下载网 » 写给自己看的React源码解析(四):React事件系统的实现原理

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元