Skip to content

react 事件原理 ​

2023-06-13 21:37:08

React 中所有触发的事件,都是自己在其内部封装了一套事件机制。目的是为了实现全浏览器的一致性,抹平不同浏览器之间的差异性。

事件合成 ​

在 React17 之前,React 是把事件委托在 document 上的,React17 及以后版本不再把事件委托在 document 上,而是委托在挂载的容器上。React 合成事件采用的是事件冒泡机制,当在某具体元素上触发事件时,等冒泡到顶部被挂载事件的那个元素时,才会真正地执行事件。

而原生事件,当某具体元素触发事件时,会立刻执行该事件。因此若要比较事件触发的先后时机时,原生事件会先执行,React 合成事件会后执行。

React 的事件不是绑定在元素上的,而是统一绑定在顶部容器上,在 v17 之前是绑定在 document 上的,在 v17 改成了 app 容器上。这样更利于一个 html 下存在多个应用(微前端)。

绑定事件并不是一次性绑定所有事件,比如发现了 onClick 事件,就会绑定 click 事件,比如发现 onChange 事件,会绑定 [blur,change ,focus ,keydown,keyup] 多个事件。

看下面这段代码的执行顺序

tsx
class App extends React.Component {
  constructor(props) {
    super(props)
    this.parentRef = React.createRef()
    this.childRef = React.createRef()
  }
  componentDidMount() {
    console.log('React componentDidMount!')
    this.parentRef.current?.addEventListener('click', () => {
      console.log('原生事件:父元素 DOM 事件监听!')
    })
    this.childRef.current?.addEventListener('click', () => {
      console.log('原生事件:子元素 DOM 事件监听!')
    })
    document.addEventListener('click', e => {
      console.log('原生事件:document DOM 事件监听!')
    })
  }
  parentClickFun = () => {
    console.log('React 事件:父元素事件监听!')
  }
  childClickFun = () => {
    console.log('React 事件:子元素事件监听!')
  }
  render() {
    return (
      <div ref={this.parentRef} onClick={this.parentClickFun}>
        <div ref={this.childRef} onClick={this.childClickFun}>
          分析事件执行顺序
        </div>
      </div>
    )
  }
}
export default App

输出顺序为

txt
原生事件:子元素 DOM 事件监听!
原生事件:父元素 DOM 事件监听!
React 事件:子元素事件监听!
React 事件:父元素事件监听!
原生事件:document DOM 事件监听!

可以得出结论

  • React 所有事件都挂载在 document 对象上
  • 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件
  • 所以会先执行原生事件,然后处理 React 事件
  • 最后真正执行 document 上挂载的事件

所以想要阻止不同时间段的冒泡行为,对应使用不同的方法,对应如下:

  • 阻止合成事件间的冒泡,用 e.stopPropagation()
  • 阻止合成事件与最外层 document 上的事件间的冒泡,用 e.nativeEvent.stopImmediatePropagation()
  • 阻止合成事件与除最外层 document 上的原生事件上的冒泡,通过判断 e.target 来避免

事件插件机制 ​

React 有一种事件插件机制,比如上述 onClick 和 onChange ,会有不同的事件插件 SimpleEventPlugin,ChangeEventPlugin 处理。

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

registrationNameModules 记录了 React 事件(比如 onBlur )和与之对应的处理插件的映射,比如上述的 onClick ,就会用 SimpleEventPlugin 插件处理,onChange 就会用 ChangeEventPlugin 处理。应用于事件触发阶段,根据不同事件使用不同的插件。

ts
const registrationNameDependencies = {
  onBlur: ['blur'],
  onClick: ['click'],
  onClickCapture: ['click'],
  onChange: [
    'blur',
    'change',
    'click',
    'focus',
    'input',
    'keydown',
    'keyup',
    'selectionchange'
  ],
  onMouseEnter: ['mouseout', 'mouseover'],
  onMouseLeave: ['mouseout', 'mouseover']
}

registrationNameDependencies 对象保存了 React 事件和原生事件对应关系,这就解释了为什么上述只写了一个 onChange ,会有很多原生事件绑定在 document 上。在事件绑定阶段,如果发现有 React 事件,比如 onChange ,就会找到对应的原生事件数组,逐一绑定。

总结 ​

  • React 上注册的事件最终会绑定在 document 这个 DOM 上,而不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都- 绑定在 document 上,其他节点没有绑定事件)
  • React 自身实现了一套事件冒泡机制,所以这也就是为什么我们 event.stopPropagation()无效的原因。
  • React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback
  • React 有一套自己的合成事件 SyntheticEvent

Released under the MIT License.