视频1 视频21 视频41 视频61 视频文章1 视频文章21 视频文章41 视频文章61 推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37 推荐39 推荐41 推荐43 推荐45 推荐47 推荐49 关键词1 关键词101 关键词201 关键词301 关键词401 关键词501 关键词601 关键词701 关键词801 关键词901 关键词1001 关键词1101 关键词1201 关键词1301 关键词1401 关键词1501 关键词1601 关键词1701 关键词1801 关键词1901 视频扩展1 视频扩展6 视频扩展11 视频扩展16 文章1 文章201 文章401 文章601 文章801 文章1001 资讯1 资讯501 资讯1001 资讯1501 标签1 标签501 标签1001 关键词1 关键词501 关键词1001 关键词1501 专题2001
对React事件系统的解析
2020-11-27 19:33:20 责编:小采
文档


5.React stopPropagation 与 stopImmediatePropagation

事件冒泡机制:

通过 React 绑定的事件,其回调函数中的 event 对象,是经过 React 合成的 SyntheticEvent,与原生的 DOM 事件的 event 不是一回事。准确地说,在 React 中,e.nativeEvent 才是原生 DOM 事件的那个 event。


React 合成事件与原生事件执行顺序图:

从图中我们可以得到一下结论:
(1)DOM 事件冒泡到document上才会触发React的合成事件,所以React 合成事件对象的e.stopPropagation,只能阻止 React 模拟的事件冒泡,并不能阻止真实的 DOM 事件冒泡
(2)DOM 事件的阻止冒泡也可以阻止合成事件原因是DOM 事件的阻止冒泡使事件不会传播到document上
(3)当合成事件和DOM 事件 都绑定在document上的时候,React的处理是合成事件应该是先放进去的所以会先触发,在这种情况下,原生事件对象的 stopImmediatePropagation能做到阻止进一步触发document DOM事件


stopImmediatePropagation :如果有多个相同类型事件的事件监听函数绑定到同一个元素,则当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了 event.stopImmediatePropagation()方法,则剩下的监听函数将不会被执行

6.React 阻止冒泡总结

(1)阻止合成事件间的冒泡,用e.stopPropagation();
(2)阻止合成事件与最外层document上的事件间的冒泡,用e.nativeEvent.stopImmediatePropagation();
(3)阻止合成事件与除最外层document上的原生事件上的冒泡,通过判断e.target来避免,代码如下:

componentDidMount() { 
document.body.addEventListener('click', e => { 
 if (e.target && e.target.matches('p.code')) { 
 return; 
 } 
 this.setState({ active: false, }); }); 
 }

7.通过源码看本质

7-1 事件注册
事件注册即在 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 事件逻辑,也节约了内存。

7-2.事件存储
事件注册之后,还需要将事件绑定的回调函数存储下来。这样,在触发事件后才能去寻找相应回调来触发。在一开始的代码中,我们已经看到,是使用 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;
}

7-3.事件执行

  • 每次触发事件都会执行根节点上 addEventListener 注册的回调,也就是 ReactEventListener.dispatchEvent 方法,事件分发入口函数。该函数的主要业务逻辑如下:

  • 根据 DOM 事件构造 React 合成事件。

  • 将合成事件放入队列。

  • 批处理队列中的事件(包含之前未处理完的,先入先处理)

  • 找到事件触发的 DOM 和 React Component

  • 从该 React Component,调用 findParent 方法,遍历得到所有父组件,存在数组中。

  • 从该组件直到最后一个父组件,根据之前事件存储,用 React 事件名 + 组件 key,找到对应绑定回调方法,执行,详细过程为:

  • React合成事件的冒泡并不是真的冒泡,而是节点的遍历。

    三 后记

    个人觉得stopImmediatePropagation非常有用,很有必要阻止合成事件冒泡到DOM document上,原因是:

    1.合成事件本来就绑定在document上,完全可以获取这个document
    2.stopImmediatePropagation可以阻止触发的document DOM上的事件,这十分有必要
    3.不会阻止DOM 上的事件冒泡到document DOM

    下载本文
    显示全文
    专题