React 源码-React 事件流程解析

事件系统

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.extractEvents 创建 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 中的内容了。

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

flowchart TB trigger[click] --> |省略了一些中间函数|dispatchEvent[dispatchEvent] --> attemptToDispatchEvent["attemptToDispatchEvent \n 获取真实 触发事件的 DOM 和 Fiber"] --> dispatchEventForPluginEventSystem --> batchEventUpdates[batchEventUpdates \n 批量更新] --> extractEvent["EventPlugin.extractEvent"] --> accumulateSinglePhaseListeners[获取 listeners 数组] --> |返回 listeners 数组|extractEvent2[回到 EventPlugin.extractEvent \n 如果 listeners.length 存在,\n那么获取react合成事件, 一起压入 dispatchQueue] --> execDispatchQueue["遍历 dispatchQueue, 取出对应的 \n react合成事件, listeners \n 根据不同的触发阶段按照不同的顺序执行 listener \n 捕获阶段, 从前往后也就是 0 -> n \n 冒泡阶段, 从后往前也就是 n -> 0"] --> other[然后有经过一系列的包装函数, 最后顺利执行事件处理函数] --> bubble[又开始原生的冒泡事件] --> trigger accumulateSinglePhaseListenersDetial["accumulateSinglePhaseListeners \n 过程"] --> 1["获取当前的 reactEventName, 因为当前是 click 事件 \n 如果是捕获阶段, 那么值为 onClickCapture \n 如果是冒泡阶段, 那么值为 onClick"] --> 2[进入循环, 不断向上循环 Fiber] --> 3[循环中, 通过 reactEventName 取得\n当前 Fiber 的事件处理函数, 即 listener \n\n 如果有 listener 那么就\n push 到 listeners, 结构上面已经说了] --> 4[循环完成, 返回 listeners]

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

注意: listeners 中的顺序就是遍历的顺序, 也就是 子Fiber 到 根Fiber 的顺序压入, 所以才需要在执行的时候按照不同的顺序执行 listener

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

posted @ 2022-08-22 01:08  99xx  阅读(251)  评论(0)    收藏  举报