一前言
这篇文章全篇适用于 react
和 react-dom
的 version 17
之前的版本。v17
版本有了重大改动(将时间委托从 document
切换成 root
),这部分内容在文章最后会有讲解
React
事件系统有两类:合成事件和原生事件。
在写React组件是我们很容易绑定一个合成事件,但是在一个组件里面是没有办法去绑定另一个组件的合成事件的,此时原生事件就派上了用场。
除了讲述混合(合成事件与原生事件混用)事件,事件冒泡也是我们经常需要处理的事情,这篇文章结合 React
进行介绍。
正文
一 React事件系统
React
基于Virtual DOM
实现了一个SyntheticEvent
(合成事件)层,我们所定义的事件处理器会接收到一个SyntheticEvent
对象的实例,同样支持事件的冒泡机制,我们可以使用e.stopPropagation()
和e.preventDefault()
来中断它。- 所有事件都自动绑定到最外层上(
document
)。
二 合成事件绑定机制
在 React
底层,主要对合成事件做了两件事:事件委派和自动绑定。
事件委派在使用
React
事件前,一定要熟悉它的事件代理机制。它并不会把事件处理函数直接绑定到真实的节点上,而是把所有事件绑定到结构的最外层,使用一个统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大提升。
自动绑定
在 React
组件中,每个方法的上下文都会指向该组件的实例,即自动绑定 this
为当前组件。而且 React
还会对这种引用进行缓存,以达到 CPU
和内存的最优化。
三 在React中使用原生事件
React
架构下也可以使用原生事件。React
提供了完备的生命周期方法,其中componentDidMount
会在组件已经完成安装并且在浏览器中存在真实的 DOM 后调用,此时我们就可以完成原生事件的绑定。
但是 React
不会自动管理原生事件,所以需要你在卸载组件的时候注销掉原生事件。
四 合成事件与原生事件混用
书中讲到(这里不做过多介绍):
- 不要将合成事件与原生事件混用
- 通过
e.target
判断来避免
重点是下面这段话,这也是我们今天要着重解决的问题:
用reactEvent.nativeEvent.stopPropagation()
来阻止冒泡是不行的。阻止React
事件冒泡的行为只能用于 React 合成事件系统中,且没办法阻止原生事件的冒泡。反之,在原生事件中的阻止冒泡行为,却可以阻止 React 合成事件的传播。
五 React stopPropagation 与 stopImmediatePropagation
事件冒泡机制
通过 React
绑定的事件,其回调函数中的 event
对象,是经过 React
合成的 SyntheticEvent
,与原生的 DOM
事件的 event
不是一回事。准确地说,在 React
中,e.nativeEvent
才是原生 DOM
事件的那个 event
。
React 合成事件与原生事件执行顺序
合成事件触发时机:
从图中我们可以得到一下结论(DOM
指的是 React DOM
):
DOM
事件冒泡到document
上才会触发React
的合成事件,所以React
合成事件对象的e.stopPropagation
,只能阻止React
模拟的事件冒泡,并不能阻止真实的DOM
事件冒泡DOM
事件的阻止冒泡也可以阻止合成事件原因是DOM
事件的阻止冒泡使事件不会传播到document
上- 当合成事件和
DOM
事件 都绑定在document
上的时候,React
的处理是:合成事件是先放进去的所以会先触发,在这种情况下,原生事件对象的stopImmediatePropagation
能做到阻止进一步触发document DOM
事件
事件顺序:
stopImmediatePropagation vs stopPropagation
stopPropagation: 方法阻止捕获和冒泡阶段中当前事件的进一步传播。
stopImmediatePropagation : 如果有多个相同类型事件的事件监听函数绑定到同一个元素,则当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了 event.stopImmediatePropagation()
方法,则剩下的监听函数将不会被执行
总结: stopImmediatePropagation
不仅有阻止冒泡的功能,还有防止一个元素绑定多个事件的功能。
六 React 阻止事件冒泡
React 16 Event Demo
场景:点击按钮出现弹窗,点击弹窗外的区域关闭弹窗
代码:react 16 事件机制
总结
- 阻止合成事件间的冒泡,用
e.stopPropagation()
阻止合成事件与最外层
document
上的事件间的冒泡,用e.nativeEvent.stopImmediatePropagation()
- 由于合成事件的事件委托机制,此时的
e.nativeEvent
指的是原生的document dom event
stopImmediatePropagation
可阻止document dom
再次被绑定事件,这样就不会触发document.addEventListener()
里的事件
- 由于合成事件的事件委托机制,此时的
e.nativeEvent.stopPropagation()
不会生效:e.nativeEvent
指的是原生的document dom event
,阻止它的冒泡没有任何意义,不会阻止document dom
再次被绑定事件- 阻止合成事件与除最外层
document
上的原生事件上的冒泡,可以通过判断e.target
来避免,代码如下:
componentDidMount() {
document.body.addEventListener('click', e => {
if (e.target && e.target.matches('div.code')) {
return;
}
this.setState({ active: false, }); });
}
七 通过源码看本质
事件注册
事件注册即在 document
节点,将 React
事件转化为 DOM
原生事件,并注册回调。
// enqueuePutListener 负责事件注册。
// inst:注册事件的 React 组件实例
// registrationName:React 事件,如:onClick、onChange
// listener:和事件绑定的 React 回调方法,如:handleClick、handleChange
// transaction:React 事务流,不懂没关系,不太影响对事件系统的理解
function enqueuePutListener(inst, registrationName, listener, transaction) {
... ...
// doc 为找到的 document 节点
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
// 事件注册
listenTo(registrationName, doc);
// 事件存储,之后会讲到,即存储事件回调方法
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
});
}
来看事件注册的具体代码,如何在 document 上绑定 DOM 原生事件。
// 事件注册
// registrationName:React 事件名,如:onClick、onChange
// contentDocumentHandle:要将事件绑定到的 DOM 节点
listenTo: function (registrationName, contentDocumentHandle) {
// document
var mountAt = contentDocumentHandle;
// React 事件和绑定在根节点的 topEvent 的转化关系,如:onClick -> topClick
var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];
for (var i = 0; i < dependencies.length; i++){
// 内部有大量判断浏览器兼容等的步骤,提取一下核心代码
var dependency = dependencies[i];
// topEvent 和原生 DOM 事件的转化关系
if (topEventMapping.hasOwnProperty(dependency)) {
// 三个参数为 topEvent、原生 DOM Event、Document
// 将事件绑定到冒泡阶段
trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
}
}
}
来看将事件绑定到冒泡阶段的具体代码:
// 三个参数为 topEvent、原生 DOM Event、Document(挂载节点)
trapBubbledEvent: function (topLevelType, handlerBaseName, element) {
if (!element) {
return null;
}
return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
}
// 三个参数为 Document(挂载节点)、原生 DOM Event、事件绑定函数
listen: function listen(target, eventType, callback) {
// 去除浏览器兼容部分,留下核心后
target.addEventListener(eventType, callback, false);
// 返回一个解绑的函数
return {
remove: function remove() {
target.removeEventListener(eventType, callback, false);
}
}
}
在 listen
方法中,我们终于发现了熟悉的 addEventListener
这个原生事件注册方法。只有 document
节点才会调用这个方法,故仅仅只有 document
节点上才有 DOM 事件。这大大简化了 DOM
事件逻辑,也节约了内存。
事件存储
事件注册之后,还需要将事件绑定的回调函数存储下来。这样,在触发事件后才能去寻找相应回调来触发。在一开始的代码中,我们已经看到,是使用 putListener 方法来进行事件回调存储。
// inst:注册事件的 React 组件实例
// registrationName:React 事件,如:onClick、onChange
// listener:和事件绑定的 React 回调方法,如:handleClick、handleChange
putListener: function (inst, registrationName, listener) {
// 核心代码如下
// 生成每个组件实例唯一的标识符 key
var key = getDictionaryKey(inst);
// 获取某种 React 事件在回调存储银行中的对象
var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
bankForRegistrationName[key] = listener;
}
事件执行
每次触发事件都会执行根节点上
addEventListener
注册的回调,也就是ReactEventListener.dispatchEvent
方法,事件分发入口函数。该函数的主要业务逻辑如下:- 找到事件触发的
DOM
和React Component
- 从该
React Component
,调用findParent
方法,遍历得到所有父组件,存在数组中。 从该组件直到最后一个父组件,根据之前事件存储,用 React 事件名 + 组件 key,找到对应绑定回调方法,执行,详细过程为:
- 根据 DOM 事件构造 React 合成事件。
- 将合成事件放入队列。
- 批处理队列中的事件(包含之前未处理完的,先入先处理)
- 找到事件触发的
React合成事件的冒泡并不是真的冒泡,而是节点的遍历。
八 React 17 开始
介绍
React 17
之前是将合成事件委托在 document
元素上的,17 之后将合成事件委托在 root
元素上了。这样做的好处是页面上可以共存多个 react
版本了,以后做迁移的时候也可以是渐进式的了。
点击 Button 事件触发顺序:
16 到 17 事件系统影响
这里有个需求场景:点击按钮出现弹窗,点击弹窗外的区域关闭弹窗。这里通常需要绑定 document
原生事件,在点击的时候处理关闭。
我们要检查的是:
- 确认下有无上面的需求场景,迁移之后验证一下是否生效(一般来说没有问题,如果没有情况2)
- 检查下
root
节点下有没有绑定事件(root.addEventListener
)。如果绑定事件里面有e.stopPropagation
(16里面会阻止触发合成事件,17里面则不会阻止),需要确定下影响
react 17 : https://codepen.io/specialCod...
后记
个人觉得 stopImmediatePropagation
非常有用,很有必要阻止合成事件冒泡到 DOM
document
上,原因是:
- 合成事件本来就绑定在
document
上,完全可以获取这个document
stopImmediatePropagation
可以阻止触发的document DOM
上的事件,这十分有必要- 不会阻止
DOM
上的事件冒泡到document DOM
参考资料:《深入React技术栈》@陈屹著
参考链接:
1.https://juejin.im/post/59db6e...
2.https://github.com/youngwind/...
3.https://zhuanlan.zhihu.com/p/...
4.https://zhuanlan.zhihu.com/p/...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。