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

相关推荐
苏打水com16 小时前
字节跳动前端业务:从「短视频交互」到「全球化适配」的技术挑战
前端·音视频
又是忙碌的一天17 小时前
前端学习 JavaScript
前端·javascript·学习
Jagger_17 小时前
Cursor + Apifox MCP:告别手动复制接口,AI 助你高效完成接口文档开发
前端
IT_陈寒17 小时前
Redis性能优化:5个被低估的配置项让你的QPS提升50%
前端·人工智能·后端
Hilaku17 小时前
重新思考CSS Reset:normalize.css vs reset.css vs remedy.css,在2025年该如何选?
前端·css·代码规范
袁煦丞18 小时前
一图看懂Docker管理 Portainer:cpoar内网穿透实验室第652个成功挑战
前端·程序员·远程工作
右子18 小时前
微信小程序开发“闭坑”指南
前端·javascript·微信小程序
AGG_Chan18 小时前
flutter专栏--深入了解widget原理
开发语言·javascript·flutter
冰镇生鲜18 小时前
前端模拟 流式文本接口 打字机效果 mockStreamText
javascript
入秋18 小时前
Three.js后期处理实战:噪点 景深 以及色彩调整
前端·javascript·three.js