跳至主要內容

事件系统

Mr.LRH大约 30 分钟

事件系统

原生 DOM 事件

注册事件

  • 设置事件目标的事件属性。事件处理属性由 on + 事件名组成(比如:onchangeonloadonmouseover等)。

    window.onload = function () {
      const shippingAddressDOM = document.getElementById('shippingAddress')
      shippingAddressDOM.onsubmit = function () {
        return this
      }
    }
    
  • 设置 HTML 标签元素的事件属性。

    <button onclick="alert('Hello World!')"></button>
    

    当指定一串 JavaScript 代码作为 HTML 事件处理程序属性的值时,浏览器会把代码转换为类似如下的函数

    function (event) {
      with(document) {
        with(this.form || {}) {
          with(this) {
            /* ... 这里是编码 */
          }
        }
      }
    }
    
  • EventTarget.addEventListener():将指定的监听器注册到 EventTarget 上,当该对象触发指定的事件时,指定的回调函数就会被执行。事件目标可以是一个文档上的元素 ElementDocumentWindow,以及任何支持事件的对象(比如 XMLHttpRequest)。

    addEventListener(type, listener, useCapture)
    
    • type : 监听的事件类型(名称),大小写敏感。
    • listener : 监听函数。当所监听的事件类型触发时,会调用该监听函数。
    • useCapture : 可选,布尔值。true 表示监听函数将在捕获阶段(capture)触发,默认值为 false(监听函数只在冒泡阶段被触发)。

事件流

事件流分为 3 个阶段:

  • 捕获阶段(Capturing phase) :从 window 对象传导到目标节点。
  • 目标阶段(Target phase) :在目标节点上触发。
  • 冒泡阶段(Bubbling phase) :从目标节点传导回 window 对象(从底层传回上层)。并不是所有的事件都会冒泡,有些事件并不存在冒泡事件,如:blurfocusmouseenter

阻止事件冒泡:

  • event.stopPropagation() :如果当前元素上存在其他处理程序都会继续运行。同时,不能防止任何默认行为的发生,可以使用 event.preventDefault() 阻止事件触发后默认动作的发生。
  • event.stopImmediatePropagation() : 如果多个事件监听器被附加到相同元素的相同事件类型上,当此事件触发时,它们会按其被添加的顺序被调用。如果在其中一个事件监听器中执行 stopImmediatePropagation() ,那么剩下的事件监听器都不会被调用。

event.eventPhase 获取事件流当前处于的阶段,返回一个代表当前执行阶段的整数值。

  • 0 : 没有事件正在被处理
  • 1 : 表示处于捕获阶段(Capturing phase)
  • 2 : 表示处于目标阶段(Target phase)
  • 3 : 表示处于冒泡阶段(Bubbling phase)

事件委托(代理)

由于事件会在冒泡阶段向上传播到父节点,可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。

var ul = document.querySelector('ul')

ul.addEventListener('click', function (event) {
  if (event.target.tagName.toLowerCase() === 'li') {
    // do something ...
  }
})

React 合成事件(SyntheticEvent)

合成事件(SyntheticEvent):React 跨浏览器原生事件包装器。具有与浏览器原生事件相同的接口,包括 stopPropagation()preventDefault() ,除了事件在所有浏览器中他们工作方式都相同。

采用合成事件模式的好处:

  • 兼容性,跨平台。每个浏览器的内核都不相同,React 通过顶层事件代理机制,保证冒泡的统一性,抹平不同浏览器事件对象之间的差异,将不同平台的事件进行模拟成合成事件,使其能够跨浏览器执行。
  • 将所有事件统一管理。在原生事件中,所有的事件都绑定在对应的 DOM 上,如果页面复杂,绑定的事件会非常多,这样就会造成一些不可控的问题。
  • 避免垃圾回收。事件会被频繁的创建和回收,会影响性能。为了解决这个问题,React 引入事件池,通过事件池来获取和释放事件。所有的事件并不会被释放,而是存入到一个数组中,如果这个事件触发,则直接在这个数组中弹出即可,避免了频繁创建和销毁。

与原生事件的区别

  • React 事件使用驼峰命名,而不是全部小写(比如:onClick);原生事件使用纯小写命名(比如:onclick)。

  • 通过 JSX , 传递一个函数作为事件处理程序;原生事件传递的是字符串。

  • 事件源不同,阻止默认事件的方式不同。

    在 React 中,获取到的事件源(event)并非是真正的事件 event,而是经过 React 单独处理的 event

    • 在合成事件中,事件处理程序返回 false 将不再停止事件冒泡。 应该根据需要手动 e.stopPropagation()e.preventDefault()
    • 在原生事件中,可以通过 e.preventDefault()return false 来阻止默认事件。
  • 处理捕获阶段的注册事件。

    在 React 中,所有的绑定事件(如:onClickonChange)都是冒泡阶段执行。为捕获阶段注册事件处理程序,需要将 Capture 附加到事件名称中(如:onClickCapture)。

合成事件与原生事件的执行顺序

  • React 16.x : document 捕获 --> 原生事件捕获 --> 原生事件冒泡 --> React 合成事件捕获 --> React 合成事件冒泡 --> document 冒泡
  • React 17.x / 18.x : document 捕获 --> React 合成事件捕获 --> 原生事件捕获 --> 原生事件冒泡 --> React 合成事件冒泡 --> document 冒泡
function EventCompound() {
  useEffect(() => {
    const div = document.getElementById('div')
    const button = document.getElementById('button')

    div.addEventListener('click', () => console.log('原生冒泡:div元素'))
    button.addEventListener('click', () => console.log('原生冒泡:button元素'))

    div.addEventListener('click', () => console.log('原生捕获:div元素'), true)
    button.addEventListener(
      'click',
      () => console.log('原生捕获:button元素'),
      true
    )

    document.addEventListener('click', () => console.log('document元素冒泡'))
    document.addEventListener(
      'click',
      () => console.log('document元素捕获'),
      true
    )
  }, [])

  return (
    <div
      id="div"
      onClick={() => console.log('React冒泡:div元素')}
      onClickCapture={() => console.log('React捕获:div元素')}
    >
      <button
        id="button"
        onClick={() => console.log('React冒泡:button元素')}
        onClickCapture={() => console.log('React捕获:button元素')}
      >
        执行顺序 v16/v17/v18
      </button>
    </div>
  )
}

React 16 事件系统

事件池

注意

事件池仅适用于 React 16 及更早版本,Web 端的 React 17+ 不再使用事件池。

事件池简介

React 合成事件(SyntheticEvent)对象会被放入事件池中统一管理,不同类型的合成事件对应不同的事件池。

合成事件(SyntheticEvent)对象可以被复用,当所有事件处理函数被调用之后,其所有属性都会被置空。如果需要在事件处理函数运行之后获取事件对象的属性,需要调用 e.persist()

function ExampleEventCompound() {
  const handleChange = event => {
    event.persist()

    console.log('input value', event.target.value)
    setTimeout(() => {
      /* 如果未调用 e.persist() ,会提示警告,event 属性被设置为空。 */
      console.log('setTimeout input value', event.target.value)
    }, 100)
  }

  return <input onChange={handleChange} />
}

事件池分析

在点击事件中,实际上会调用 SimpleEventPluginextractEvents 方法,返回 event。其中,event 通过调用 EventConstructor.getPooled 方法生成。

SimpleEventPlugin
// packages\react-dom\src\events\SimpleEventPlugin.js

const SimpleEventPlugin: PluginModule<MouseEvent> = {
  // simpleEventPluginEventTypes gets populated from
  // the DOMEventProperties module.
  eventTypes: simpleEventPluginEventTypes,
  extractEvents: function (
    topLevelType: TopLevelType,
    targetInst: null | Fiber,
    nativeEvent: MouseEvent,
    nativeEventTarget: null | EventTarget,
    eventSystemFlags: EventSystemFlags
  ): null | ReactSyntheticEvent {
    const dispatchConfig = topLevelEventsToDispatchConfig.get(topLevelType)
    if (!dispatchConfig) {
      return null
    }
    let EventConstructor
    switch (topLevelType) {
      case DOMTopLevelEventTypes.TOP_KEY_PRESS:
        // Firefox creates a keypress event for function keys too. This removes
        // the unwanted keypress events. Enter is however both printable and
        // non-printable. One would expect Tab to be as well (but it isn't).
        if (getEventCharCode(nativeEvent) === 0) {
          return null
        }
      /* falls through */
      case DOMTopLevelEventTypes.TOP_KEY_DOWN:
      case DOMTopLevelEventTypes.TOP_KEY_UP:
        EventConstructor = SyntheticKeyboardEvent
        break
      case DOMTopLevelEventTypes.TOP_BLUR:
      case DOMTopLevelEventTypes.TOP_FOCUS:
        EventConstructor = SyntheticFocusEvent
        break
      case DOMTopLevelEventTypes.TOP_CLICK:
        // Firefox creates a click event on right mouse clicks. This removes the
        // unwanted click events.
        if (nativeEvent.button === 2) {
          return null
        }
      /* falls through */
      case DOMTopLevelEventTypes.TOP_AUX_CLICK:
      case DOMTopLevelEventTypes.TOP_DOUBLE_CLICK:
      case DOMTopLevelEventTypes.TOP_MOUSE_DOWN:
      case DOMTopLevelEventTypes.TOP_MOUSE_MOVE:
      case DOMTopLevelEventTypes.TOP_MOUSE_UP:
      // TODO: Disabled elements should not respond to mouse events
      /* falls through */
      case DOMTopLevelEventTypes.TOP_MOUSE_OUT:
      case DOMTopLevelEventTypes.TOP_MOUSE_OVER:
      case DOMTopLevelEventTypes.TOP_CONTEXT_MENU:
        EventConstructor = SyntheticMouseEvent
        break
      case DOMTopLevelEventTypes.TOP_DRAG:
      case DOMTopLevelEventTypes.TOP_DRAG_END:
      case DOMTopLevelEventTypes.TOP_DRAG_ENTER:
      case DOMTopLevelEventTypes.TOP_DRAG_EXIT:
      case DOMTopLevelEventTypes.TOP_DRAG_LEAVE:
      case DOMTopLevelEventTypes.TOP_DRAG_OVER:
      case DOMTopLevelEventTypes.TOP_DRAG_START:
      case DOMTopLevelEventTypes.TOP_DROP:
        EventConstructor = SyntheticDragEvent
        break
      case DOMTopLevelEventTypes.TOP_TOUCH_CANCEL:
      case DOMTopLevelEventTypes.TOP_TOUCH_END:
      case DOMTopLevelEventTypes.TOP_TOUCH_MOVE:
      case DOMTopLevelEventTypes.TOP_TOUCH_START:
        EventConstructor = SyntheticTouchEvent
        break
      case DOMTopLevelEventTypes.TOP_ANIMATION_END:
      case DOMTopLevelEventTypes.TOP_ANIMATION_ITERATION:
      case DOMTopLevelEventTypes.TOP_ANIMATION_START:
        EventConstructor = SyntheticAnimationEvent
        break
      case DOMTopLevelEventTypes.TOP_TRANSITION_END:
        EventConstructor = SyntheticTransitionEvent
        break
      case DOMTopLevelEventTypes.TOP_SCROLL:
        EventConstructor = SyntheticUIEvent
        break
      case DOMTopLevelEventTypes.TOP_WHEEL:
        EventConstructor = SyntheticWheelEvent
        break
      case DOMTopLevelEventTypes.TOP_COPY:
      case DOMTopLevelEventTypes.TOP_CUT:
      case DOMTopLevelEventTypes.TOP_PASTE:
        EventConstructor = SyntheticClipboardEvent
        break
      case DOMTopLevelEventTypes.TOP_GOT_POINTER_CAPTURE:
      case DOMTopLevelEventTypes.TOP_LOST_POINTER_CAPTURE:
      case DOMTopLevelEventTypes.TOP_POINTER_CANCEL:
      case DOMTopLevelEventTypes.TOP_POINTER_DOWN:
      case DOMTopLevelEventTypes.TOP_POINTER_MOVE:
      case DOMTopLevelEventTypes.TOP_POINTER_OUT:
      case DOMTopLevelEventTypes.TOP_POINTER_OVER:
      case DOMTopLevelEventTypes.TOP_POINTER_UP:
        EventConstructor = SyntheticPointerEvent
        break
      default:
        // HTML Events
        // @see http://www.w3.org/TR/html5/index.html#events-0
        EventConstructor = SyntheticEvent
        break
    }
    const event = EventConstructor.getPooled(
      dispatchConfig,
      targetInst,
      nativeEvent,
      nativeEventTarget
    )
    accumulateTwoPhaseDispatches(event)
    return event
  },
}

export default SimpleEventPlugin

EventConstructor.getPooled 实际调用为 SyntheticEvent 中的 getPooledEvent 方法,当 EventConstructor.eventPool 存在时会复用事件对象,否则会创建新的对象。

getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) 方法中:

  • dispatchConfig :该参数将事件对应的 React 元素实例、原生事件、原生事件对应的 DOM 封装成了一个合成事件。比如,冒泡事件中的 onClick 和捕获事件中的 onClickCapture
  • targetInst :组件的实例,通过 e.target(事件源) 得到对应的 ReactDomComponent
  • nativeEvent :对应原生事件对象
  • nativeInst :原生的事件源
// packages\legacy-events\SyntheticEvent.js

function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
  const EventConstructor = this
  if (EventConstructor.eventPool.length) {
    const instance = EventConstructor.eventPool.pop()
    EventConstructor.call(
      instance,
      dispatchConfig,
      targetInst,
      nativeEvent,
      nativeInst
    )
    return instance
  }
  return new EventConstructor(
    dispatchConfig,
    targetInst,
    nativeEvent,
    nativeInst
  )
}

事件池填充合成事件对象时,会调用 SyntheticEvent 中的 releasePooledEvent 方法。会执行 event.destructor() 方法重置 event 的部分属性,如果事件池没有满,则会填充进去。

// packages\legacy-events\SyntheticEvent.js

const EVENT_POOL_SIZE = 10

function releasePooledEvent(event) {
  const EventConstructor = this
  invariant(
    event instanceof EventConstructor,
    'Trying to release an event instance into a pool of a different type.'
  )
  event.destructor()
  if (EventConstructor.eventPool.length < EVENT_POOL_SIZE) {
    EventConstructor.eventPool.push(event)
  }
}

事件插件初始化

React 将合成事件与原生事件的对应关系存放在 React 事件插件(EventPlugin)中。事件插件可以认为是 React 将不同的合成事件处理函数封装成了一个模块,每个模块处理对应的合成事件。例如:

  • onClick 事件通过 SimpleEventPlugin 插件进行处理
  • onChange 事件通过 ChangeEventPlugin 插件进行处理
  • onMouseEnteronMouseLeave 通过 EnterLeaveEventPlugin 插件进行处理
  • ......

注册事件插件

为处理合成事件与原生事件的对应关系,React 采用了初始化注册事件插件的方式。通过执行 EventPluginRegistry 中的 injectEventPluginsByName() 方法进行相关事件插件注册。

// packages\react-dom\src\client\ReactDOMClientInjection.js

/**
 * Some important event plugins included by default (without having to require
 * them).
 */
injectEventPluginsByName({
  SimpleEventPlugin: SimpleEventPlugin,
  EnterLeaveEventPlugin: EnterLeaveEventPlugin,
  ChangeEventPlugin: ChangeEventPlugin,
  SelectEventPlugin: SelectEventPlugin,
  BeforeInputEventPlugin: BeforeInputEventPlugin,
})
  • 执行 EventPluginRegistry 中的 injectEventPluginsByName() 函数生成 namesToPlugins,然后执行 recomputePluginOrdering。生成的 namesToPlugins 如下:

    const namesToPlugins = {
      SimpleEventPlugin,
      EnterLeaveEventPlugin,
      ChangeEventPlugin,
      SelectEventPlugin,
      BeforeInputEventPlugin,
    }
    
    injectEventPluginsByName(injectedNamesToPlugins) 函数
    // packages\legacy-events\EventPluginRegistry.js
    
    export function injectEventPluginsByName(
      injectedNamesToPlugins: NamesToPlugins
    ): void {
      let isOrderingDirty = false
      for (const pluginName in injectedNamesToPlugins) {
        if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) {
          continue
        }
        const pluginModule = injectedNamesToPlugins[pluginName]
        if (
          !namesToPlugins.hasOwnProperty(pluginName) ||
          namesToPlugins[pluginName] !== pluginModule
        ) {
          invariant(
            !namesToPlugins[pluginName],
            'EventPluginRegistry: Cannot inject two different event plugins ' +
              'using the same name, `%s`.',
            pluginName
          )
          namesToPlugins[pluginName] = pluginModule
          isOrderingDirty = true
        }
      }
      if (isOrderingDirty) {
        recomputePluginOrdering()
      }
    }
    
  • 执行 recomputePluginOrdering() 函数生成 plugins 数组(存储注册的所有插件列表,初始化为空),然后执行 publishEventForPlugin。生成的 plugins 如下:

    const plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];
    
    recomputePluginOrdering 函数
    // packages\legacy-events\EventPluginRegistry.js
    
    function recomputePluginOrdering(): void {
      if (!eventPluginOrder) {
        // Wait until an `eventPluginOrder` is injected.
        return
      }
      for (const pluginName in namesToPlugins) {
        const pluginModule = namesToPlugins[pluginName]
        const pluginIndex = eventPluginOrder.indexOf(pluginName)
        invariant(
          pluginIndex > -1,
          'EventPluginRegistry: Cannot inject event plugins that do not exist in ' +
            'the plugin ordering, `%s`.',
          pluginName
        )
        if (plugins[pluginIndex]) {
          continue
        }
        invariant(
          pluginModule.extractEvents,
          'EventPluginRegistry: Event plugins must implement an `extractEvents` ' +
            'method, but `%s` does not.',
          pluginName
        )
        plugins[pluginIndex] = pluginModule
        const publishedEvents = pluginModule.eventTypes
        for (const eventName in publishedEvents) {
          invariant(
            publishEventForPlugin(
              publishedEvents[eventName],
              pluginModule,
              eventName
            ),
            'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.',
            eventName,
            pluginName
          )
        }
      }
    }
    
  • 执行 publishEventForPlugin() 函数生成 registrationNameModules对象(React 合成事件与对应事件插件的映射关系) 和 registrationNameDependencies(React 合成事件到原生事件的映射关系)。

    publishEventForPlugin(dispatchConfig, pluginModule, eventName) 函数
    // packages\legacy-events\EventPluginRegistry.js
    
    function publishEventForPlugin(
      dispatchConfig: DispatchConfig,
      pluginModule: PluginModule<AnyNativeEvent>,
      eventName: string
    ): boolean {
      invariant(
        !eventNameDispatchConfigs.hasOwnProperty(eventName),
        'EventPluginRegistry: More than one plugin attempted to publish the same ' +
          'event name, `%s`.',
        eventName
      )
      eventNameDispatchConfigs[eventName] = dispatchConfig
    
      const phasedRegistrationNames = dispatchConfig.phasedRegistrationNames
      if (phasedRegistrationNames) {
        for (const phaseName in phasedRegistrationNames) {
          if (phasedRegistrationNames.hasOwnProperty(phaseName)) {
            const phasedRegistrationName = phasedRegistrationNames[phaseName]
            publishRegistrationName(
              phasedRegistrationName,
              pluginModule,
              eventName
            )
          }
        }
        return true
      } else if (dispatchConfig.registrationName) {
        publishRegistrationName(
          dispatchConfig.registrationName,
          pluginModule,
          eventName
        )
        return true
      }
      return false
    }
    

EventPluginRegistry 中,定义的全局对象:

  • registrationNameModule 全局对象:用于存储 React 合成事件与对应事件插件的映射关系。包含了 React 所支持的所有事件类型,在处理原生组件的 props 时,可用于判断一个组件的 prop 是否为事件类型,会根据不同的事件名称,找到对应的事件插件,然后统一绑定在 document 上。

    {
      onBlur: SimpleEventPlugin,
      onClick: SimpleEventPlugin,
      onClickCapture: SimpleEventPlugin,
      onChange: ChangeEventPlugin,
      onChangeCapture: ChangeEventPlugin,
      onMouseEnter: EnterLeaveEventPlugin,
      onMouseLeave: EnterLeaveEventPlugin,
      // ...
    }
    
  • registrationNameDependencies 全局对象:用于存储 React 合成事件到原生事件的映射关系。

    {
      onBlur: ['blur'],
      onClick: ['click'],
      onClickCapture: ['click'],
      onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
      onMouseEnter: ['mouseout', 'mouseover'],
      onMouseLeave: ['mouseout', 'mouseover'],
      // ...
    }
    
  • plugins 全局对象:用于存储注册的所有插件列表,初始化为空。

    const plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];
    

事件插件结构

export type DispatchConfig = {|
  // 依赖的原生事件,即与之相关联的原生事件。注意,大多数事件一般只对应一个,复杂的事件会对应多个(如:onChange)
  dependencies: Array<TopLevelType>,
  // 对应的事件名称,React 会根据该参数查找对应的事件类型。
  phasedRegistrationNames?: {|
    bubbled: string, // 对应冒泡阶段
    captured: string, // 对应捕获阶段
  |},
  // props事件注册名称,并不是所有的事件都具有冒泡事件的(比如:onMouseEnter),如果不支持冒泡,只会有 registrationName,而不会有 phasedRegistrationNames
  registrationName?: string,
  eventPriority: EventPriority, // 用来处理事件的优先级
|}

export type EventTypes = { [key: string]: DispatchConfig, ... }

export type PluginModule<NativeEvent> = {
  eventTypes: EventTypes, // 声明插件的事件类型
  // 事件进行处理的参数
  extractEvents: (
    topLevelType: TopLevelType,
    targetInst: null | Fiber,
    nativeTarget: NativeEvent,
    nativeEventTarget: null | EventTarget,
    eventSystemFlags: EventSystemFlags,
    container?: Document | Element | Node
  ) => ?ReactSyntheticEvent,
  tapMoveThreshold?: number,
}

事件绑定流程

事件绑定处理逻辑

  • diffProperties 处理 React 合成事件

    • 在 React 的调和过程(Reconcilliation)中,通过 JSX 编译转换为 React Element 对象的每一个子节点都会形成一个与之对应的 fiber 对象。事件最终保存在 fiber 中的 memoizedPropspendingProps 中。

    • React 在调合子节点后,进入 diff 阶段,会用 diff props 函数 diffProperties 单独处理。如果发现是 React 合成事件就会调用 legacyListenToEvent 函数,注册事件监听器。

      diffProperties(domElement, tag, lastRawProps, nextRawProps, rootContainerElement) 函数
      // packages\react-dom\src\client\ReactDOMComponent.js
      
      function ensureListeningTo(
        rootContainerElement: Element | Node,
        registrationName: string
      ): void {
        const isDocumentOrFragment =
          rootContainerElement.nodeType === DOCUMENT_NODE ||
          rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE
        const doc = isDocumentOrFragment
          ? rootContainerElement
          : rootContainerElement.ownerDocument
        legacyListenToEvent(registrationName, doc)
      }
      
      // Calculate the diff between the two objects.
      export function diffProperties(
        domElement: Element,
        tag: string,
        lastRawProps: Object,
        nextRawProps: Object,
        rootContainerElement: Element | Document
      ): null | Array<mixed> {
        // ......
        for (propKey in nextProps) {
          const nextProp = nextProps[propKey]
          const lastProp = lastProps != null ? lastProps[propKey] : undefined
      
          //......
      
          if (registrationNameModules.hasOwnProperty(propKey)) {
            if (nextProp != null) {
              ensureListeningTo(rootContainerElement, propKey)
            }
            if (!updatePayload && lastProp !== nextProp) {
              // This is a special case. If any listener updates we need to ensure
              // that the "current" props pointer gets updated so we need a commit
              // to update this element.
              updatePayload = []
            }
          }
      
          // ......
        }
      
        // ......
      }
      
  • legacyListenToEvent 注册事件监听器。

    legacyListenToEvent 函数中,

    • 首先,获取 React 合成事件对应的原生事件集合。比如:onClick 对应 [click]onChange 对应 [blur , change , input , keydown , keyup]
    • 然后,遍历依赖项的数组,调用 legacyListenToTopLevelEvent 绑定事件。
      • 基础事件(比如:click 等),会默认按照事件冒泡处理(调用 trapBubbledEvent 函数处理)。
      • 特殊事件(比如:scrollfocusblur 等),会按照事件捕获处理(调用 trapCapturedEvent 函数处理)。
    legacyListenToEvent(registrationName, mountAt) 函数
    export function legacyListenToEvent(
      registrationName: string, // 合成事件名
      mountAt: Document | Element | Node
    ): void {
      const listenerMap = getListenerMapForElement(mountAt)
      const dependencies = registrationNameDependencies[registrationName]
    
      for (let i = 0; i < dependencies.length; i++) {
        const dependency = dependencies[i] // 合成事件所依赖的事件组
        legacyListenToTopLevelEvent(dependency, mountAt, listenerMap)
      }
    }
    
    export function legacyListenToTopLevelEvent(
      topLevelType: DOMTopLevelEventType,
      mountAt: Document | Element | Node,
      listenerMap: Map<DOMTopLevelEventType | string, null | (any => void)>
    ): void {
      if (!listenerMap.has(topLevelType)) {
        switch (topLevelType) {
          case TOP_SCROLL:
            trapCapturedEvent(TOP_SCROLL, mountAt)
            break
          case TOP_FOCUS:
          case TOP_BLUR:
            trapCapturedEvent(TOP_FOCUS, mountAt)
            trapCapturedEvent(TOP_BLUR, mountAt)
            // We set the flag for a single dependency later in this function,
            // but this ensures we mark both as attached rather than just one.
            listenerMap.set(TOP_BLUR, null)
            listenerMap.set(TOP_FOCUS, null)
            break
          case TOP_CANCEL:
          case TOP_CLOSE:
            if (isEventSupported(getRawEventName(topLevelType))) {
              trapCapturedEvent(topLevelType, mountAt)
            }
            break
          case TOP_INVALID:
          case TOP_SUBMIT:
          case TOP_RESET:
            // We listen to them on the target DOM elements.
            // Some of them bubble so we don't want them to fire twice.
            break
          default:
            // By default, listen on the top level to all non-media events.
            // Media events don't bubble so adding the listener wouldn't do anything.
            const isMediaEvent = mediaEventTypes.indexOf(topLevelType) !== -1
            if (!isMediaEvent) {
              trapBubbledEvent(topLevelType, mountAt)
            }
            break
        }
        listenerMap.set(topLevelType, null)
      }
    }
    
    trapBubbledEvent(topLevelType, element) 函数 / trapCapturedEvent(topLevelType, element) 函数
    // packages\react-dom\src\events\ReactDOMEventListener.js
    
    export function trapBubbledEvent(
      topLevelType: DOMTopLevelEventType,
      element: Document | Element | Node
    ): void {
      trapEventForPluginEventSystem(element, topLevelType, false)
    }
    
    export function trapCapturedEvent(
      topLevelType: DOMTopLevelEventType,
      element: Document | Element | Node
    ): void {
      trapEventForPluginEventSystem(element, topLevelType, true)
    }
    
  • 绑定事件统一处理函数 dispatchEvent,进行事件监听。

    对合成事件进行冒泡/捕获处理,会调用 trapEventForPluginEventSystem() 函数。在函数中:

    • 判断事件类型,并绑定事件统一处理函数 dispatchEvent,对 listerner 进行赋值。
    • 然后,判断是否为捕获,调用 addEventCaptureListener(container, rawEventName, listener)addEventBubbleListener(container, rawEventName, listener) 进行事件绑定。

    实际上,所有的事件都绑定到 document 容器上。

    trapEventForPluginEventSystem(container, topLevelType, capture) 函数
    // packages\react-dom\src\events\ReactDOMEventListener.js
    
    function trapEventForPluginEventSystem(
      container: Document | Element | Node,
      topLevelType: DOMTopLevelEventType,
      capture: boolean
    ): void {
      let listener
      switch (getEventPriorityForPluginSystem(topLevelType)) {
        case DiscreteEvent:
          listener = dispatchDiscreteEvent.bind(
            null,
            topLevelType,
            PLUGIN_EVENT_SYSTEM,
            container
          )
          break
        case UserBlockingEvent:
          listener = dispatchUserBlockingUpdate.bind(
            null,
            topLevelType,
            PLUGIN_EVENT_SYSTEM,
            container
          )
          break
        case ContinuousEvent:
        default:
          listener = dispatchEvent.bind(
            null,
            topLevelType,
            PLUGIN_EVENT_SYSTEM,
            container
          )
          break
      }
    
      const rawEventName = getRawEventName(topLevelType)
      if (capture) {
        addEventCaptureListener(container, rawEventName, listener)
      } else {
        addEventBubbleListener(container, rawEventName, listener)
      }
    }
    
    // packages\react-dom\src\events\EventListener.js
    
    export function addEventBubbleListener(
      element: Document | Element | Node,
      eventType: string,
      listener: Function
    ): void {
      element.addEventListener(eventType, listener, false)
    }
    
    export function addEventCaptureListener(
      element: Document | Element | Node,
      eventType: string,
      listener: Function
    ): void {
      element.addEventListener(eventType, listener, true)
    }
    

事件绑定流程总结

  • 在 React 中,将元素转换为与之对应的 fiber 对象,如果发现 props 是合成事件(如:onClick),则会按照事件系统逻辑进行处理
  • 根据 React 合成事件类型,获取到对应的原生事件类型。比如:onClick 对应 [click]onChange 对应 [blur , change , input , keydown , keyup]
  • 判断原生事件类型,进行事件冒泡/事件捕获逻辑处理。大部分事件(如:onClick)都按照冒泡逻辑处理,少数事件(如:scroll)会按照捕获逻辑处理。
  • 调用 trapEventForPluginEventSystem() 函数进行事件绑定。事件绑定在 document 上,并绑定事件统一处理函数 dispatchEvent

注:并不是捕获事件就会走捕获的阶段(如:onClickCapture),实际上,onClickCaptureonClick 一样,都是走的冒泡阶段。onScrollonBluronFocus等在事件捕获阶段发生的。

事件触发流程

事件触发处理逻辑

  • dispatchEvent 处理事件触发

    React 进行事件绑定的时候,会绑定事件统一处理函数 dispatchEvent,进行事件监听。当触发事件之后,会执行 dispatchEvent 函数。其中,会调用 attemptToDispatchEvent() 函数调度事件。

    • 首先,根据事件源(即:nativeEvent),调用 const nativeEventTarget = getEventTarget(nativeEvent) 找到真实 DOM 元素(即:nativeEventTarget
    • 然后,调用 let targetInst = getClosestInstanceFromNode(nativeEventTarget) ,根据该 DOM 元素,找到与之对应的 fiber ,并赋值给 targetInst。React 在初始化真实 DOM 时,会使用一个随机的 key internalInstanceKey 指针指向当前 DOM 对应的 fiber 对象,fiber 对象用 stateNode 指向了当前的 DOM 元素。
    • 最后,执行 dispatchEventForLegacyPluginEventSystem 函数,进入 legacy 模式的事件处理函数系统
    dispatchEvent(topLevelType, eventSystemFlags, container, nativeEvent) 函数
    // packages\react-dom\src\events\ReactDOMEventListener.js
    
    export function dispatchEvent(
      topLevelType: DOMTopLevelEventType,
      eventSystemFlags: EventSystemFlags,
      container: Document | Element | Node,
      nativeEvent: AnyNativeEvent
    ): void {
      if (!_enabled) {
        return
      }
      if (hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(topLevelType)) {
        // If we already have a queue of discrete events, and this is another discrete
        // event, then we can't dispatch it regardless of its target, since they
        // need to dispatch in order.
        queueDiscreteEvent(
          null, // Flags that we're not actually blocked on anything as far as we know.
          topLevelType,
          eventSystemFlags,
          container,
          nativeEvent
        )
        return
      }
    
      const blockedOn = attemptToDispatchEvent(
        topLevelType,
        eventSystemFlags,
        container,
        nativeEvent
      )
    
      if (blockedOn === null) {
        // We successfully dispatched this event.
        clearIfContinuousEvent(topLevelType, nativeEvent)
        return
      }
    
      if (isReplayableDiscreteEvent(topLevelType)) {
        // This this to be replayed later once the target is available.
        queueDiscreteEvent(
          blockedOn,
          topLevelType,
          eventSystemFlags,
          container,
          nativeEvent
        )
        return
      }
    
      if (
        queueIfContinuousEvent(
          blockedOn,
          topLevelType,
          eventSystemFlags,
          container,
          nativeEvent
        )
      ) {
        return
      }
    
      // We need to clear only if we didn't queue because
      // queueing is accummulative.
      clearIfContinuousEvent(topLevelType, nativeEvent)
    
      // This is not replayable so we'll invoke it but without a target,
      // in case the event system needs to trace it.
      if (enableDeprecatedFlareAPI) {
        if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) {
          dispatchEventForLegacyPluginEventSystem(
            topLevelType,
            eventSystemFlags,
            nativeEvent,
            null
          )
        }
        if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) {
          // React Flare event system
          DEPRECATED_dispatchEventForResponderEventSystem(
            (topLevelType: any),
            null,
            nativeEvent,
            getEventTarget(nativeEvent),
            eventSystemFlags
          )
        }
      } else {
        dispatchEventForLegacyPluginEventSystem(
          topLevelType,
          eventSystemFlags,
          nativeEvent,
          null
        )
      }
    }
    
    // Attempt dispatching an event. Returns a SuspenseInstance or Container if it's blocked.
    export function attemptToDispatchEvent(
      topLevelType: DOMTopLevelEventType,
      eventSystemFlags: EventSystemFlags,
      container: Document | Element | Node,
      nativeEvent: AnyNativeEvent
    ): null | Container | SuspenseInstance {
      // TODO: Warn if _enabled is false.
    
      const nativeEventTarget = getEventTarget(nativeEvent)
      let targetInst = getClosestInstanceFromNode(nativeEventTarget)
    
      if (targetInst !== null) {
        let nearestMounted = getNearestMountedFiber(targetInst)
        if (nearestMounted === null) {
          // This tree has been unmounted already. Dispatch without a target.
          targetInst = null
        } else {
          const tag = nearestMounted.tag
          if (tag === SuspenseComponent) {
            let instance = getSuspenseInstanceFromFiber(nearestMounted)
            if (instance !== null) {
              // Queue the event to be replayed later. Abort dispatching since we
              // don't want this event dispatched twice through the event system.
              // TODO: If this is the first discrete event in the queue. Schedule an increased
              // priority for this boundary.
              return instance
            }
            // This shouldn't happen, something went wrong but to avoid blocking
            // the whole system, dispatch the event without a target.
            // TODO: Warn.
            targetInst = null
          } else if (tag === HostRoot) {
            const root: FiberRoot = nearestMounted.stateNode
            if (root.hydrate) {
              // If this happens during a replay something went wrong and it might block
              // the whole system.
              return getContainerFromFiber(nearestMounted)
            }
            targetInst = null
          } else if (nearestMounted !== targetInst) {
            // If we get an event (ex: img onload) before committing that
            // component's mount, ignore it for now (that is, treat it as if it was an
            // event on a non-React tree). We might also consider queueing events and
            // dispatching them after the mount.
            targetInst = null
          }
        }
      }
    
      if (enableDeprecatedFlareAPI) {
        if (eventSystemFlags & PLUGIN_EVENT_SYSTEM) {
          dispatchEventForLegacyPluginEventSystem(
            topLevelType,
            eventSystemFlags,
            nativeEvent,
            targetInst
          )
        }
        if (eventSystemFlags & RESPONDER_EVENT_SYSTEM) {
          // React Flare event system
          DEPRECATED_dispatchEventForResponderEventSystem(
            (topLevelType: any),
            targetInst,
            nativeEvent,
            nativeEventTarget,
            eventSystemFlags
          )
        }
      } else {
        dispatchEventForLegacyPluginEventSystem(
          topLevelType,
          eventSystemFlags,
          nativeEvent,
          targetInst
        )
      }
      // We're not blocked on anything.
      return null
    }
    
  • legacy 事件处理系统与批量更新

    dispatchEventForLegacyPluginEventSystem 函数中:

    • 首先,根据 getTopLevelCallbackBookKeeping 函数找到事件池中对应的属性,将 topLevelTypetargetInst 等属性赋予给事件。

    • 然后,通过 batchedEventUpdates 函数处理批量更新。

      实际上,React 是通过 isBatchingEventUpdates 控制是否进行批量更新。

      batchedEventUpdates 会打开批量渲染开关(isBatchingEventUpdates)并调用 handleTopLevel 执行事件处理插件。

      事件的执行是在 handleTopLevel(bookKeeping) 中执行的。

      batchedEventUpdates(fn, a, b) 函数
      // packages\legacy-events\ReactGenericBatching.js
      
      export function batchedEventUpdates(fn, a, b) {
        if (isBatchingEventUpdates) {
          // If we are currently inside another batch, we need to wait until it
          // fully completes before restoring state.
          return fn(a, b)
        }
        isBatchingEventUpdates = true
        try {
          return batchedEventUpdatesImpl(fn, a, b)
        } finally {
          isBatchingEventUpdates = false
          finishEventHandler()
        }
      }
      

      在函数中触发 setStateisBatchingEventUpdatestrue,则具备了批量更新的能力。

      export default class ExampleComponent extends React.Component {
        state = {
          count: 0,
        }
      
        handleClick = () => {
          this.setState({ count: this.state.count + 1 })
          console.log(this.state.count) // 0
      
          setTimeout(() => {
            this.setState({ count: this.state.count + 1 })
            console.log(this.state.count) // 2
          })
        }
      
        render() {
          return <button onClick={this.handleClick}>点击</button>
        }
      }
      

      在示例中:

      • 执行第一个 setState :符合批量更新的条件,isBatchingEventUpdatestrue ,则打印的值不是最新数值(即:setState 为异步)。
      • 执行第二个 setState :在 eventLoop 事件循环中,setTimeout 在下一次事件循环中执行,此时 isBatchingEventUpdatesfalse ,则能获取到最新值,打印为最新数值。
    • 最终,通过 releaseTopLevelCallbackBookKeeping 来释放事件池。

    dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, targetInst) 函数
    // packages\react-dom\src\events\DOMLegacyEventPluginSystem.js
    
    export function dispatchEventForLegacyPluginEventSystem(
      topLevelType: DOMTopLevelEventType,
      eventSystemFlags: EventSystemFlags,
      nativeEvent: AnyNativeEvent,
      targetInst: null | Fiber
    ): void {
      const bookKeeping = getTopLevelCallbackBookKeeping(
        topLevelType,
        nativeEvent,
        targetInst,
        eventSystemFlags
      )
    
      try {
        // Event queue being processed in the same cycle allows
        // `preventDefault`.
        batchedEventUpdates(handleTopLevel, bookKeeping)
      } finally {
        releaseTopLevelCallbackBookKeeping(bookKeeping)
      }
    }
    
  • 执行事件插件函数

    在进行事件处理系统与批量更新中,调用 handleTopLevel 函数主要是获取对应事件处理插件,比如 onClick 对应 SimpleEventPlugin,调用流程:

    handleTopLevel --> runExtractedPluginEventsInBatch --> extractPluginEvents --> runEventsInBatch

    获取事件处理插件后,调用事件插件的处理函数 extractEvents 。通过 extractEvents 可以不需要考虑浏览器兼容问题,交给 React 底层统一处理。

    handleTopLevel(bookKeeping) 函数
    // packages\react-dom\src\events\DOMLegacyEventPluginSystem.js
    
    function extractPluginEvents(
      topLevelType: TopLevelType,
      targetInst: null | Fiber,
      nativeEvent: AnyNativeEvent,
      nativeEventTarget: null | EventTarget,
      eventSystemFlags: EventSystemFlags
    ): Array<ReactSyntheticEvent> | ReactSyntheticEvent | null {
      let events = null
      for (let i = 0; i < plugins.length; i++) {
        // Not every plugin in the ordering may be loaded at runtime.
        const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i]
        if (possiblePlugin) {
          const extractedEvents = possiblePlugin.extractEvents(
            topLevelType,
            targetInst,
            nativeEvent,
            nativeEventTarget,
            eventSystemFlags
          )
          if (extractedEvents) {
            events = accumulateInto(events, extractedEvents)
          }
        }
      }
      return events
    }
    
    function runExtractedPluginEventsInBatch(
      topLevelType: TopLevelType,
      targetInst: null | Fiber,
      nativeEvent: AnyNativeEvent,
      nativeEventTarget: null | EventTarget,
      eventSystemFlags: EventSystemFlags
    ) {
      const events = extractPluginEvents(
        topLevelType,
        targetInst,
        nativeEvent,
        nativeEventTarget,
        eventSystemFlags
      )
      runEventsInBatch(events)
    }
    
    function handleTopLevel(bookKeeping: BookKeepingInstance) {
      let targetInst = bookKeeping.targetInst
    
      // Loop through the hierarchy, in case there's any nested components.
      // It's important that we build the array of ancestors before calling any
      // event handlers, because event handlers can modify the DOM, leading to
      // inconsistencies with ReactMount's node cache. See #1105.
      let ancestor = targetInst
      do {
        if (!ancestor) {
          const ancestors = bookKeeping.ancestors
          ;((ancestors: any): Array<Fiber | null>).push(ancestor)
          break
        }
        const root = findRootContainerNode(ancestor)
        if (!root) {
          break
        }
        const tag = ancestor.tag
        if (tag === HostComponent || tag === HostText) {
          bookKeeping.ancestors.push(ancestor)
        }
        ancestor = getClosestInstanceFromNode(root)
      } while (ancestor)
    
      for (let i = 0; i < bookKeeping.ancestors.length; i++) {
        targetInst = bookKeeping.ancestors[i]
        const eventTarget = getEventTarget(bookKeeping.nativeEvent)
        const topLevelType =
          ((bookKeeping.topLevelType: any): DOMTopLevelEventType)
        const nativeEvent = ((bookKeeping.nativeEvent: any): AnyNativeEvent)
        let eventSystemFlags = bookKeeping.eventSystemFlags
    
        // If this is the first ancestor, we mark it on the system flags
        if (i === 0) {
          eventSystemFlags |= IS_FIRST_ANCESTOR
        }
    
        runExtractedPluginEventsInBatch(
          topLevelType,
          targetInst,
          nativeEvent,
          eventTarget,
          eventSystemFlags
        )
      }
    }
    
  • extractEvents 形成事件对象 event 和事件处理函数队列

    • 首先,形成 React 合成事件源对象,保存整个事件的信息。将作为参数传递给真正的事件处理函数。
    • 然后,声明事件执行队列,按照冒泡和捕获逻辑,从事件源开始逐渐向上,查找子节点与之对应的 fiber 对象 ,收集上面的 React 合成事件(例如: onClick / onClickCapture)。
      • 对于冒泡阶段的事件(如:onClick),将 push 到执行队列后面
      • 对于捕获阶段的事件(如:onClickCapture),将 unShift 到执行队列的前面。
    • 最后,将事件执行队列,保存到 React 事件源对象上,等待执行。
    export default class ExampleComponent extends React.Component {
      handleDivClick = () => console.log('React冒泡:div元素')
      handleDivClickCapture = () => console.log('React捕获:div元素')
      handleButtonClick = () => console.log('React冒泡:button元素')
      handleButtonClickCapture = () => console.log('React捕获:button元素')
    
      render() {
        return (
          <div
            id="div"
            onClick={this.handleDivClick}
            onClickCapture={this.handleDivClickCapture}
          >
            <button
              id="button"
              onClick={this.handleButtonClick}
              onClickCapture={this.handleButtonClickCapture}
            >
              按钮
            </button>
          </div>
        )
      }
    }
    
    // 输出为:
    // React捕获:div元素
    // React捕获:button元素
    // React冒泡:button元素
    // React冒泡:div元素
    

    在示例中:

    • 首先,遍历 button 对应的 fiber 。将 onClickCapture 事件处理程序 handleButtonClickCapture,添加到事件队列最前面;将 onClick 事件处理程序 handleButtonClick,添加到事件队列中。形成结构为 [handleButtonClickCapture, handleButtonClick]
    • 然后,向上遍历 div 对应的 fiber ,将 onClickCapture 事件处理程序 handleDivClickCapture,添加到事件队列最前面;将 onClick 事件处理程序 handleDivClick,添加到事件队列中。形成结构为 [handleDivClickCapture, handleButtonClickCapture, handleButtonClick, handleDivClick]
  • 事件触发

    形成事件对象 event 和事件处理函数队列之后,通过 runEventsInBatch 函数进行批量执行,触发事件。同时,如果发现有阻止冒泡,则会跳出循环,重置事件源。

    runEventsInBatch(events) 函数
    // packages\legacy-events\EventBatching.js
    
    let eventQueue: ?(Array<ReactSyntheticEvent> | ReactSyntheticEvent) = null
    
    const executeDispatchesAndRelease = function (event: ReactSyntheticEvent) {
      if (event) {
        executeDispatchesInOrder(event)
    
        if (!event.isPersistent()) {
          event.constructor.release(event)
        }
      }
    }
    const executeDispatchesAndReleaseTopLevel = function (e) {
      return executeDispatchesAndRelease(e)
    }
    
    export function runEventsInBatch(
      events: Array<ReactSyntheticEvent> | ReactSyntheticEvent | null
    ) {
      if (events !== null) {
        eventQueue = accumulateInto(eventQueue, events)
      }
    
      // Set `eventQueue` to null before processing it so that we can tell if more
      // events get enqueued while processing.
      const processingEventQueue = eventQueue
      eventQueue = null
    
      if (!processingEventQueue) {
        return
      }
    
      forEachAccumulated(
        processingEventQueue,
        executeDispatchesAndReleaseTopLevel
      )
      invariant(
        !eventQueue,
        'processEventQueue(): Additional events were enqueued while processing ' +
          'an event queue. Support for this has not yet been implemented.'
      )
      // This would be a good time to rethrow if any of the event handlers threw.
      rethrowCaughtError()
    }
    
    // packages\legacy-events\forEachAccumulated.js
    
    function forEachAccumulated<T>(
      arr: ?(Array<T> | T),
      cb: (elem: T) => void,
      scope: ?any
    ) {
      if (Array.isArray(arr)) {
        arr.forEach(cb, scope)
      } else if (arr) {
        cb.call(scope, arr)
      }
    }
    

事件触发流程总结

  • 首先通过统一事件处理函数 dispatchEvent,进行批量更新 batchedEventUpdates
  • 根据事件源找到与之匹配的 DOM 元素 fiber,执行事件对应的处理插件中的 extractEvents,并进行遍历,形成一个事件执行队列,React 使用该队列模拟 事件捕获 --> 事件源 --> 事件冒泡 过程。
  • 最后通过 runEventsInBatch 执行事件队列,完成整个触发流程。如果发现有阻止事件冒泡,则会跳出循环,重置事件源,放回到事件池中,完成整个流程。

React 17、18 事件系统

React 17、18 事件系统的相关调整:

  • 事件统一绑定到 container 上 (ReactDOM.render(app, container)) ,而不是 document 上。有利于微前端,如果同时存在多个子应用,全部绑定在 document 上,可能会出现问题。
  • 对齐原生浏览器事件。React 17 中支持原生捕获事件,对齐了浏览器原生标准。同时,onScroll 事件不再进行事件冒泡,onFocusonBlur 使用原生 focusinfocusout 合成。
  • 取消事件池。React 17 取消事件池复用,也就解决了在 React 16 中,如果需要在事件处理函数运行之后获取事件对象的属性需要调用 e.persist() 的问题。

事件绑定流程

React 事件系统,在 createRoot 中,通过 listenToAllSupportedEvents 会向外层容器注册全部事件。

createRoot(container, options) 函数
// packages\react-dom\src\client\ReactDOMRoot.js

export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions
): RootType {
  if (!isValidContainer(container)) {
    throw new Error('createRoot(...): Target container is not a DOM element.')
  }

  warnIfReactDOMContainerInDEV(container)

  let isStrictMode = false
  let concurrentUpdatesByDefaultOverride = false
  let identifierPrefix = ''
  let onRecoverableError = defaultOnRecoverableError
  let transitionCallbacks = null

  if (options !== null && options !== undefined) {
    if (options.unstable_strictMode === true) {
      isStrictMode = true
    }
    if (
      allowConcurrentByDefault &&
      options.unstable_concurrentUpdatesByDefault === true
    ) {
      concurrentUpdatesByDefaultOverride = true
    }
    if (options.identifierPrefix !== undefined) {
      identifierPrefix = options.identifierPrefix
    }
    if (options.onRecoverableError !== undefined) {
      onRecoverableError = options.onRecoverableError
    }
    if (options.transitionCallbacks !== undefined) {
      transitionCallbacks = options.transitionCallbacks
    }
  }

  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks
  )
  markContainerAsRoot(root.current, container)

  const rootContainerElement: Document | Element | DocumentFragment =
    container.nodeType === COMMENT_NODE
      ? (container.parentNode: any)
      : container
  listenToAllSupportedEvents(rootContainerElement)

  return new ReactDOMRoot(root)
}

listenToAllSupportedEvents 通过 listenToNativeEvent 绑定浏览器事件。

  • 常规事件,则执行两次 listenToNativeEvent,分别在冒泡和捕获阶段绑定事件。
  • 不冒泡事件,则执行 listenToNativeEvent(domEventName, true, rootContainerElement)

listenToAllSupportedEvents(rootContainerElement) 函数中:

  • rootContainerElement : 根节点 root
  • allNativeEvents 常量 : Set 集合,保存了 81 个浏览器常用事件。
  • nonDelegatedEvents 常量 : Set 集合,保存了浏览器中不会冒泡的事件,一般指媒体事件(比如:pauseplayplaying 等),还有一些特殊事件(比如:cancelcloseinvalidloadscrolltoggle)。
listenToAllSupportedEvents(rootContainerElement) 函数
// packages\react-dom\src\events\DOMPluginEventSystem.js

export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  if (!(rootContainerElement: any)[listeningMarker]) {
    ;(rootContainerElement: any)[listeningMarker] = true
    allNativeEvents.forEach(domEventName => {
      // We handle selectionchange separately because it
      // doesn't bubble and needs to be on the document.
      if (domEventName !== 'selectionchange') {
        /* nonDelegatedEvents 保存了浏览器中不会冒泡的事件 */
        if (!nonDelegatedEvents.has(domEventName)) {
          /* 在冒泡阶段绑定事件 */
          listenToNativeEvent(domEventName, false, rootContainerElement)
        }
        /* 在捕获阶段绑定事件 */
        listenToNativeEvent(domEventName, true, rootContainerElement)
      }
    })
    const ownerDocument =
      (rootContainerElement: any).nodeType === DOCUMENT_NODE
        ? rootContainerElement
        : (rootContainerElement: any).ownerDocument
    if (ownerDocument !== null) {
      // The selectionchange event also needs deduplication
      // but it is attached to the document.
      if (!(ownerDocument: any)[listeningMarker]) {
        ;(ownerDocument: any)[listeningMarker] = true
        listenToNativeEvent('selectionchange', false, ownerDocument)
      }
    }
  }
}

listenToNativeEvent 本质上是向原生 DOM 中注册事件,进行事件监听。

listenToNativeEvent 中,调用 addTrappedEventListener 函数根据事件获取对应的优先级,不同的优先级在容器 DOM 节点注册不同的事件回调函数。

listenToNativeEvent(domEventName, isCapturePhaseListener, target) 函数中:

  • domEventName 入参对应的事件名(如:click
  • isCapturePhaseListener 入参表示是否捕获:true 为捕获,false 为冒泡
listenToNativeEvent(domEventName, isCapturePhaseListener, target) 函数
// packages\react-dom\src\events\DOMPluginEventSystem.js

export function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,
  target: EventTarget
): void {
  let eventSystemFlags = 0
  if (isCapturePhaseListener) {
    eventSystemFlags |= IS_CAPTURE_PHASE
  }
  addTrappedEventListener(
    target,
    domEventName,
    eventSystemFlags,
    isCapturePhaseListener
  )
}

addTrappedEventListener 中,调用 createEventListenerWrapperWithPriority 函数判断事件执行的优先级,并返回对应的监听器。

createEventListenerWrapperWithPriority 函数中,根据 eventPriority 来判断优先级,不同的优先级返回不同的监听函数。

  • dispatchDiscreteEvent :离散事件监听器,优先级为 0
  • dispatchContinuousEvent :用户阻塞事件监听器,优先级为 1
  • dispatchEvent :连续事件或其他事件监听器,优先级为 2
addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener, isDeferredListenerForLegacyFBSupport) 函数
// packages\react-dom\src\events\DOMPluginEventSystem.js

function addTrappedEventListener(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  isCapturePhaseListener: boolean,
  isDeferredListenerForLegacyFBSupport?: boolean
) {
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags
  )
  // If passive option is not supported, then the event will be
  // active and not passive.
  let isPassiveListener = undefined
  if (passiveBrowserEventsSupported) {
    // Browsers introduced an intervention, making these events
    // passive by default on document. React doesn't bind them
    // to document anymore, but changing this now would undo
    // the performance wins from the change. So we emulate
    // the existing behavior manually on the roots now.
    // https://github.com/facebook/react/issues/19651
    if (
      domEventName === 'touchstart' ||
      domEventName === 'touchmove' ||
      domEventName === 'wheel'
    ) {
      isPassiveListener = true
    }
  }

  targetContainer =
    enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
      ? (targetContainer: any).ownerDocument
      : targetContainer

  let unsubscribeListener
  // When legacyFBSupport is enabled, it's for when we
  // want to add a one time event listener to a container.
  // This should only be used with enableLegacyFBSupport
  // due to requirement to provide compatibility with
  // internal FB www event tooling. This works by removing
  // the event listener as soon as it is invoked. We could
  // also attempt to use the {once: true} param on
  // addEventListener, but that requires support and some
  // browsers do not support this today, and given this is
  // to support legacy code patterns, it's likely they'll
  // need support for such browsers.
  if (enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport) {
    const originalListener = listener
    listener = function (...p) {
      removeEventListener(
        targetContainer,
        domEventName,
        unsubscribeListener,
        isCapturePhaseListener
      )
      return originalListener.apply(this, p)
    }
  }
  // TODO: There are too many combinations here. Consolidate them.
  if (isCapturePhaseListener) {
    if (isPassiveListener !== undefined) {
      unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
        targetContainer,
        domEventName,
        listener,
        isPassiveListener
      )
    } else {
      unsubscribeListener = addEventCaptureListener(
        targetContainer,
        domEventName,
        listener
      )
    }
  } else {
    if (isPassiveListener !== undefined) {
      unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
        targetContainer,
        domEventName,
        listener,
        isPassiveListener
      )
    } else {
      unsubscribeListener = addEventBubbleListener(
        targetContainer,
        domEventName,
        listener
      )
    }
  }
}
createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) 函数
// packages\react-dom\src\events\ReactDOMEventListener.js

export function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags
): Function {
  const eventPriority = getEventPriority(domEventName)
  let listenerWrapper
  switch (eventPriority) {
    case DiscreteEventPriority:
      listenerWrapper = dispatchDiscreteEvent
      break
    case ContinuousEventPriority:
      listenerWrapper = dispatchContinuousEvent
      break
    case DefaultEventPriority:
    default:
      listenerWrapper = dispatchEvent
      break
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer
  )
}

function dispatchDiscreteEvent(
  domEventName,
  eventSystemFlags,
  container,
  nativeEvent
) {
  const previousPriority = getCurrentUpdatePriority()
  const prevTransition = ReactCurrentBatchConfig.transition
  ReactCurrentBatchConfig.transition = null
  try {
    setCurrentUpdatePriority(DiscreteEventPriority)
    dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent)
  } finally {
    setCurrentUpdatePriority(previousPriority)
    ReactCurrentBatchConfig.transition = prevTransition
  }
}

export function dispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent
): void {
  if (!_enabled) {
    return
  }
  if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
    dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
      domEventName,
      eventSystemFlags,
      targetContainer,
      nativeEvent
    )
  } else {
    dispatchEventOriginal(
      domEventName,
      eventSystemFlags,
      targetContainer,
      nativeEvent
    )
  }
}

事件触发流程

触发事件时,首先会执行 dispatchEvent 函数,最终会通过 batchedUpdates(批量更新) 来处理 dispatchEventsForPlugins

执行流程为:dispatchEvent --> dispatchEventOriginal / dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay --> dispatchEventForPluginEventSystem --> batchedUpdates --> dispatchEventsForPlugins

dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) 函数
export function dispatchEvent(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget,
  nativeEvent: AnyNativeEvent
): void {
  if (!_enabled) {
    return
  }
  if (enableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay) {
    dispatchEventWithEnableCapturePhaseSelectiveHydrationWithoutDiscreteEventReplay(
      domEventName,
      eventSystemFlags,
      targetContainer,
      nativeEvent
    )
  } else {
    dispatchEventOriginal(
      domEventName,
      eventSystemFlags,
      targetContainer,
      nativeEvent
    )
  }
}

dispatchEventsForPlugins 函数中:

  • 相关入参
    • domEventName :事件名称
    • eventSystemFlags :事件处理的阶段。(0 - 冒泡阶段, 4 - 捕获阶段)
    • nativeEvent :原生事件的事件源(event
    • targetInst :DOM 元素对应的节点,即 fiber 节点
    • targetContainer :根节点
  • 首先,通过 const nativeEventTarget = getEventTarget(nativeEvent) 获取到发生事件的元素,即事件源。
  • 然后,创建事件队列 const dispatchQueue = [],用于存储待更新的事件队列
  • 接着,通过 extractEvents 收集事件
  • 最后,通过 processDispatchQueue 执行事件
dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) 函数
// packages\react-dom\src\events\DOMPluginEventSystem.js

function dispatchEventsForPlugins(
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,
  nativeEvent: AnyNativeEvent,
  targetInst: null | Fiber,
  targetContainer: EventTarget
): void {
  const nativeEventTarget = getEventTarget(nativeEvent)
  const dispatchQueue: DispatchQueue = []
  extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer
  )
  processDispatchQueue(dispatchQueue, eventSystemFlags)
}

extractEvents 收集事件,以 SimpleEventPlugin.extractEvents 为例。在 SimpleEventPlugin 中的 extractEvents 中:

  • 通过 topLevelEventsToReactNames.get(domEventName) 来获取对应的合成事件名称,如: onMouseOver
  • SyntheticEventCtor() 是合成函数的构造函数。
  • 然后,通过 switch case 来匹配对应的合成事件的构造函数。
  • inCapturePhase 判断是否捕获阶段。
  • 通过 accumulateSinglePhaseListeners() 函数来获取当前阶段的所有事件。在该函数中,会获取存储在 Fiber 上的 Props 的对应事件,然后通过 createDispatchListener 返回的对象加入到监听集合上,如果是不会冒泡的函数则会停止(比如:scroll),反之,则会向上递归。
  • 最后,通过 new SyntheticEventCtor() 生成对应的事件源,插入队列中。
extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags, targetContainer) 函数
// packages\react-dom\src\events\DOMPluginEventSystem.js

function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget
) {
  // TODO: we should remove the concept of a "SimpleEventPlugin".
  // This is the basic functionality of the event system. All
  // the other plugins are essentially polyfills. So the plugin
  // should probably be inlined somewhere and have its logic
  // be core the to event system. This would potentially allow
  // us to ship builds of React without the polyfilled plugins below.
  SimpleEventPlugin.extractEvents(
    dispatchQueue,
    domEventName,
    targetInst,
    nativeEvent,
    nativeEventTarget,
    eventSystemFlags,
    targetContainer
  )
  const shouldProcessPolyfillPlugins =
    (eventSystemFlags & SHOULD_NOT_PROCESS_POLYFILL_EVENT_PLUGINS) === 0
  // We don't process these events unless we are in the
  // event's native "bubble" phase, which means that we're
  // not in the capture phase. That's because we emulate
  // the capture phase here still. This is a trade-off,
  // because in an ideal world we would not emulate and use
  // the phases properly, like we do with the SimpleEvent
  // plugin. However, the plugins below either expect
  // emulation (EnterLeave) or use state localized to that
  // plugin (BeforeInput, Change, Select). The state in
  // these modules complicates things, as you'll essentially
  // get the case where the capture phase event might change
  // state, only for the following bubble event to come in
  // later and not trigger anything as the state now
  // invalidates the heuristics of the event plugin. We
  // could alter all these plugins to work in such ways, but
  // that might cause other unknown side-effects that we
  // can't foresee right now.
  if (shouldProcessPolyfillPlugins) {
    EnterLeaveEventPlugin.extractEvents(
      dispatchQueue,
      domEventName,
      targetInst,
      nativeEvent,
      nativeEventTarget,
      eventSystemFlags,
      targetContainer
    )
    ChangeEventPlugin.extractEvents(
      dispatchQueue,
      domEventName,
      targetInst,
      nativeEvent,
      nativeEventTarget,
      eventSystemFlags,
      targetContainer
    )
    SelectEventPlugin.extractEvents(
      dispatchQueue,
      domEventName,
      targetInst,
      nativeEvent,
      nativeEventTarget,
      eventSystemFlags,
      targetContainer
    )
    BeforeInputEventPlugin.extractEvents(
      dispatchQueue,
      domEventName,
      targetInst,
      nativeEvent,
      nativeEventTarget,
      eventSystemFlags,
      targetContainer
    )
  }
}
SimpleEventPlugin.extractEvents 函数
// packages\react-dom\src\events\plugins\SimpleEventPlugin.js

function extractEvents(
  dispatchQueue: DispatchQueue,
  domEventName: DOMEventName,
  targetInst: null | Fiber,
  nativeEvent: AnyNativeEvent,
  nativeEventTarget: null | EventTarget,
  eventSystemFlags: EventSystemFlags,
  targetContainer: EventTarget
): void {
  const reactName = topLevelEventsToReactNames.get(domEventName)
  if (reactName === undefined) {
    return
  }
  let SyntheticEventCtor = SyntheticEvent
  let reactEventType: string = domEventName
  switch (domEventName) {
    case 'keypress':
      // Firefox creates a keypress event for function keys too. This removes
      // the unwanted keypress events. Enter is however both printable and
      // non-printable. One would expect Tab to be as well (but it isn't).
      if (getEventCharCode(((nativeEvent: any): KeyboardEvent)) === 0) {
        return
      }
    /* falls through */
    case 'keydown':
    case 'keyup':
      SyntheticEventCtor = SyntheticKeyboardEvent
      break
    case 'focusin':
      reactEventType = 'focus'
      SyntheticEventCtor = SyntheticFocusEvent
      break
    case 'focusout':
      reactEventType = 'blur'
      SyntheticEventCtor = SyntheticFocusEvent
      break
    case 'beforeblur':
    case 'afterblur':
      SyntheticEventCtor = SyntheticFocusEvent
      break
    case 'click':
      // Firefox creates a click event on right mouse clicks. This removes the
      // unwanted click events.
      if (nativeEvent.button === 2) {
        return
      }
    /* falls through */
    case 'auxclick':
    case 'dblclick':
    case 'mousedown':
    case 'mousemove':
    case 'mouseup':
    // TODO: Disabled elements should not respond to mouse events
    /* falls through */
    case 'mouseout':
    case 'mouseover':
    case 'contextmenu':
      SyntheticEventCtor = SyntheticMouseEvent
      break
    case 'drag':
    case 'dragend':
    case 'dragenter':
    case 'dragexit':
    case 'dragleave':
    case 'dragover':
    case 'dragstart':
    case 'drop':
      SyntheticEventCtor = SyntheticDragEvent
      break
    case 'touchcancel':
    case 'touchend':
    case 'touchmove':
    case 'touchstart':
      SyntheticEventCtor = SyntheticTouchEvent
      break
    case ANIMATION_END:
    case ANIMATION_ITERATION:
    case ANIMATION_START:
      SyntheticEventCtor = SyntheticAnimationEvent
      break
    case TRANSITION_END:
      SyntheticEventCtor = SyntheticTransitionEvent
      break
    case 'scroll':
      SyntheticEventCtor = SyntheticUIEvent
      break
    case 'wheel':
      SyntheticEventCtor = SyntheticWheelEvent
      break
    case 'copy':
    case 'cut':
    case 'paste':
      SyntheticEventCtor = SyntheticClipboardEvent
      break
    case 'gotpointercapture':
    case 'lostpointercapture':
    case 'pointercancel':
    case 'pointerdown':
    case 'pointermove':
    case 'pointerout':
    case 'pointerover':
    case 'pointerup':
      SyntheticEventCtor = SyntheticPointerEvent
      break
    default:
      // Unknown event. This is used by createEventHandle.
      break
  }

  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0
  if (
    enableCreateEventHandleAPI &&
    eventSystemFlags & IS_EVENT_HANDLE_NON_MANAGED_NODE
  ) {
    const listeners = accumulateEventHandleNonManagedNodeListeners(
      // TODO: this cast may not make sense for events like
      // "focus" where React listens to e.g. "focusin".
      ((reactEventType: any): DOMEventName),
      targetContainer,
      inCapturePhase
    )
    if (listeners.length > 0) {
      // Intentionally create event lazily.
      const event = new SyntheticEventCtor(
        reactName,
        reactEventType,
        null,
        nativeEvent,
        nativeEventTarget
      )
      dispatchQueue.push({ event, listeners })
    }
  } else {
    // Some events don't bubble in the browser.
    // In the past, React has always bubbled them, but this can be surprising.
    // We're going to try aligning closer to the browser behavior by not bubbling
    // them in React either. We'll start by not bubbling onScroll, and then expand.
    const accumulateTargetOnly =
      !inCapturePhase &&
      // TODO: ideally, we'd eventually add all events from
      // nonDelegatedEvents list in DOMPluginEventSystem.
      // Then we can remove this special list.
      // This is a breaking change that can wait until React 18.
      domEventName === 'scroll'

    const listeners = accumulateSinglePhaseListeners(
      targetInst,
      reactName,
      nativeEvent.type,
      inCapturePhase,
      accumulateTargetOnly,
      nativeEvent
    )
    if (listeners.length > 0) {
      // Intentionally create event lazily.
      const event = new SyntheticEventCtor(
        reactName,
        reactEventType,
        null,
        nativeEvent,
        nativeEventTarget
      )
      dispatchQueue.push({ event, listeners })
    }
  }
}

processDispatchQueue 执行事件。在该函数中,遍历对应的合成事件,获取对应的事件源和监听的函数,最后会调用 processDispatchQueueItemsInOrder 函数。

processDispatchQueueItemsInOrde 函数中,通过 inCapturePhase 来模拟对应的冒泡与捕获。

  • event.isPropagationStopped() :用于判断是否阻止冒泡(e.stopPropagation),如果阻止冒泡,则会退出,从而模拟事件流的过程。
  • executeDispatch() :执行事件的函数。
processDispatchQueue(dispatchQueue, eventSystemFlags) 函数
// packages\react-dom\src\events\DOMPluginEventSystem.js

export function processDispatchQueue(
  dispatchQueue: DispatchQueue,
  eventSystemFlags: EventSystemFlags
): void {
  const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0
  for (let i = 0; i < dispatchQueue.length; i++) {
    const { event, listeners } = dispatchQueue[i]
    processDispatchQueueItemsInOrder(event, listeners, inCapturePhase)
    //  event system doesn't use pooling.
  }
  // This would be a good time to rethrow if any of the event handlers threw.
  rethrowCaughtError()
}

function processDispatchQueueItemsInOrder(
  event: ReactSyntheticEvent,
  dispatchListeners: Array<DispatchListener>,
  inCapturePhase: boolean
): void {
  let previousInstance
  if (inCapturePhase) {
    for (let i = dispatchListeners.length - 1; i >= 0; i--) {
      const { instance, currentTarget, listener } = dispatchListeners[i]
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return
      }
      executeDispatch(event, listener, currentTarget)
      previousInstance = instance
    }
  } else {
    for (let i = 0; i < dispatchListeners.length; i++) {
      const { instance, currentTarget, listener } = dispatchListeners[i]
      if (instance !== previousInstance && event.isPropagationStopped()) {
        return
      }
      executeDispatch(event, listener, currentTarget)
      previousInstance = instance
    }
  }
}
上次编辑于:
贡献者: lingronghai