React 有着自成一派的事件系统。
React 版本为 16.13.x
一个页面会绑定很多的事件,而页面接收事件的顺序,就是事件流。
W3C 标准约定,一个事件传播需要经过3个阶段:
首先经历捕获过程,事件从最外层逐层进入到内层元素,持续到事件抵达目标元素为止; 此时事件流进入到 目标阶段,事件被目标元素所接收; 然后事件会被回弹,进入到冒泡阶段,再从内部一层一层走回去。
在原生 DOM 中,事件委托也是一种重要的性能优化手段。
问:在这段 HTML 渲染出的界面里,我希望做到点击每一个 li 元素,都能输出它内在的文本内容。你会怎么做?
上述方法为每个 li 元素绑定了点击事件,开销很大。使用 事件冒泡,在 ul 元素上处理。 ul 元素通过事件对象中的 target 属性,拿到实际触发事件的元素,针对这个元素分发事件处理的逻辑。
e.target 这个属性,它指的是触发事件的具体目标,它记录着事件的源头。不管监听函数在哪一层执行,只要拿到 e.target,就相当于拿到了真正触发的那个元素。
利用事件的冒泡特性,把多个子元素的同一类型的监听逻辑,合并到父元素上通过监听函数来管理行为,就是事件委托。
通过事件委托,可以减少内存开销、简化注册步骤,提高开发效率。
React 的事件系统沿用了事件委托的思想。除少数不可冒泡的事件无法被事件系统处理外,绝大多数事件都统一被绑定在页面的 document 上。 当事件在具体的 DOM 节点上被触发后,最终都会冒泡到 document 上,document 上所绑定的统一事件处理程序会将事件分发到具体的组件实例。
在分发事件之前,React 首先会对事件进行包装,把原生 DOM 事件包装成合成事件。
合成事件是 React 自定义的事件对象,符合 W3C 规范,在底层抹平了不同浏览器的差异,在上层面向开发者暴露统一的、稳定的、与 DOM 原生事件相同的事件接口。
合成事件保存了原生 DOM 事件的引用,当需要访问原生 DOM 事件对象时,可以通过合成事件对象的 e.nativeEvent 属性获取。
事件的绑定是在组件挂载过程中完成的,即 completeWork
中完成的。
completeWork 有三个关键动作:创建 DOM 节点、将 DOM 节点插入到 DOM 树中、为 DOM 节点设置属性。
其中 为 DOM 节点 设置属性 环节,会遍历 FiberNode
的 props key。
当遍历到事件相关的 props
时,就会触发事件的注册链路。
工作流:
completeWork
-> createInstance
创建 DOM 节点,appendAllChildren
将 DOM 节点插入 DOM 树 ->
finalizeInitialChildren
设置 DOM 节点的属性 -> setInitialProperties
->
setInitialDOMProperties
设置 DOM 节点的初始化属性 -> ensureListeningTo
进入事件监听的注册逻辑 ->
判断是捕获还是冒泡 -> 捕获:trapCapturedEvent
/ 冒泡:trapBubbleEvent
->
addTrappedEventListener
将事件注册到 document
上
所以 事件的注册过程是由 ensureListeningTo
函数开启的。在 ensureListeningTo
中,尝试获取当前 DOM 结构中的根节点(document 对象),
然后通过调用 legacyListenToEvent
,将统一的事件监听函数注册到 document
上面。
legacyListenToEvent
实际上是通过调用 legacyListenToTopLevelEvent
来处理事件和 document
之间的关系的。
legacyListenToTopLevelEvent
函数中有一个 listenerMap
来记录 当前 document
已经监听了哪些事件。
会使用 listenerMap.has(topLevelType)
判断是否继续执行。
因此,多次调用同一个事件的监听,也只会在 document
上注册一次。并且最终注册到 document
上的并不是某一个 DOM 节点上对应的具体回调逻辑,而是一个统一的事件分发函数。
addEventListener
是原生 DOM 里专门用来注册事件监听器的接口。
eventType
表示事件类型;
listener
表示最终注册到 document
上的一个统一的事件分发函数;
listener
可能为下面三个函数之一:
dispatchDiscreteEvent
dispatchUserBlockingUpdate
dispatchEvent
不过最后都是通过调用 dispatchEvent
来执行事件分发的。
本质上是对 dispatchEvent
函数的调用。
核心工作流:
事件触发,冒泡至 document
-> 执行 dispatchEvent
-> 创建事件对应的合成事件对象(SyntheticEvent
)->
收集事件在捕获阶段所波及的回调函数和对应的节点实例 -> 收集事件在冒泡阶段所波及的回调函数和对应的节点实例 ->
将收集回来的回调按顺序执行,执行时 SyntheticEvent
会作为如惨被传入每个回调
收集过程对应的源码为 traverseTwoPhase
函数,
需要注意的是,当前事件对应的 SyntheticEvent 实例有且仅有一个, 因此在模拟捕获和模拟冒泡这两个过程中, 收集到的实例会被推入同一个 SyntheticEvent._dispatchInstances, 收集到的事件回调也会被推入同一个 SyntheticEvent._dispatchListeners。
React 事件系统的设计动机是什么?
对 React 来说,事件委托主要的作用应该在于帮助 React 实现了对所有事件的中心化管控。