【React进阶系列】史上最全React事件机制详解

React事件机制全览.png

框架总览


  • 😁 DOM事件流的三个阶段
  • 😁 关于React事件的疑问
    • 🆕 React事件绑定机制
    • 🆕 React事件和原生事件有什么区别
    • 🆕 React事件和原生事件的执行顺序,可以混用吗
    • 🆕 React事件如何解决跨浏览器兼容
    • 🆕 React stopPropagation 与 stopImmediatePropagation
  • 😁 从React的事件机制源码看整个流程
    • 🆕 基本流程
    • 🆕 事件注册
    • 🆕 事件触发
  • 😁 总结
  • 😁 站在巨人肩上

DOM事件流的三个阶段

事件流.jpg

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

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

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

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

关于React事件的疑问1.png

关于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事件和原生事件有什么区别

带着问题用以下用代码来展示两者的区别:

  1. 点击button,最后的输出顺序是什么?
  2. 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()方法,则剩下的监听函数将不会被执行。


从React的事件机制源码看整个流程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值