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事件不会被合成事件阻止冒泡。那该怎么办呢?他是在组件的didmount阶段,才绑定document的事件的,实际上,可以拿到合成事件的原生event,调用event.nativeEvent.stopImmediatePropagation(),可以阻止和document的合成事件同级的剩余事件。

参考资料:

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

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

相关推荐

  1. React 表单事件

    2024-04-22 09:52:08       37 阅读
  2. React事件原生事件的执行顺序

    2024-04-22 09:52:08       18 阅读
  3. ReactReact响应事件

    2024-04-22 09:52:08       18 阅读
  4. React 之 Effect事件(event)(八)

    2024-04-22 09:52:08       13 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-22 09:52:08       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-22 09:52:08       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-22 09:52:08       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-22 09:52:08       20 阅读

热门阅读

  1. 【Pytorch】torch.cat()函数

    2024-04-22 09:52:08       17 阅读
  2. android学习笔记(三)

    2024-04-22 09:52:08       12 阅读
  3. 如何让一个人自律

    2024-04-22 09:52:08       14 阅读
  4. C#字典底层原理

    2024-04-22 09:52:08       15 阅读
  5. SQL语句每日一练十

    2024-04-22 09:52:08       13 阅读
  6. Eureka、Nacos、Zookeeper、Redis等应用是AP还是CP?

    2024-04-22 09:52:08       15 阅读