问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

史上最全React事件机制详解

创作时间:
作者:
@小白创作中心

史上最全React事件机制详解

引用
CSDN
1.
https://blog.csdn.net/kelai_6792/article/details/143082485

React事件机制是React开发中的重要组成部分,理解其工作原理对于提升开发效率和代码质量至关重要。本文将从DOM事件流的三个阶段开始,深入探讨React事件绑定机制、React事件与原生事件的区别、执行顺序、跨浏览器兼容性以及源码分析等核心内容。

DOM事件流的三个阶段

  1. 事件捕获阶段
    当某个事件触发时,文档根节点最先接受到事件,然后根据DOM树结构向具体绑定事件的元素传递。该阶段为父元素截获事件提供了机会。
    事件传递路径为:
    window —> document —> body —> div—> text

  2. 目标阶段
    具体元素已经捕获事件。之后事件开始向根节点冒泡。

  3. 事件冒泡阶段
    该阶段的开始即是事件的开始,根据DOM树结构由具体触发事件的元素向根节点传递。
    事件传递路径:
    text—> div —> body —> document —> window

使用addEventListener函数在事件流的的不同阶段监听事件。
DOMEle.addEventListener(‘事件名称’,handleFn,Boolean);
此处第三个参数Boolean即代表监听事件的阶段;
为true时,在在捕获阶段监听事件,执行逻辑处理;
为false时,在冒泡阶段监听事件,执行逻辑处理。

关于React事件的疑问

1. React事件绑定机制

考虑到浏览器的兼容性和性能问题,React 基于 Virtual DOM 实现了一个SyntheticEvent(合成事件)层,我们所定义的事件处理器会接收到一个SyntheticEvent对象的实例。与原生事件直接在元素上注册的方式不同的是,react的合成事件不会直接绑定到目标dom节点上,用事件委托机制,以队列的方式,从触发事件的组件向父组件回溯直到document节点,因此React组件上声明的事件最终绑定到了document 上。用一个统一的监听器去监听,这个监听器上保存着目标节点与事件对象的映射,当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做的好处:
1.减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 document 上注册一次
2.统一规范,解决 ie 事件兼容问题,简化事件逻辑
3.对开发者友好

React Event的主要四个文件是
ReactBrowerEventEmitter.js
(负责节点绑定的回调函数,该回调函数执行过程中构建合成事件对象,获取组件实例的绑定回调并执行,若有state变更,则重绘组件),
ReactEventListener.js
(负责事件注册和事件分发),
ReactEventEmitter
(负责事件的执行),
EventPluginHub.js
(负责事件的存储)和
ReactEventEmitterMixin.js
(负责事件的合成)。

2. React事件和原生事件有什么区别

带着问题用以下用代码来展示两者的区别:
2. 点击button,最后的输出顺序是什么?
4. B,G 处的type都是啥?

export default class Test extends React.Component {
    componentDidMount() {
        document.querySelector('#btn').addEventListener('click', (e) => {
            console.log('A inner listener')
            setTimeout(() => {
                console.log('B inner listener timer', e.type)
            })
        })
        document.body.addEventListener('click', (e) => {
            console.log('C document listener')
        })
        window.addEventListener('click', (e) => {
            console.log('D window listener')
        })
    }
    outClick(e) {
        setTimeout(() => {
            console.log('E out timer', e.type)
        })
        console.log('F out e', e.type)
    }
    innerClick = (e) => {
        console.log('G inner e',e.type)
        e.stopPropagation()
    }
    render() {
        return (
            <div onClick={this.outClick}>
                <button id="btn" onClick={this.innerClick}>点我</button>
            </div>
        )
    }
}
  1. 最后的输出顺序为 A C G B
  2. B处的type为click,而G处的type为null

响应过程(对应第一问)
我们参照上题,详细说一下事件的响应过程:
由于我们写的几个监听事件addEventListener,都没有给第三个参数,默认值为false,所以在事件捕获阶段,原生的监听事件没有响应,react合成事件只实现了事件冒泡。所以在捕获阶段没有事件响应。
接着到了事件绑定的阶段,button上挂载了原生事件,于是输出"A",setTimeout中的"B"则进入EVENT LOOP。在上一段中,我们提到react的合成事件是挂载到document上,所以“G”没有输出。
之后进入冒泡阶段,到了div上,与上条同理,不会响应outClick,继续向上冒泡。
之后冒泡到了document上,先响应挂载到document的原生事件,输出"c"。之后接着由里向外响应合成事件队列,即输出"G",由于innerClick函数内设置了
e.stopPropagation()
。所以阻止了冒泡,父元素的事件响应函数没有执行。
React合成事件执行e.stopPropagation()不会影响document层级之前的原生事件冒泡。但是会影响document之后的原生事件。所以没有执行body的事件响应函数。
之后再处理EVENT LOOP上的事件,输出'B'.

事件池(对应第二问)
在react中,合成事件被调用后,合成事件对象会被重用,所有属性被置为null

event.constructor.release(event);

所以题目中outClick中通过异步方式访问e.type是取不到任何值的,如果需要保留属性,可以调用event.persist()事件,会保留引用。

总结
(1)命名规范不同
React事件的属性名是采用驼峰形式的,事件处理函数是一个函数;
原生事件通过addEventListener给事件添加事件处理函数
(2)React事件只支持事件冒泡。原生事件通过配置第三个参数,true为事件捕获,false为事件冒泡
(3)事件挂载目标不同
React事件统一挂载到document上;
原生事件挂载到具体的DOM上
(4)this指向不同
原生事件:
1.如果onevent事件属性定义的时候将this作为参数,在函数中获取到该参数是DOM对象。用该方法可以获取当前DOM。
2在方法中直接访问this, this指向当前函数所在的作用域。或者说调用函数的对象。
React事件:
React中this指向一般都期望指向当前组件,如果不绑定this,this一般等于undefined。
React事件需要手动为其绑定this具体原因可以参考文章:
为什么需要在 React 类组件中为事件处理程序绑定 this
(5)事件对象不同
原生js中事件对象是原生事件对象,它存在浏览器兼容性,需要用户自己处理各浏览器兼容问题;
ReactJS中的事件对象是React将原生事件对象(event)进行了跨浏览器包装过的合成事件(SyntheticEvent)。
为了性能考虑,执行完后,合成事件的事件属性将不能再访问

React事件和原生事件的执行顺序,可以混用吗

由上面的代码我们可以理解:
react的所有事件都挂载在document中
当真实dom触发后冒泡到document后才会对react事件进行处理
所以原生的事件会先执行
然后执行react合成事件
最后执行真正在document上挂载的事件

不要将合成事件与原生事件混用。执行React事对象件的e.stopPropagation()可以阻止React事件冒泡。但是不能阻止原生事件冒泡;反之,在原生事件中的阻止冒泡行为,却可以阻止 React 合成事件的传播。因为无法将事件冒泡到document上导致的

React事件如何解决跨浏览器兼容

react事件在给document注册事件的时候也是对兼容性做了处理。

image.png
上面这个代码就是给document注册事件,内部其实也是做了对ie浏览器的兼容做了处理。
其实react内部还处理了很多,比如react合成事件:
React
根据W3C规范 定义了这个合成事件,所以你不需要担心跨浏览器的兼容性问题。
事件处理程序将传递

SyntheticEvent
的实例,这是一个跨浏览器原生事件包装器。 它具有与浏览器原生事件相同的接口,包括
stopPropagation()

preventDefault()
,在所有浏览器中他们工作方式都相同。
每个
SyntheticEvent
对象都具有以下属性:
属性名 类型 描述
bubbles boolean 事件是否可冒泡
cancelable boolean 事件是否可拥有取消的默认动作
currentTarget DOMEventTarget 事件监听器触发该事件的元素(绑定事件的元素)
defaultPrevented boolean 当前事件是否调用了 event.preventDefault()方法
eventPhase number 事件传播的所处阶段[0:Event.NONE-没有事件被处理,1:Event.CAPTURING_PHASE - 捕获阶段,2:被目标元素处理,3:冒泡阶段(Event.bubbles为true时才会发生)]
isTrusted boolean 触发是否来自于用户行为,false为脚本触发
nativeEvent DOMEvent 浏览器原生事件
preventDefault() void 阻止事件的默认行为
isDefaultPrevented() boolean 返回的事件对象上是否调用了preventDefault()方法
stopPropagation() void 阻止冒泡
isPropagationStopped() boolean 返回的事件对象上是否调用了stopPropagation()方法
target DOMEventTarget 触发事件的元素
timeStamp number 事件生成的日期和时间
type string 当前 Event 对象表示的事件的名称,是注册事件的句柄,如,click、mouseover...etc.

React
合成的
SyntheticEvent
采用了事件池,这样做可以大大节省内存,而不会频繁的创建和销毁事件对象。
另外,不管在什么浏览器环境下,浏览器会将该事件类型统一创建为合成事件,从而达到了浏览器兼容的目的。

React stopPropagation 与 stopImmediatePropagation
React 合成事件与原生事件执行顺序图:

3853478932-5a9ff2f3efa39_articlex.png

从图中我们可以得到一下结论:
(1)DOM 事件冒泡到document上才会触发React的合成事件,所以React 合成事件对象的e.stopPropagation,只能阻止 React 模拟的事件冒泡,并不能阻止真实的 DOM 事件冒泡
(2)DOM 事件的阻止冒泡也可以阻止合成事件原因是DOM 事件的阻止冒泡使事件不会传播到document上
(3)当合成事件和DOM 事件 都绑定在document上的时候,React的处理是合成事件应该是先放进去的所以会先触发,在这种情况下,原生事件对象的 stopImmediatePropagation能做到阻止进一步触发document DOM事件
stopImmediatePropagation
:如果有多个相同类型事件的事件监听函数绑定到同一个元素,则当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了 event.stopImmediatePropagation()方法,则剩下的监听函数将不会被执行。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号