首页 > 编程语言 >React 源码-React 事件全解

React 源码-React 事件全解

时间:2022-08-22 01:22:20浏览次数:116  
标签:react 函数 事件处理 绑定 React listeners 源码 事件 全解

事件系统

react v17

事件绑定

事件绑定在函数 setInitialDOMProperties

setInitialDOMProperties 将在 complete 阶段执行

function setInitialDOMProperties(
  tag: string,
  domElement: Element,
  rootContainerElement: Element | Document,
  nextProps: Object,
  isCustomComponentTag: boolean
): void {
  // *遍历 props
  for (const propKey in nextProps) {
    if (!nextProps.hasOwnProperty(propKey)) { continue; }
    const nextProp = nextProps[propKey];
    if (...) { ... }
    // *registrationNameDependencies 包含 react 支持的所有的事件,如果当前的 propKey 是 react支持的事件就进入该 if
    else if (registrationNameDependencies.hasOwnProperty(propKey)) {
      if (nextProp != null) {
        // !注意,这里与 react v16 有所不同,v16 这里直接执行 ensureListeningTo 函数,但是 v17 这里不会执行。因为 enableEagerRootListeners 是一个常量,值一直为 true,if (false) 自然不会执行,并且在 react-dom.development.js 中直接没有这个 if,只剩下 onScroll 的判断。这样更能说明问题了
        if (!enableEagerRootListeners) {
          // *忽略这个函数,并没有执行
          ensureListeningTo(rootContainerElement, propKey, domElement);
        } else if (propKey === 'onScroll') {
          listenToNonDelegatedEvent('scroll', domElement);
        }
      }
    } else if (nextProp != null) {
      setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
    }
  }
}

那么在 react v17 中遍历到 onClick 这种事件的时候貌似并没有做什么。那么事件绑定是什么时候绑定的呢?其实在最开始的 createRootImpl 也就是创建 HostRootFiber 时就通过 listenToAllSupportedEvents 将所有支持的事件都绑定到了 rootContainerElement (这里也对应了 react v17 就将事件统统绑定到 rootContainer 而不是 document)

那么事件处理函数又是多久绑定的呢?

通过对绑定的事件处理函数进行 debugger 可以发现,其实根本没有将事件处理函数直接绑定到 rootContainerElement 上,而是直接使用的上面 listenToAllSupportedEvents 中绑定的事件。大概的流程为:

  1. listenToAllSupportedEventsrootContainerElement 绑定所有的事件
  2. 点击子组件,其实就相当于在点击 rootContainerElement 所以会触发对应的点击事件。
  3. 绑定事件的时候会根据事件优先级绑定不同的处理函数,但是最终其实都是执行 dispatchEvent
  4. dispatchEvent 内部将将进入其他函数,获取触发事件的元素,然后根据对应的 Fiber 然后在根据很多层函数,最终执行事件处理函数。

事件触发

使用的案例

import React from 'react'
import './index.css'
class EventDemo extends React.Component{
  state = {
    count: 0,
  }

  onDemoClick = e => {this.setState({ count: this.state.count + 1 })}
  onParentClick = () => {console.log('父级元素的点击事件被触发了');}
  onParentClickCapture = () => {console.log('父级元素捕获到点击事件');}
  onSubCounterClick = () => {console.log('子元素点击事件');}
  onSubCounterClickCapture = () => {console.log('子元素点击事件 capture')}

  render() {
    const { count } = this.state
    return <div className={'counter-parent'} onClick={this.onParentClick} onClickCapture={this.onParentClickCapture}>
      counter-parent
      <div onClick={this.onDemoClick} className={'counter'}>
        counter:{count}
        <div className={'sub-counter'} onClick={this.onSubCounterClick} onClickCapture={this.onSubCounterClickCapture}>
          子组件
        </div>
      </div>
    </div>
  }
}

export default EventDemo
  1. 点击子元素后,自然会执行 dispatchEvent
  2. 然后会进入 attemptToDispatchEvent
    (如果没有正在进行的事件?因为在进入 attemptToDispatchEvent 之前会进行 hasQueuedDiscreteEvents
    hasQueuedDiscreteEvents 判断 具体可以看 dispatchEvent)
    然后在 attemptToDispatchEvent 中会通过原生的事件参数(event)获取到触发事件的 DOM,然后通过该 DOM 获取到对应的 Fiber
    然后正常情况下会进入 dispatchEventForPluginEventSystem.
  3. dispatchEventForPluginEventSystem 一般会进入批量更新,也就是 batchEventUpdates,与 render 时的一样,也会传入一个匿名函数,不过该匿名函数内部执行的是:dispatchEventsForPlugins.
  4. dispatchEventsForPlugins 内部又执行 extractEvents 函数
  5. extractEvents 函数内部又会使用 EventPlugin 创建 react合成事件 的 Event 参数,并且会遍历 Fiber 链表,将将会触发的事件统统放到 dispatchQueue 中(具体遍历 Fiber 的函数是在 accumulateSinglePhaseListeners )。
    accumulateSinglePhaseListeners 具体流程如下
    1. 首先会判断当前是 捕获阶段 还是 冒泡阶段 根据阶段的不同,使用不同的 reactEventName (例如:onClick 还是 onClickCapture)
    2. 然后会进入 while 循环,循环中会通过 reactEventName 获取 instance 的事件处理函数,即 listener 如果 listener 不为 null 那么就会将 { currentTarget, instance, listener } 放到 listeners 中(currentTarget 是当前的 dom 元素,instance 是当前的 Fiber,listener 是当前的事件处理函数),接着将 instance 指向 instance.return 继续 while 循环。
    3. while 循环结束后,会将 listeners 返回出去。
  6. extractEvents 执行完成后,就会开始执行 dispatchQueue 中的内容了。

针对我们的案例,分析一下具体的流程

  1. 点击子元素,那么就会直接触发 root 的 clickCapture 事件
  2. 进入 dispatchEvent
  3. 进入 attemptToDispatchEvent, 获取真实触发事件的 dom 和对应的 Fiber
  4. 进入 dispatchEventForPluginEventSystem
  5. 进入 batchEventUpdates
  6. 进入 dispatchEventsForPlugins
  7. 进入 extractEvent 获取 react合成事件参数
  8. 进入 accumulateSinglePhaseListeners 获取 listeners 数组因为当前是捕获阶段(在代码中会判断是什么阶段),所以就只会收集捕获阶段的事件处理函数(直接 push 到 listeners 并不会像小册中说的那样 遇到捕获事件就 unshift 可能是版本问题),经过测试可以得知,无论案例中是否绑定了 clickCapture 都会去试图收集捕获阶段的事件处理函数, 只是收集不到而已
  9. 返回 extractEvent 将 listeners 放到 dispatchQueue 中去
  10. 返回 dispatchEventsForPlugins 进入 processDispatchQueue 内部会判断当前到底是什么阶段,接着循环 dispatchQueue
  11. 进入 processDispatchQueueItemsInOrder ,根据阶段不同,按照不同的顺序执行 listeners,比如捕获阶段的话,就是从后往前,冒泡阶段的话就是从前往后。
  12. 再经过一系列函数的包裹,最终顺利执行函数。
  13. capture 阶段完成后,直接进入 bubble 阶段,再次按照上面的顺序执行,最终 bubble 阶段也完成。就是这样。

注意:listeners 的结构应该是 { currentTarget, instance, listener }, dispatchQueue 的结构应该是 [ { event, listeners } ] 此时的 event 应该是 React合成事件 event

注意:在此案例中,无论是捕获阶段,还是冒泡阶段,因为 listeners 是一个数组(该阶段将要触发的所有 listener 数组),所以 dispatchQueue 中都只有一个元素,不清楚在上面情况下 dispatchQueue 才有多个元素。

标签:react,函数,事件处理,绑定,React,listeners,源码,事件,全解
From: https://www.cnblogs.com/99xx/p/react-source-parse-event.html

相关文章

  • vscode中react标签自动补全
    在vscode中编写react时,发现标签没有自动补全,写起来不太灵活,查资料发现:默认在js文件中JSX语法无法自动补全,解决方法如下:文件=》首选项=》设置搜索"emmet.includeLanguages"......
  • Linux 基于源码安装 Redis
    1.下载Redis:前往Redis官网复制Redis相应版本的下载链接,到终端下载2.进入到指定目录,下载redis.tar.gz包,运行wget+复制的下载链接 例如:wgethttps://d......
  • react组件三大核心之一state
    -<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="wi......
  • Spring容器创建Bean源码分析
    一、Spring提供了多种上下文来实现容器功能比如:AnnotationConfigApplicationContext、ClassPathXmlApplicationContext这里使用AnnotationConfigApplicationCont......
  • Mybatis源码4 Cache的实现和其原理
    Mybatis源码4Cache的实现和其原理一丶Cache的实现类TransactionalCache事务缓存,一次性存入多个缓存,移除多个缓存PerpetualCache基于HashMap的缓存实现LoggingCach......
  • Mybatis源码5 StatementHandler ,ParameterHandler
    Mybatis源码5StatementHandler,ParameterHandler一丶概述前面我们总结了SqlSession--->CachingExecutor--->BaseExector---->Excutor子类doQuery,doUpdate的执行流程,my......
  • Mybatis 源码6 结果集映射流程 ,mybatis插件实现原理和基于mybatis插件实现参数化类型T
    Mybatis源码6结果集映射流程,mybatis插件实现原理和基于mybatis插件实现参数化类型TypeHandler一丶前情回顾书接上回,下面是SimpleExecutor执行查询的主要逻辑prepa......
  • Mybatis源码1JDBC->mybatis主要流程->mybatis Excutor简介
    Mybatis源码1JDBC->mybatis主要流程->mybatisExcutor简介一丶mybatis概述MyBatis是一款优秀的持久层框架,它支持自定义SQL、存储过程以及高级映射。MyBatis免除了几乎......
  • Spring源码学习笔记4——BeanFactoryPostProcessor执行
    一丶BeanFactoryPostProcessor是什么Spring留给我们的一个扩展接口,在BeanDefinition加载注册完之后,并执行一些前置操作(笔记3)之后会反射生产所有的BeanFactoryPostProcesso......
  • Spring源码学习笔记6——Spring bean的实例化
    一丶前言前面我们了解到读取xmlor根据扫描路径生成BeanDefinition并注册到BeanFactory,相当于我们具备了生火做饭的原材料:BeanDefinition,接下来就是Spring最为核心的,根据......