react-事件系统1-原生事件和合成事件

loading 2023年01月08日 306次浏览

1. react事件绑定在哪?

我们知道,原生dom事件就绑定在对应的元素上,但是这点在react中有所不同:
在react中,假设给一个button绑定了点击事件,会发现事件并没有绑定在button上,button上只绑定了一个空函数,而真正的事件绑定到了document上

本文先讲述react17之前事件版本,也就是事件绑定在document上。

也有一些事件不冒泡,如input的focus事件,video的onplay,不是在document元素上监听,而是在元素本身上监听

这里做一个小拓展,事件到底被存到了哪里?

我们知道jsx代码会被babel转化成React.createElement的形式,最终被转化成fiber对象,所以事件最终其实被保存到了fiber的memoizedPropspendingProps中。

2. 合成事件

2.1 概念

合成事件,是react创造的模拟原生dom事件所有能力的一个事件对象

比如上面给button绑定的点击事件,react中是'onClick',而这并不是原生事件,而是react的合成事件。

又比如说一个好用的合成事件'onChange',其实是很多原生事件的综合,比如blur、change、focus、input等。

2.2 为什么要使用合成事件?

主要有这些原因:

  • 增强兼容性: 每个浏览器的内核都不相同,而React通过顶层事件代理机制,保证冒泡的统一性,抹平不同浏览器事件对象之间的差异,将不同平台的事件进行模拟成合成事件,使其能够跨浏览器执行
  • 统一管理所有事件: 原生事件系统中,所有事件都被绑定在对应的dom上,如果页面复杂,会产生非常多的绑定事件;而react将所有事件都放在document上,便于统一管理
  • 避免垃圾回收: 假设给原生事件系统和react事件系统中的输入框都绑定onChange,那么原生事件绑定onchange对应的就是change,而React会被处理为很多的监听器。在实际中,我们的事件会被频繁的创建和回收,这样会影响其性能,为了解决这个问题,React引入事件池,通过事件池来获取和释放事件

事件池在react17中被删除

2.3 对比原生事件和合成事件

2.3.1 执行顺序对比

import React, { useEffect, useRef } from "react";

export default function App(props) {

    const ref = useRef(null)
    const ref1 = useRef(null)

    useEffect(() => {
        const div = document.querySelector("div")
        const button = document.querySelector("button")

        div.addEventListener("click", () => console.log("原生:div元素"))
        button.addEventListener("click", () => console.log("原生:button元素"))
        document.addEventListener("click", () => console.log("原生:document元素"))
    }, [])

    return (
        <div onClick={() => console.log('React:div元素')}>
            <button
                onClick={() => console.log('React:按钮元素')}
            >
                执行顺序
            </button>
        </div>
    );
}

可以看出,当dom元素事件被触发后,会先执行原生事件,再处理react事件,最后真正执行document上挂载的事件。

2.3.2 和原生事件的区别?

(1) 事件名不同
原生事件,是以纯小写来命名,如:onclick
合成事件,是以小驼峰式来命名,如:onClick

(2) 接收的参数不同
原生事件接收字符串,如

onClick = 'Click()'

合成事件接收函数,如

onClick = {() => Click()}

(3) 事件源不同
在React中,我们的所有事件都可以说是虚拟的,并不是原生的事件。
我们在React中拿到的事件源(e)也并非是真正的事件e,而是经过React单独处理的e。

(4) 阻止默认事件方式不同
原生事件中,可以通过e.preventDefault()和return false 来阻止默认事件
合成事件中,通过e.preventDefault()阻止默认事件

注意原生事件和合成事件的e.preventDefault()并非是同一个函数,React的事件源e是单独创立的

拓展:合成事件中的阻止冒泡:

  • e.stopPropagation() :阻止虚拟dom中合成事件的冒泡,但不会阻止document上的事件
  • e.nativeEvent.stopImmediatePropagation() :与上一种相反,只能阻止绑定在document上的事件冒泡

react通过e.nativeEvent可以得到真实dom的对象e

2.3.3 合成事件的冒泡和捕获

在React中,所有的绑定事件(如:onClick、onChange)都是冒泡阶段执行。

所有的捕获阶段统一加Capture,如onClickCapture、onChangeCapture

    <button
      onClick={() => {console.log('冒泡')}} 
      onClickCapture={() => {console.log("捕获")}} >
        点击
    </button>

2.4 两种事件是否可以混用?

先举个例子

import React, { useEffect, useRef } from "react";

export default function App(props) {

    useEffect(() => {
        const button = document.querySelector("button")

        button.addEventListener("click", (e) => {
            e.stopPropagation();
            console.log("原生button阻止冒泡");
        })
        document.addEventListener("click", () => {
            console.log("原生document元素");
        });
    }, [])

    return (
        <button
            onClick={() => {
                console.log('按钮事件')
            }}
        >
            混用
        </button>
    );
}

最终只输出“原生button阻止冒泡”。

原因是原生事件会先执行,并且执行了阻止冒泡语句后,button的点击事件不会冒泡到document,最终导致无法触发合成按钮事件。

而反过来,给合成事件阻止冒泡,是无法阻止真实dom事件的执行的。