React
有着独特的事件机制-合成事件,React
的初学者确定碰到过这种问题,使用event.stopPropagation();
,却仍是没法禁止当前组件的事件冒泡,这就是React
的事件机制的缘由,它并不与DOM
事件相同。react
本文将从源码的角度来解析,React
的事件系统究竟是如何实现的?git
DOM
事件流属于比较基础的知识点,本文不会详细的再叙述,只列出须要一些关键点。W3C标准约定了一个事件的传播过程要通过如下 3 个阶段:github
经过DOM
事件流,咱们常常会用到一直常见的性能优化思路:事件委托。数组
React
事件系统也是基于事件委托这个特性实现的。浏览器
在React
中,除了一些不可冒泡的事件外,其它的事件都不会被绑定在具体的元素上,而是统一被绑定到document
上(17版本以后修改成绑定到React
的根DOM组件上),当事件在具体的DOM
节点上被触发后,最终都会冒泡到document
上,React
根组件上所绑定的统一事件处理程序会将事件分发到具体的组件实例。性能优化
在分发事件以前,React
首先会对事件进行包装,把原生DOM
事件包装成合成事件。markdown
合成事件是React
自定义的事件对象,它在底层抹平了不一样浏览器的差别,在上层面向开发者暴露统一的、稳定的、与DOM
原生事件相同的事件接口。app
虽然合成事件并非原生DOM
事件,但它保存了原生DOM
事件的引用。当你须要访问原生DOM
事件对象时,能够经过合成事件对象的e.nativeEvent
属性获取到原生DOM
事件。dom
事件的绑定是在组件的首次渲染链路的completeWork
方法中完成的。关于首次渲染的流程,能够看我以前的文章异步
写给本身看的React源码解析(一):你的React代码是怎么渲染成DOM的?
completeWork
主要作了三件事情:
再finalizeInitialChildren
方法中,会遍历节点的props
。当遍历到事件相关的props
时,就会触发事件的注册链路。
本文基于16.13版本的React
源码。注意:17版本的事件系统在源码上的改动比较大,源码链路上跟本文并不一致。
在ensureListeningTo
中,会获取当前DOM
中的document
对象,而后经过调用legacyListenToEvent
,将统一的事件监听函数注册到document
上面。
在legacyListenToEvent
中,其实是经过调用legacyListenToTopLevelEvent
来处理事件和document
之间的关系的。 legacyListenToTopLevelEvent
直译过来是“监听顶层的事件”,这里的“顶层”就能够理解为事件委托的最上层,也就是document
节点。
注意:在17版本中,流程中不会存在ensureListeningTo
和legacyListenToEvent
方法,React
会在finalizeInitialChildren
方法下的setInitialProperties
根据节点的tag
类型,传入不一样的参数并调用listenToNonDelegatedEvent
方法。在这个方法里,会直接调用addTrappedEventListener
添加事件到React
的根组件DOM
元素上。
咱们接着来看,最终注册到document
上的并非某一个DOM
节点上对应的具体回调逻辑,而是一个统一的事件分发函数listener
,它的本体是一个dispatchEvent
。
事件的触发其实就是对于dispatchEvent
函数的调用。
咱们根据下面的这个demo来走流程
import React from 'react';
import { useState } from 'react'
function App() {
const [state, setState] = useState(0);
return (
<div className="App"> <div className="container" onClickCapture={() => console.log('捕获通过 div')} onClick={() => console.log('冒泡通过 div')} > <p>{state}</p> <button onClick={() => { setState(state + 1) }}>点击+1</button> </div> </div>
);
}
export default App;
复制代码
这个demo的功能很简单,每次点击按钮都会给state
加1。并给container
这个div上添加了两个点击事件,一个捕获事件,一个冒泡事件。下图是这个demo的fiber
树结构。
收集的逻辑过程在traverseTwoPhase
函数
function traverseTwoPhase(inst, fn, arg) {
// 定义一个 path 数组
var path = [];
while (inst) {
// 将当前节点收集进 path 数组
path.push(inst);
// 向上收集 tag===HostComponent 的父节点
inst = getParent(inst);
}
var i;
// 从后往前,收集 path 数组中会参与捕获过程的节点与对应回调
for (i = path.length; i-- > 0;) {
fn(path[i], 'captured', arg);
}
// 从前日后,收集 path 数组中会参与冒泡过程的节点与对应回调
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
复制代码
traverseTwoPhase
函数作了如下三件事情。
traverseTwoPhase
会以触发事件的目标节点为起点,经过getParent
方法,不断向上寻找tag===HostComponent
的父节点,并将这些节点按顺序收集进path
数组中。tag===HostComponent
的节点是DOM
元素对应的的fiber
节点类型,也就是说只收集DOM元素对应的节点。
按照demo中的fiber
树来讲,最后收集到的节点为div#container
、div.App
及button
节点。
for (i = path.length; i-- > 0;) {
fn(path[i], 'captured', arg);
}
复制代码
path
数组是从子节点出发,向上收集得来的。因此,模拟事件的捕获顺序,须要从后往前遍历path
数组。在遍历的过程当中,fn
函数检测每一个节点的事件回调,若该节点上对应当前事件的捕获回调不为空,那么节点fiber
实例会被收集到合成事件的SyntheticEvent._dispatchInstances
中,事件回调则会被收集到合成事件的SyntheticEvent._dispatchListeners
属性。
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
复制代码
这里功能跟上一步一致,区别只是从前日后来遍历path
数组。
最后,咱们来看下SyntheticEvent
对象上的_dispatchInstances
和_dispatchListeners
。
咱们只要按顺序调用执行回调函数,就可以模拟出DOM
事件流,也就是 “捕获-目标-冒泡”这三个阶段。
上文的源码解析是基于16.13.x
版本的,17版本以后的事件系统,有了挺大的区别。
React
的根组件dom上onScroll
事件再也不冒泡onFocus
和onBlur
事件已在底层切换为原生的focusin
和focusout
事件onClickCapture
如今使用的是实际浏览器中的捕获监听器(合成事件只会存在listenToNonDelegatedEvent
添加的冒泡事件)SyntheticEvent
再也不复用,在点击事件中使用异步方法也将能够获取到点击事件。不须要再使用e.persist()
方法若是本文对你有所帮助,请帮忙点个赞,感谢!