react合成事件与原生事件区别备忘

朋友问起在做一个下拉框组件,下拉的点击事件是用react的onClick触发,外部区域点击关闭则用dom的原生点击事件绑定,问题是下拉的点击事件无法阻止冒泡到dom的原生事件。

我说,react的合成事件 和 原生事件是不一样的,尽可能不要混用,不然很绕。翻开之前在codepen写的demo

https://codepen.io/shellphon-the-encoder/pen/vYPEggK

也把自己绕晕了一下。

react的合成事件,注入onClick等事件,是在根元素上事件代理模拟的。react 16.8.0和之前的版本,是在document上事件代理,react 17则是在root

以demo上的div结构为例:v17.0.2

复制代码
const {useState, useEffect, useRef} = React;
document.getElementById('root').addEventListener('click', (e) =>{
      console.log('外部原生root 点击', e);
  //e.stopPropagation();
    });
document.addEventListener('click', (e) =>{
      console.log('外部原生document 点击', e);
    }); 
const App = () => {
  const sonRef = useRef(null);
  const parentRef = useRef(null);
  const parentClick = (e)=>{
    console.log('合成事件parent click',e);
    //e.stopPropagation();
  };
  const sonClick = (e)=>{
    console.log('合成事件son click', e);
  }
  const sonClickNo = (e)=>{
    console.log('合成事件son click并阻止冒泡', e);
    e.stopPropagation();
  }
  useEffect(() => {
   document.addEventListener('click', (e) =>{
      console.log('内部原生document click', e);
    }); document.getElementById('root').addEventListener('click', (e) =>{
      console.log('内部原生 root click', e);
    });
    parentRef.current.addEventListener('click', (e) =>{
      console.log('内部原生事件ref p', e);
    });
    sonRef.current.addEventListener('click', (e) =>{
      console.log('内部原生事件ref son', e);
    });
  }, [])
  
  return <div ref={parentRef} onClick={parentClick}>
    <div onClick={sonClick}>son</div>
    <div onClick={sonClickNo} ref={sonRef}>son no</div>
  </div>
};

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

document>root>div>.parent>.son

son上的onClick(合成事件),实际是react root上的点击事件,在内部做模拟冒泡。

因为demo写的事件比较多,比较绕,所以画了出来。

当只有合成事件的时候,无非就是 son点击响应,然后parent点击响应。

同一个元素的原生事件和合成事件

当往son原生div加click事件时,点击son,会先响应原生click,再然后才是去合成事件,这中间如果parent也有原生click,那也是先原生click再到合成事件去。

如下:

复制代码
const {useState, useEffect, useRef} = React;

const App = () => {
  const sonRef = useRef(null);
  const parentRef = useRef(null);
  const parentClick = (e)=>{
    console.log('合成事件parent click',e);
    //e.stopPropagation();
  };
  const sonClick = (e)=>{
    console.log('合成事件son click', e);
  }
  const sonClickNo = (e)=>{
    console.log('合成事件son click并阻止冒泡', e);
    e.stopPropagation();
  }
  useEffect(() => {
   
    parentRef.current.addEventListener('click', (e) =>{
      console.log('内部原生事件ref p', e);
    });
    sonRef.current.addEventListener('click', (e) =>{
      console.log('内部原生事件ref son', e);
    });
  }, [])
  
  return <div ref={parentRef} onClick={parentClick}>
    <div onClick={sonClick}>son</div>
    <div onClick={sonClickNo} ref={sonRef}>son no</div>
  </div>
};

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

点击第一个son时:

可以看到先响应了parent的原生事件,然后才到son的合成事件

合成事件和外部root事件的关系

在react外面给root绑定click事件,看合成事件的顺序

复制代码
const {useState, useEffect, useRef} = React;
document.getElementById('root').addEventListener('click', (e) =>{
      console.log('外部原生root 点击', e);
  //e.stopPropagation();
    });
document.addEventListener('click', (e) =>{
      console.log('外部原生document 点击', e);
    }); 
const App = () => {
  const sonRef = useRef(null);
  const parentRef = useRef(null);
  const parentClick = (e)=>{
    console.log('合成事件parent click',e);
    //e.stopPropagation();
  };
  const sonClick = (e)=>{
    console.log('合成事件son click', e);
  }
  const sonClickNo = (e)=>{
    console.log('合成事件son click并阻止冒泡', e);
    e.stopPropagation();
  }
  useEffect(() => {
    parentRef.current.addEventListener('click', (e) =>{
      console.log('内部原生事件ref p', e);
    });
    sonRef.current.addEventListener('click', (e) =>{
      console.log('内部原生事件ref son', e);
    });
  }, [])
  
  return <div ref={parentRef} onClick={parentClick}>
    <div onClick={sonClick}>son</div>
    <div onClick={sonClickNo} ref={sonRef}>son no</div>
  </div>
};

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

点击第一个son时,先原生事件, 然后 到了外部root绑定的事件,再到合成事件的son、parent,再然后是document

合成事件是在root上模拟的,而外部绑定root的事件和这个合成事件也就是在一个dom上多次绑定事件响应,各不相干,顺序上谁先绑定谁先响应,于是外部root原生先响应,再到合成事件的处理。而document是在root的上层,因此document事件是在最后才响应。

如果在组件周期里也给root加一个原生事件响应,那它会在合成事件完成之后才响应,因为它也是给同一个dom绑定的事件之一,只是晚于合成事件。

回到最开始的demo代码,当在son的onClick上阻止冒泡时,它做了两件事情:

  1. 阻止了向上冒泡的模拟

  2. 调用了原生事件的阻止冒泡 (解释了合成事件阻止冒泡后,为什么document事件没有响应)

由此,如果朋友在react 17版本上document上绑定的事件应该是能被合成事件阻止冒泡的。

但如果在react 16.8.0 合成事件是在document上绑定,那么额外绑定的document事件不会被合成事件阻止冒泡。

参考资料:

https://github.com/youngwind/blog/issues/107

https://mdnice.com/writing/85c044f9087746dcbd719e4a0b847278

相关推荐
Hilaku25 分钟前
我用 Gemini 3 Pro 手搓了一个并发邮件群发神器(附源码)
前端·javascript·github
IT_陈寒25 分钟前
Java性能调优实战:5个被低估却提升30%效率的JVM参数
前端·人工智能·后端
快手技术26 分钟前
AAAI 2026|全面发力!快手斩获 3 篇 Oral,12 篇论文入选!
前端·后端·算法
颜酱28 分钟前
前端算法必备:滑动窗口从入门到很熟练(最长/最短/计数三大类型)
前端·后端·算法
全栈前端老曹36 分钟前
【包管理】npm init 项目名后底层发生了什么的完整逻辑
前端·javascript·npm·node.js·json·包管理·底层原理
HHHHHY42 分钟前
mathjs简单实现一个数学计算公式及校验组件
前端·javascript·vue.js
boooooooom1 小时前
Vue3 provide/inject 跨层级通信:最佳实践与避坑指南
前端·vue.js
一颗烂土豆1 小时前
Vue 3 + Three.js 打造轻量级 3D 图表库 —— chart3
前端·vue.js·数据可视化
青莲8431 小时前
Android 动画机制完整详解
android·前端·面试
iReachers1 小时前
HTML打包APK(安卓APP)中下载功能常见问题和详细介绍
前端·javascript·html·html打包apk·网页打包app·下载功能