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

相关推荐
zengyuhan5033 分钟前
Windows BLE 开发指南(Rust windows-rs)
前端·rust
醉方休7 分钟前
Webpack loader 的执行机制
前端·webpack·rust
前端老宋Running15 分钟前
一次从“卡顿地狱”到“丝般顺滑”的 React 搜索优化实战
前端·react.js·掘金日报
隔壁的大叔15 分钟前
如何自己构建一个Markdown增量渲染器
前端·javascript
用户44455436542617 分钟前
Android的自定义View
前端
WILLF18 分钟前
HTML iframe 标签
前端·javascript
枫,为落叶35 分钟前
Axios使用教程(一)
前端
小章鱼学前端40 分钟前
2025 年最新 Fabric.js 实战:一个完整可上线的图片选区标注组件(含全部源码).
前端·vue.js
ohyeah41 分钟前
JavaScript 词法作用域、作用域链与闭包:从代码看机制
前端·javascript
流星稍逝43 分钟前
手搓一个简简单单进度条
前端