react-事件系统0-最开始写的事件系统博客

loading 2022年12月01日 135次浏览

1. 事件代理

1.1 代理节点

react17之前,react的事件并非绑定到具体的DOM节点上,而是统一绑定在了document上,然后由统一的事件监听器去监听事件的触发。

但是在react17中,事件委托的节点从document更改为react树的根DOM容器

这一改动的出发点是如果页面中存在多个React应用,由于他们都会在顶层document注册事件处理器,如果你在一个React子应用的React事件中调用了e.stopPropagation(),无法阻止事件冒泡到外部树,因为真实的事件早已传播到document。

而将事件委托在React应用的根DOM容器则可以避免这样的问题,减少了多个React应用并存可能产生的问题,并且事件系统的运行也更贴近现在浏览器的表现。

1.2 代理阶段

react16和react17在事件代理阶段也有所差别。

react16中对document的事件委托都在冒泡阶段

当事件冒泡到document后触发绑定的回调函数,在回调函数中重新模拟一次捕获-冒泡的行为,所以React事件中的e.stopPropagation()无法阻止原生事件的捕获和冒泡,因为原生事件的捕获和冒泡已经执行完了。

react16中想要阻止冒泡只能通过e.preventDefault() 不能通过e.stopPropagation(),而且事件的执行顺序为原生事件先执行,合成事件后执行,合成事件会冒泡绑定到 document 上,所以尽量避免原生事件与合成事件混用,如果原生事件阻止冒泡,可能会导致合成事件不执行,因为需要冒泡到document 上合成事件才会执行。

react17中对react树根DOM容器的事件委托分别在捕获阶段和冒泡阶段

当根容器接收到捕获事件时,先触发一次react事件的捕获阶段,然后再执行原生事件的捕获传播。所以React事件的捕获阶段调用e.stopPropagation()能阻止原生事件的传播。

当根容器接受到冒泡事件时,会触发一次React事件的冒泡阶段,此时原生事件的冒泡传播已经传播到根了,所以React事件的冒泡阶段调用e.stopPropagation()不能阻止原生事件向根容器的传播,但是能阻止根容器到页面顶层的传播。

在原生scroll里,scroll是不存在冒泡阶段的,但是React16中模拟了scroll的冒泡阶段,React17中将此特性去除,避免了当一个嵌套且可滚动的元素在其父元素触发事件时造成混乱。

2. 合成事件

2.1 概念和优点

React合成事件机制:

React并不是将click事件直接绑定在dom上面,而是采用事件冒泡的形式冒泡到document上面,然后React将事件封装给正式的函数处理运行和处理。

其优点如下:

  • 兼容所有浏览器,兼容性好
  • 将事件都代理到root上,减少了事件监听器的创建,节省内存
  • 方便react进行统一管理
    对于原生事件来说,浏览器会监听事件是否被触发,当事件触发时会创建一个事件对象,当多个事件被触发时就会创建多个事件对象,这样存在内部分配的问题。
    对于合成事件来说,有一个专门事件池来管理事件的创建和销毁,当需要使用事件时,就会在事件池中复用对象,事件回调结束后,再销毁事件对象上的属性,以便于下次再复用对象。这样做就不会为每个事件都创建一个事件对象,减少了内存的消耗,提升性能。

事件池在react17中已被去除,只在react16及之前版本被使用。

2.2 原理

  • 当用户在为事件(比如onClick)添加处理函数时,react并没有将该事件绑定到DOM上
  • react在root处监听所有支持的事件,当事件发生并冒泡到document处时,react会将事件交给合成事件层SyntheticEvent处理
  • 然后通过统一的分发函数dispatchEvent将封装的事件内容交给对应处理函数执行

3. 事件注册

事件注册过程包含两件事:在root上注册事件,存储事件

3.1 在root上注册事件

在React组件挂载阶段,根据组件内声明的事件类型(onClick、onChange等),在root上通过addEventListener注册事件,并指定统一的回调函数dispatchEvent

在root上不管注册了啥事件,都具有统一的回调函数。因此对于同一种事件类型,不论在root上注册了几次,最终也只会保留一个实例,以减少内存消耗。

比如这个例子,事件类型是onClick,react会指定统一的回调函数,所以最终只会在root上保留一个click事件,类似于root.addEventListener('click', dispatchEvent)。

function TestComponent() {
  handleFatherClick=()=>{
		// ...
  }
 
  handleChildClick=()=>{
		// ...
  }
 
  return <div className="father" onClick={this.handleFatherClick}>
	<div className="child" onClick={this.handleChildClick}>child </div>
  </div>
}

3.2 存储事件

React在内部维护了一个映射表来记录事件与组件的事件处理函数的对应关系,根据事件类型分类存储,例如click事件就统一存储到一个对象中。

存储采用键值对的形式,key代表组件唯一标识,value代表事件的回调函数。

当某个事件触发时,React根据映射表将事件分派给指定的事件处理函数。当一个组件挂载与卸载时,相应的事件处理函数会自动被添加到事件监听器的内部映射表中或从表中删除。这样做简化了事件处理和回收机制,大幅提升了效率。

3.3 事件注册总结

  • 首先React生成要挂载的组件的虚拟DOM
  • 然后处理组件的props,判断props内是否有声明为事件的属性比如 onClick,onChange,这个时候得到事件类型 click,change 和对应的事件处理函数
  • 将这些事件在root上注册
  • 在组件挂载完成后,将事件处理函数存储到映射表中

4. 事件触发

此处react17往后和react16有了变化,见1.2中代理阶段的区别描述。这里就先把react16的写一下。

react事件触发只发生在DOM的冒泡阶段(注意这里都是react16)

4.1 流程

  • 触发事件,开始DOM事件流:事件捕获阶段、处于目标阶段、事件冒泡阶段
  • 当事件冒泡到document,触发统一的事件回调函数模拟捕获-冒泡
  • 根据原生事件对象找到事件触发节点对应组件
  • 开始合成事件
  • 批量执行合成事件内的回调函数
  • 如果没有阻止冒泡,会将继续进行DOM事件流的冒泡(从 document 到 window),否则结束事件触发

4.2 例子

    handleFatherClick=(e)=>{
        console.log('father click');
    }

    handleChildClick=(e)=>{
        console.log('child click');
    }

    render(){
        return <div className="box">
                    <div className="father" onClick={this.handleFatherClick}> father
                        <div className="child" onClick={this.handleChildClick}>child </div>
                    </div>
               </div>
    }

当我们点击child div时,会同时触发father的click事件。

在点击了child div时,浏览器会捕获到这个事件,然后经过冒泡,事件被冒泡到document上,交给统一事件处理函数dispatchEvent对事件进行分发,根据之前在listenerBank存储的键值对找到触发事件的组件,获取到触发这个事件的元素,遍历这个元素的所有父元素,依次对每一级元素进行处理。构造合成事件,将每一级的合成事件存储在eventQueue事件队列中,然后批量执行存储的回调函数。