React中阻止事件冒泡的问题详析


Posted in Javascript onApril 12, 2019

前言

最近在研究react、redux等,网上找了很久都没有完整的答案,索性自己整理下,这篇文章就来给大家介绍了关于React阻止事件冒泡的相关内容,下面话不多说了,来一起看看详细的介绍吧

在正式开始前,先来看看 JS 中事件的触发与事件处理器的执行。

JS 中事件的监听与处理

事件捕获与冒泡

DOM 事件会先后经历 捕获 与 冒泡 两个阶段。捕获即事件沿着 DOM 树由上往下传递,到达触发事件的元素后,开始由下往上冒泡。

IE9 及之前的版本只支持冒泡

                  |  A
 -----------------|--|-----------------
 | Parent         |  |                |
 |   -------------|--|-----------     |
 |   |Children    V  |          |     |
 |   ----------------------------     |
 |                                    |
 --------------------------------------

事件处理器

默认情况下,事件处理器是在事件的冒泡阶段执行,无论是直接设置元素的 onclick 属性还是通过 EventTarget.addEventListener() 来绑定,后者在没有设置 useCapture 参数为 true 的情况下。

考察下面的示例:

<button onclick="btnClickHandler(event)">CLICK ME</button>
<script>
 document.addEventListener("click", function(event) {
 console.log("document clicked");
 });

 function btnClickHandler(event) {
 console.log("btn clicked");
 }
</script>

输出:

btn clicked
document clicked

阻止事件的冒泡

通过调用事件身上的 stopPropagation() 可阻止事件冒泡,这样可实现只我们想要的元素处理该事件,而其他元素接收不到。

<button onclick="btnClickHandler(event)">CLICK ME</button>
<script>
 document.addEventListener(
 "click",
 function(event) {
 console.log("document clicked");
 },
 false
 );

 function btnClickHandler(event) {
 event.stopPropagation();
 console.log("btn clicked");
 }
</script>

输出:

btn clicked

一个阻止冒泡的应用场景

常见的弹窗组件中,点击弹窗区域之外关闭弹窗的功能,可通过阻止事件冒泡来方便地实现,而不用这种方式的话,会引入复杂的判断当前点击坐标是否在弹窗之外的复杂逻辑。

document.addEventListener("click", () => {
 // close dialog
});

dialogElement.addEventListener("click", event => {
 event.stopPropagation();
});

但如果你尝试在 React 中实现上面的逻辑,一开始的尝试会让你怀疑人生。

React 下事件执行的问题

了解了 JS 中事件的基础,一切都没什么难的。在引入 React 后,,事情开始起变化。将上面阻止冒泡的逻辑在 React 里实现一下,代码大概像这样:

function App() {
 useEffect(() => {
 document.addEventListener("click", documentClickHandler);
 return () => {
 document.removeEventListener("click", documentClickHandler);
 };
 }, []);

 function documentClickHandler() {
 console.log("document clicked");
 }

 function btnClickHandler(event) {
 event.stopPropagation();
 console.log("btn clicked");
 }

 return <button onClick={btnClickHandler}>CLICK ME</button>;
}

输出:

btn clicked
document clicked

document 上的事件处理器正常执行了,并没有因为我们在按钮里面调用 event.stopPropagation() 而阻止。

那么问题出在哪?

React 中事件处理的原理

考虑下面的示例代码并思考点击按钮后的输出。

import React, { useEffect } from "react";
import ReactDOM from "react-dom";

window.addEventListener("click", event => {
 console.log("window");
});

document.addEventListener("click", event => {
 console.log("document:bedore react mount");
});

document.body.addEventListener("click", event => {
 console.log("body");
});

function App() {
 function documentHandler() {
 console.log("document within react");
 }

 useEffect(() => {
 document.addEventListener("click", documentHandler);
 return () => {
 document.removeEventListener("click", documentHandler);
 };
 }, []);

 return (
 <div
 onClick={() => {
 console.log("raect:container");
 }}
 >
 <button
 onClick={event => {
  console.log("react:button");
 }}
 >
 CLICK ME
 </button>
 </div>
 );
}

ReactDOM.render(<App />, document.getElementById("root"));

document.addEventListener("click", event => {
 console.log("document:after react mount");
});

现在对代码做一些变动,在 body 的事件处理器中把冒泡阻止,再思考其输出。

document.body.addEventListener("click", event => {
+ event.stopPropagation();
 console.log("body");
});

下面是剧透环节,如果你懒得自己实验的话。

点击按钮后的输出:

body
document:bedore react mount
react:button
raect:container
document:after react mount
document within react
window

bdoy 上阻止冒泡后,你可能会觉得,既然 body 是按钮及按钮容器的父级,那么按钮及容器的事件会正常执行,事件到达 body 后, body 的事件处理器执行,然后就结束了。 document 上的事件处理器一个也不执行。

事实上,按钮及按钮容器上的事件处理器也没执行,只有 body 执行了。

输出:

body

通过下面的分析,你能够完全理解上面的结果。

SyntheticEvent

React 有自身的一套事件系统,叫作 SyntheticEvent。叫什么不重要,实现上,其实就是通过在 document 上注册事件代理了组件树中所有的事件(facebook/react#4335),并且它监听的是 document 冒泡阶段。你完全可以忽略掉 SyntheticEvent 这个名词,如果觉得它有点让事情变得高大上或者增加了一些神秘的话。

除了事件系统,它有自身的一套,另外还需要理解的是,界面上展示的 DOM 与我们代码中的 DOM 组件,也是两样东西,需要在概念上区分开来。

所以,当你在页面上点击按钮,事件开始在原生 DOM 上走捕获冒泡流程。React 监听的是 document 上的冒泡阶段。事件冒泡到 document 后,React 将事件再派发到组件树中,然后事件开始在组件树 DOM 中走捕获冒泡流程。

现在来尝试理解一下输出结果:

  • 事件最开始从原生 DOM 按钮一路冒泡到 body,body 的事件处理器执行,输出 body。注意此时流程还没进入 React。为什么?因为 React 监听的是 document 上的事件。
  • 继续往上事件冒泡到 document。
    • 事件到达 document 之后,发现 document 上面一共绑定了三个事件处理器,分别是代码中通过 document.addEventListener ReactDOM.render 前后调用的,以及一个隐藏的事件处理器,是 ReactDOM 绑定的,也就是前面提到的 React 用来代理事件的那个处理器。
    • 同一元素上如果对同一类型的事件绑定了多个处理器,会按照绑定的顺序来执行。
    • 所以 ReactDOM.render 之前的那个处理器先执行,输出 document:before react mount
    • 然后是 React 的事件处理器。此时,流程才真正进入 React,走进我们的组件。组件里面就好理解了,从 button 冒泡到 container,依次输出。
    • 最后 ReactDOM.render 之后的那个处理器先执行,输出 document:after react mount
  • 事件完成了在 document 上的冒泡,往上到了 window,执行相应的处理器并输出 window。

理解 React 是通过监听 document 冒泡阶段来代理组件中的事件,这点很重要。同时,区分原生 DOM 与 React 组件,也很重要。并且,React 组件上的事件处理器接收到的 event 对象也有别于原生的事件对象,不是同一个东西。但这个对象上有个 nativeEvent 属性,可获取到原生的事件对象,后面会用到和讨论它。

紧接着的代码的改动中,我们在 body 上阻止了事件冒泡,这样事件在 body 就结束了,没有到达 document,那么 React 的事件就不会被触发,所以 React 组件树中,按钮及容器就没什么反应。如果没理解到这点,光看表象还以为是 bug。

进而可以理解,如果在 ReactDOM.render() 之前的的 document 事件处理器上将冒泡结束掉,同样会影响 React 的执行。只不过这里需要调用的不是 event.stopPropagation() ,而是 event.stopImmediatePropagation()

document.addEventListener("click", event => {
+ event.stopImmediatePropagation();
 console.log("document:bedore react mount");
});

输出:

body
document:bedore react mount

stopImmediatePropagation 会产生这样的效果,即,如果同一元素上同一类型的事件(这里是 click)绑定了多个事件处理器,本来这些处理器会按绑定的先后来执行,但如果其中一个调用了 stopImmediatePropagation,不但会阻止事件冒泡,还会阻止这个元素后续其他事件处理器的执行。

所以,虽然都是监听 document 上的点击事件,但 ReactDOM.render() 之前的这个处理器要先于 React,所以 React 对 document 的监听不会触发。

解答前面按钮未能阻止冒泡的问题

如果你已经忘了,这是相应的代码及输出。

到这里,已经可以解答为什么 React 组件中 button 的事件处理器中调用 event.stopPropagation() 没有阻止 document 的点击事件执行的问题了。因为 button 事件处理器的执行前提是事件达到 document 被 React 接收到,然后 React 将事件派发到 button 组件。既然在按钮的事件处理器执行之前,事件已经达到 document 了,那当然就无法在按钮的事件处理器进行阻止了。

问题的解决

要解决这个问题,这里有不止一种方法。

用 window 替换 document

来自 React issue 回答中提供的这个方法是最快速有效的。使用 window 替换掉 document 后,前面的代码可按期望的方式执行。

function App() {
 useEffect(() => {
+ window.addEventListener("click", documentClickHandler);
 return () => {
+  window.removeEventListener("click", documentClickHandler);
 };
 }, []);

 function documentClickHandler() {
 console.log("document clicked");
 }

 function btnClickHandler(event) {
 event.stopPropagation();
 console.log("btn clicked");
 }

 return <button onClick={btnClickHandler}>CLICK ME</button>;
}

这里 button 事件处理器上接到到的 event 来自 React 系统,也就是 document 上代理过来的,所以通过它阻止冒泡后,事件到 document 就结束了,而不会往上到 window。

Event.stopImmediatePropagation()

组件中事件处理器接收到的 event 事件对象是 React 包装后的 SyntheticEvent 事件对象。但可通过它的 nativeEvent 属性获取到原生的 DOM 事件对象。通过调用这个原生的事件对象上的 stopImmediatePropagation() 方法可达到阻止冒泡的目的。

function btnClickHandler(event) {
+ event.nativeEvent.stopImmediatePropagation();
 console.log("btn clicked");
}

至于原理,其实前面已经有展示过。React 在 render 时监听了 document 冒泡阶段的事件,当我们的 App 组件执行时,准确地说是渲染完成后(useEffect 渲染完成后执行),又在 document 上注册了 click 的监听。此时 document 上有两个事件处理器了,并且组件中的这个顺序在 React 后面。

当调用 event.nativeEvent.stopImmediatePropagation() 后,阻止了 document 上同类型后续事件处理器的执行,达到了想要的效果。

但这种方式有个缺点很明显,那就是要求需要被阻止的事件是在 React render 之后绑定,如果在之前绑定,是达不到效果的。

通过元素自身来绑定事件处理器

当绕开 React 直接通过调用元素自己身上的方法来绑定事件时,此时走的是原生 DOM 的流程,都没在 React 的流程里面。

function App() {
 const btnElement = useRef(null);
 useEffect(() => {
 document.addEventListener("click", documentClickHandler);
 if (btnElement.current) {
  btnElement.current.addEventListener("click", btnClickHandler);
 }

 return () => {
  document.removeEventListener("click", documentClickHandler);
  if (btnElement.current) {
  btnElement.current.removeEventListener("click", btnClickHandler);
  }
 };
 }, []);

 function documentClickHandler() {
 console.log("document clicked");
 }

 function btnClickHandler(event) {
 event.stopPropagation();
 console.log("btn clicked");
 }

 return <button ref={btnElement}>CLICK ME</button>;
}

很明显这样是能解决问题,但你根本不会想要这样做。代码丑陋,不直观也不易理解。

结论

注意区分 React 组件的事件及原生 DOM 事件,一般情况下,尽量使用 React 的事件而不要混用。如果必需要混用比如监听 document,window 上的事件,处理 mousemove,resize 等这些场景,那么就需要注意本文提到的顺序问题,不然容易出 bug。

相关资源

  • e.stopPropagation() seems to not be working as expect. #4335
  • ReactJS SyntheticEvent stopPropagation() only works with React events?
  • Event.stopImmediatePropagation()
  • SyntheticEvent

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对三水点靠木的支持。

Javascript 相关文章推荐
JS的千分位算法实现思路
Jul 31 Javascript
JQuery实现绚丽的横向下拉菜单
Dec 19 Javascript
node.js中的http.request方法使用说明
Dec 14 Javascript
ECMAScript 6即将带给我们新的数组操作方法前瞻
Jan 06 Javascript
js焦点文字滚动效果代码分享
Aug 25 Javascript
浅谈javascript函数式编程
Sep 06 Javascript
使用vue.js制作分页组件
Jun 27 Javascript
JavaScript构建自己的对象示例
Nov 29 Javascript
bootstrap表单按回车会自动刷新页面的解决办法
Mar 08 Javascript
angular学习之从零搭建一个angular4.0项目
Jul 10 Javascript
JavaScript递归函数定义与用法实例分析
Jan 24 Javascript
jQuery实现html可联动的百分比进度条
Mar 26 jQuery
TypeScript中的方法重载详解
Apr 12 #Javascript
vue-cli 3.x配置跨域代理的实现方法
Apr 12 #Javascript
解决微信小程序调用moveToLocation失效问题【超简单】
Apr 12 #Javascript
详解Bootstrap 学习(一)入门
Apr 12 #Javascript
vue组件中iview的modal组件爬坑问题之modal的显示与否应该是使用v-show
Apr 12 #Javascript
vue+echarts实现可拖动节点的折线图(支持拖动方向和上下限的设置)
Apr 12 #Javascript
详解Vue中使用插槽(slot)、聚类插槽
Apr 12 #Javascript
You might like
二次元帅气男生排行榜,只想悄悄收藏系列
2020/03/04 日漫
编写PHP的安全策略
2006/10/09 PHP
PHP生成静态页面详解
2006/11/19 PHP
php二维数组排序方法(array_multisort usort)
2013/12/25 PHP
新浪SAE云平台下使用codeigniter的数据库配置
2014/06/12 PHP
利用PHP判断是手机移动端还是PC端访问的函数示例
2017/12/14 PHP
JavaScript库 开发规则
2009/01/31 Javascript
jQuery1.6 正式版发布并提供下载
2011/05/05 Javascript
IE6/7/8/9不支持exec的简写方式
2011/05/25 Javascript
js播放wav文件(源码)
2013/04/22 Javascript
优化Jquery,提升网页加载速度
2013/11/14 Javascript
JavaScript+CSS控制打印格式示例介绍
2014/01/07 Javascript
jQuery函数的第二个参数获取指定上下文中的DOM元素
2014/05/19 Javascript
js实现有时间限制消失的图片方法
2015/02/27 Javascript
js实现带圆角的两级导航菜单效果代码
2015/08/24 Javascript
jQuery生成假加载动画效果
2016/12/01 Javascript
jQuery+Ajax请求本地数据加载商品列表页并跳转详情页的实现方法
2017/07/12 jQuery
jquery实现左右轮播图效果
2017/09/28 jQuery
推荐15个最好用的JavaScript代码压缩工具
2019/02/13 Javascript
javascript实现移动端触屏拖拽功能
2020/07/29 Javascript
Python模拟鼠标点击实现方法(将通过实例自动化模拟在360浏览器中自动搜索python)
2017/08/23 Python
python实现数据导出到excel的示例--普通格式
2018/05/03 Python
详解Python3中的迭代器和生成器及其区别
2018/10/09 Python
python3正则提取字符串里的中文实例
2019/01/31 Python
详解python中的index函数用法
2019/08/06 Python
Django 后台带有字典的列表数据与页面js交互实例
2020/04/03 Python
Python 基于jwt实现认证机制流程解析
2020/06/22 Python
nohup的用法
2014/08/10 面试题
国际商务专业职业生涯规划书范文
2014/01/17 职场文书
北体毕业生求职信
2014/02/28 职场文书
2014年机关作风建设工作总结
2014/10/23 职场文书
百万英镑观后感
2015/06/09 职场文书
2016暑期校本培训心得体会
2016/01/08 职场文书
Python基于Opencv识别两张相似图片
2021/04/25 Python
Python代码风格与编程习惯重要吗?
2021/06/03 Python
python turtle绘图命令及案例
2021/11/23 Python