Skip to content

原理篇-事件原理(老版本)

一前言

v17 之前 React 事件都是绑定在 document 上,v17 之后 React 把事件绑定在应用对应的容器 container 上,将事件绑定在同一容器统一管理,防止很多事件直接绑定在原生的 DOM 元素上。

二独特的事件处理

冒泡阶段 绑定的事件比如: onClick,onChange。 捕获阶段 绑定的事件比如: onClickCapture,onChangeCapture。

三 事件合成

  • 第一个部分是事件合成系统,初始化会注册不同的事件插件。
  • 第二个就是在一次渲染过程中,对事件标签中事件的收集,向 container 注册事件。
  • 第三个就是一次用户交互,事件触发,到事件执行一系列过程。

事件合成概念

  • React 的事件不是绑定在元素上的,而是统一绑定在顶部容器上,在 v17 之前是绑定在 document 上的,在 v17 改成了 app 容器上。这样更利于一个 html 下存在多个应用(微前端)。
  • 绑定事件并不是一次性绑定所有事件,比如发现了 onClick 事件,就会绑定 click 事件,比如发现 onChange 事件,会绑定 [blur,change ,focus ,keydown,keyup] 多个事件
  • React 合成事件的概念: React 应用中,元素绑定的事件并不是原生的事件,而是 React 合成事件,比如 onClick 是由 click 合成,onChange 是由 blur ,change ,focus 等多个事件合成。

事件插件机制

第一个 registrationNameModules ,registrationNameModules 记录了 React 事件(比如 onBlur )和与之对应的处理插件的映射,比如上述的 onClick ,就会用 SimpleEventPlugin 插件处理,onChange 就会用 ChangeEventPlugin 处理。 React 的事件和事件源是自己合成的,所以对于不同事件需要不同的事件插件处理。

js
const registrationNameModules = {
    onBlur: SimpleEventPlugin,
    onClick: SimpleEventPlugin,
    onClickCapture: SimpleEventPlugin,
    onChange: ChangeEventPlugin,
    onChangeCapture: ChangeEventPlugin,
    onMouseEnter: EnterLeaveEventPlugin,
    onMouseLeave: EnterLeaveEventPlugin,
    ...
}

第二个 registrationNameDependencies

这个对象保存了 React 事件和原生事件对应关系。

js
{
    onBlur: ['blur'],
    onClick: ['click'],
    onClickCapture: ['click'],
    onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
    onMouseEnter: ['mouseout', 'mouseover'],
    onMouseLeave: ['mouseout', 'mouseover'],
    ...
}

四 事件绑定

如下例子, onChange 和 onClick 会保存在对应 DOM 元素类型 fiber 对象( hostComponent )的 memoizedProps 属性上

jsx
export default function Index(){
  const handleClick = () => console.log('点击事件')
  const handleChange =() => console.log('change事件)
  return <div >
     <input onChange={ handleChange }  />
     <button onClick={ handleClick } >点击</button>
  </div>
}

image

五 事件触发

假设 DOM 结构是如下这样的:

jsx
export default function Index() {
  const handleClick1 = () => console.log(1)
  const handleClick2 = () => console.log(2)
  const handleClick3 = () => console.log(3)
  const handleClick4 = () => console.log(4)
  return (
    <div onClick={handleClick3} onClickCapture={handleClick4}>
      <button onClick={handleClick1} onClickCapture={handleClick2}>
        点击
      </button>
    </div>
  )
}

第一步:批量更新

dispatchEvent 执行会传入真实的事件源 button 元素本身,通过元素可以找到 button 对应的 fiber,fiber 和原生 DOM 之间是如何建立起联系的呢? image 接下来就是批量更新环节,批量更新会 合并 state。

jsx
export function batchedEventUpdates(fn, a) {
  isBatchingEventUpdates = true //打开批量更新开关
  try {
    fn(a) // 事件在这里执行
  } finally {
    isBatchingEventUpdates = false //关闭批量更新开关
  }
}

第二步:合成事件源

通过 onClick 找到对应的处理插件 SimpleEventPlugin ,合成新的事件源 e ,里面包含了 preventDefault 和 stopPropagation 等方法。

第三步:形成事件执行队列

在第一步通过原生 DOM 获取到对应的 fiber ,接着会从这个 fiber 向上遍历,遇到元素类型 fiber ,就会收集事件,用一个数组收集事件:

  • 如果遇到捕获阶段事件 onClickCapture ,就会 unshift 放在数组前面。以此模拟事件捕获阶段。
  • 如果遇到冒泡阶段事件 onClick ,就会 push 到数组后面,模拟事件冒泡阶段。
  • 一直收集到最顶端 app ,形成执行队列,在接下来阶段,依次执行队列里面的函数。 4 个事件执行顺序是这样的:
  • 首先第一次收集是在 button 上,形成结构 [ handleClick2 , handleClick1 ]
  • 然后接着向上收集,遇到父级,收集父级 div 上的事件。[handleClick4, handleClick2 , handleClick1,handleClick3 ]
  • 依次执行数组里面的事件,所以打印 4 2 1 3。

image

React 如何模拟阻止事件冒泡

判断当前已经阻止事件冒泡了,则不执行事件队列里面的后续事件。

js
function runEventsInBatch() {
  const dispatchListeners = event._dispatchListeners
  if (Array.isArray(dispatchListeners)) {
    for (let i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        /* 判断是否已经阻止事件冒泡 */
        break
      }
      dispatchListeners[i](event) /* 执行真正的处理函数 及handleClick1... */
    }
  }
}