重学React —— React事件机制 vs 浏览器事件机制

react和浏览器的事件机制大多数人都有了解,本文主要介绍一些容易误解的点。

原生事件的冒泡和捕获

原生事件有冒泡和捕获两个阶段,两个阶段的先后顺序是捕获在前,冒泡在后。

jsx 复制代码
function EventTest() {
  useEffect(() => {
    document.querySelector("#wrap").addEventListener(
      "click",
      (e) => {
        console.log("div 捕获");
      },
      true
    );
    document.querySelector("#wrap").addEventListener("click", () => {
      console.log("div 冒泡");
    });
    document.querySelector("#btn").addEventListener(
      "click",
      () => {
        console.log("btn 捕获");
      },
      true
    );
    document.querySelector("#btn").addEventListener("click", () => {
      console.log("btn 冒泡");
    });
  }, []);
  return (
    <div id="wrap">
      <p>
        <button id="btn">btn</button>
      </p>
    </div>
  );
}

stopPropagation

stopPropagation可以阻止事件传播,如果在捕获阶段阻止,那么事件不会进入冒泡阶段。

diff 复制代码
 document.querySelector("#wrap").addEventListener(
    "click",
    (e) => {
+      e.stopPropagation();
      console.log("div 捕获");
    },
    true
  );

stopPropagation不会阻止对同一元素上的同一事件的其他事件监听

javascript 复制代码
 document.querySelector("#wrap").addEventListener(
    "click",
    (e) => {
      e.stopPropagation();
      console.log("div 捕获1");
    },
    true
  );
  document.querySelector("#wrap").addEventListener(
    "click",
    (e) => {
      console.log("div 捕获2");
    },
    true
  );

stopImmediatePropagation

如果多个事件监听器被附加到相同元素的相同事件类型上,当此事件触发时,它们会按其被添加的顺序被调用。如果在其中一个事件监听器中执行 stopImmediatePropagation() ,那么剩下的事件监听器都不会被调用。

diff 复制代码
 document.querySelector("#wrap").addEventListener(
    "click",
    (e) => {
-      e.stopPropagation();
+      e.stopImmediatePropagation()
      console.log("div 捕获1");
    },
    true
  );
  document.querySelector("#wrap").addEventListener( 
      "click", 
      (e) => { 
          console.log("div 捕获2"); 
      }, 
      true 
  );

React事件委托

React事件中的冒泡

React是在 React root节点上利用事件委托获取原生事件,并且默认是在冒泡阶段捕捉原生事件 ,因此如果在原生事件上阻止冒泡,也会阻止React的事件流 ,而阻止React的事件流并不会阻止原生事件(先原生事件,后React事件流),调用stopPropagation对原生事件没有任何影响。

jsx 复制代码
function EventTest() {
  useEffect(() => {
    document.querySelector("#wrap").addEventListener(
      "click",
      (e) => {
        console.log("div 捕获");
      },
      true
    );
    document.querySelector("#wrap").addEventListener("click", () => {
      console.log("div 冒泡");
    });
    document.querySelector("#btn").addEventListener(
      "click",
      () => {
        console.log("btn 捕获");
      },
      true
    );
    document.querySelector("#btn").addEventListener("click", (e) => {
      e.stopPropagation(); //react事件无法响应
      console.log("btn 冒泡");
    });
  }, []);
  const click = (e) => {
    console.log("react click", e);
  };
  return (
    <div id="wrap">
      <p>
        <button id="btn" onClick={click}>
          btn
        </button>
      </p>
    </div>
  );
}

React事件中的捕获

React事件可以在捕获阶段监听------使用对应的eventName+Capture,这种情况下,react的委托事件,会在事件的捕获阶段获取到原生事件。这时候,调用stopPropagation会影响原生事件的传播。

javascript 复制代码
function EventTest() {
  useEffect(() => {
    document.querySelector("#wrap").addEventListener(
      "click",
      (e) => {
        console.log("div 捕获");
      },
      true
    );
    document.querySelector("#wrap").addEventListener("click", () => {
      console.log("div 冒泡");
    });
    document.querySelector("#btn").addEventListener(
      "click",
      () => {
        console.log("btn 捕获");
      },
      true
    );
    document.querySelector("#btn").addEventListener("click", (e) => {
      console.log("btn 冒泡");
    });
  }, []);
  const click = (e) => {
    e.stopPropagation();
    console.log("react click", e);
  };
  const clickCapture = (e) => {
    e.stopPropagation();
    console.log("react click capture", e);
  };
  return (
    <div id="wrap">
      <p>
        <button id="btn" onClick={click} onClickCapture={clickCapture}>
          btn
        </button>
      </p>
    </div>
  );
}

在捕获事件中阻止事件传播后原生监听的事件都没有响应了:

React事件传播

React监听到原生事件后,会寻找对应的Fiber节点(React创建dom对象时会将其与Fiber关联)。 找到目标 Fiber 节点后,则一直往上遍历父节点 Fiber,

收集这一条链上的所有对应事件回调函数。

  • 捕获阶段:从 root 向 target 收集;
  • 冒泡阶段:从 target 向 root 收集。

然后再按顺序执行这些回调函数,完成对原生事件传播的模拟。

因为Fiber树与原生dom树未必一致,所以React的事件传播与原生事件传播也可能并不一致。比如弹窗上的点击事件可能会冒泡到弹窗外的元素上。

React事件对象:事件池与重用

在React 16中,如果异步使用合成事件对象,会出现如下报错:

这是因为React 16中会重复使用同一事件对象

  • 当浏览器触发事件(如 click)时,React 拦截它;
  • React 创建一个 SyntheticEvent 对象;
  • React 会把原生事件属性(如 target, type, clientX 等)拷贝到这个合成事件上;
  • React 调用所有注册在这个事件上的 React 回调;
  • 事件处理完后 ,React 会调用 event.release() 将该对象的所有属性清空,放回"事件池";
  • 下次有新的事件触发时,React 会复用这个对象(重新写入属性)。 因此它提供了persist方法,让该事件不被重用。

不过React 17废除了该特性。

总结

特性 React 事件冒泡 浏览器原生冒泡
冒泡路径 React 虚拟 DOM 树 实际 DOM 树
事件对象 SyntheticEvent(合成事件) 原生 Event
stopPropagation() 阻止 React 内部的合成事件传播,不一定阻止原生事件传播 阻止原生事件传播
相关推荐
福兮说8 小时前
设计稿是 #4A7C6F,页面量出来是 #4B7C6F:HEX、HSL、透明度、canvas 来回转的七个坑
前端·javascript·css·canvas
郑州光合科技余经理9 小时前
海外版外卖加盟:总站与分站配送规则怎么分开管
java·开发语言·前端·后端·uni-app·php·ai编程
小呆呆6669 小时前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
Dovis(誓平步青云)10 小时前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
码艺-Alimjan10 小时前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_11 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒11 小时前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技11 小时前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端
yivifu12 小时前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub
默_笙12 小时前
🛴 从散件到整机:DeepAgents 与 Agent 身上预留的那些"插槽"(前置介绍)
前端·javascript