在正式开始前,先来看看 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.addEventListenerReactDOM.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 的监听不会触发。

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

如果你已经忘了,这是相应的代码及输出。
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

到这里,已经可以解答为什么 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);
}
<span class="pl-k">return</span> () <span class="pl-k">=&gt;</span> {
<span class="pl-c1">document</span>.<span class="pl-c1">removeEventListener</span>(<span class="pl-s"><span class="pl-pds">"</span>click<span class="pl-pds">"</span></span>, documentClickHandler);
<span class="pl-k">if</span> (<span class="pl-smi">btnElement</span>.<span class="pl-c1">current</span>) {
<span class="pl-smi">btnElement</span>.<span class="pl-c1">current</span>.<span class="pl-c1">removeEventListener</span>(<span class="pl-s"><span class="pl-pds">"</span>click<span class="pl-pds">"</span></span>, 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 上的事件,处理 mousemoveresize 等这些场景,那么就需要注意本文提到的顺序问题,不然容易出 bug。

相关资源

React 中阻止事件冒泡的问题的更多相关文章

  1. JQuery中阻止事件冒泡的两种方式及其区别

    JQuery 提供了两种方式来阻止事件冒泡. 方式一:event.stopPropagation(); $("#div1").mousedown(function(event){ ...

  2. JQuery中阻止事件冒泡方式及其区别

    JQuery 提供了两种方式来阻止事件冒泡. 方式一:event.stopPropagation();         $("#div1").mousedown(function( ...

  3. js中阻止事件冒泡和浏览器默认行为

    在使用javascript编程时会遇到一个问题,就是当你给html添加事件时,由于浏览器默认的为冒泡型事件触发机制,所以会触发你不想触发的事件.那么通过如下的函数可以解决这个问题.[兼容IE和FF] ...

  4. angularjs中阻止事件冒泡,以及指令的注意点

    appModule.directive('newStr',function(){ return{ restrict:'AE', //阻止事件冒泡需要加$event参数 template:`<di ...

  5. Html 中阻止事件冒泡的三种方法比较

    A:return false --->In event handler ,prevents default behavior and event bubbing .         return ...

  6. jQuery阻止事件冒泡的例子

    下面给给各位朋友稍加整理了一jquery中阻止事件冒泡的一些例子,我们知道JQuery 提供了两种方式来阻止事件冒泡,但我们简单的利用它来做一些应用可能不深入或不理解,下面整理了更详细的方法,有兴趣的 ...

  7. react 阻止事件冒泡

    前言 在学习react阻止事件冒泡,需要先了解 合成事件 和 原生事件 合成事件:在jsx中直接绑定的事件,就是合成事件: 原生事件: 通过js原生代码绑定的事件,就是原生事件: react事件:re ...

  8. React阻止事件冒泡的正确打开方式

    需求:点击导航list按钮出现侧弹框,点击空白处弹框消失 问题:绑定空白处的点击事件到document上,但是非空白处的点击也会触发这个点击事件,在react中如何阻止事件冒泡? 解决方法:e.sto ...

  9. JS中的事件冒泡(Bubble)和事件捕获(capture)以及如何阻止事件的冒泡

    对“捕获”和“冒泡”这两个概念,通常我们对冒泡了解和使用的会更多一些,因为在我们使用的所有浏览器中,都支持事件冒泡 ,即事件由子元素向祖先元素传播的,就 像气泡从水底向水面上浮一样.而在像firefo ...

随机推荐

  1. WSL Windows Subsystem for Linux安装指南

    见官方文档: https://msdn.microsoft.com/en-us/commandline/wsl/install_guide

  2. [ SSH框架 ] Struts2框架学习之三(OGNl和ValueStack值栈学习)

    一.OGNL概述 1.1 什么是OGNL OGNL的全称是对象图导航语言( object-graph Navigation Language),它是一种功能强大的开源表达式语言,使用这种表达式语言,可 ...

  3. C#本质论笔记

    第一章 C#概述 1.1 Helo,World 学习一种新语言最好的办法就是动手写程序.        C#编译器创建的.exe程序是一个程序集(Assembly),我们也可以创建能由另一个较大的程序 ...

  4. GitHub学习笔记:分支管理

    GitHub对于每个开发版本都需要有一个分支,默认的分支是master往往被大家保留下来作为主分支,分支类似于进程的一个指针,往往在master这个稳定的主干版本上分出一个或多个正在开发的分支版本,开 ...

  5. centos下网络的配置

    1.网络模式要进行使用NAT,要连网的话,要配置好设置:网络要进行一下共享到虚拟机 进入vi /etc/sysconfig/network-scripts/ifcfg-eth0   把里面的onboo ...

  6. UML类图10分钟快速入门 - From 圣杰

    虚线箭头指向依赖: 实线箭头指向关联: 虚线三角指向接口: 实线三角指向父类: 空心菱形能分离而独立存在,是聚合: 实心菱形精密关联不可分,是组合: 原文作者:圣杰 原文地址:http://www.j ...

  7. 洛谷 P1069 解题报告

    P1069 细胞分裂 题目描述 \(Hanks\)博士是\(BT\) (\(Bio-Tech\),生物技术) 领域的知名专家.现在,他正在为一个细胞实验做准备工作:培养细胞样本. \(Hanks\) ...

  8. 适合Python 新手的5大练手项目,你练了么?

    接下来就给大家介绍几种适合新手的练手项目. 0.算法系列-排序与查找 Python写swap很方便,就一句话(a, b = b, a),于是写基于比较的排序能短小精悍.刚上手一门新语言练算法最合适不过 ...

  9. [CVPR2017] Weakly Supervised Cascaded Convolutional Networks论文笔记

    p.p1 { margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px "Helvetica Neue"; color: #042eee } p. ...

  10. 创建第一个core项目(netCore学习笔记1)

    1.安装 core和netFramework其实是相对独立的,但是core的IDE是在vs2017才开始支持,而vs2017的安装环境必须搭配.net4.6,所以: Step1:安装.net4.6 S ...