React之合成事件原理

144 阅读4分钟

一、事件传播机制

1.事件机制概念

  1. 主流浏览器:捕获阶段 > 目标阶段 > 冒泡阶段

  2. 非主流浏览器:冒泡阶段 > 目标阶段 > 捕获阶段

  3. 传播路径:

window > document > html > body > target > …… 或者反过来

  1. 阻止冒泡:stopPropgation || cancelBubble

  2. 阻止默认事件:preventDefault || returnValue

  3. 事件监听:addEventListenner || attcathEvent

  4. 零级事件:直接绑定在dom元素上面的onclick

<div onclick="()=>{ console.log(111) }">点我</div>
  1. 一级事件:dom.onclick
document.body.onclick = function (){}
  1. 二级事件:事件监听
/* 
  第三个参数:
  默认是false
  true=捕获阶段执行,也可以理解为阻止冒泡
  false=冒泡阶段执行
*/
document.body.addEventListener('click', function (e) {
    console.log(e)
}, true) 

2.事件委托

 body.addEventListener('click', function (ev) {
  // ev.target:事件源「点击的是谁,谁就是事件源」
  let target = ev.target,
      id = target.id;
  if (id === "root") {
      console.log('root');
      return;
  }
  if (id === "inner") {
      console.log('inner');
      return;
  }
  if (id === "AAA") {
      console.log('AAA');
      return;
  }
  // 如果以上都不是,我们处理啥....
});

二、react合成事件原理

1.前言

基于React内部的处理,如果我们给合成事件绑定一个“普通函数”,当事件行为触发,绑定的函数执行;方法中的this会是undefined「不好」!!

解决方案:将this 指向 实例

  • 我们可以基于JS中的bind方法:预先处理函数中的this和实参的
  • 推荐:当然也可以把绑定的函数设置为“箭头函数”,让其使用上下文中的this「也就是我们的实例」

2.合成事件对象

合成事件对象SyntheticBaseEvent:我们在React合成事件触发的时候,也可以获取到事件对象;

只不过此对象是合成事件对象「React内部经过特殊处理,把各个浏览器的事件对象统一化后,构建的一个事件对象」

合成事件对象中,也包含了浏览器内置事件对象中的一些属性和方法「常用的基本都有」

  • clientX/clientY
  • pageX/pageY
  • target
  • type
  • preventDefault
  • stopPropagation
  • nativeEvent:基于这个属性,可以获取浏览器内置『原生』的事件对象
  • ...

3.合成事件的处理原理

“绝对不是”给当前元素基于addEventListener单独做的事件绑定,React中的合成事件,都是基于“事件委托”处理的!

  • 在React17及以后版本,都是委托给#root这个容器「捕获和冒泡都做了委托」;
  • 在17版本以前,都是为委托给document容器的「而且只做了冒泡阶段的委托」;
  • 对于没有实现事件传播机制的事件,才是单独做的事件绑定「例如:onMouseEnter/onMouseLeave...」

在组件渲染的时候,如果发现JSX元素属性中有 onXxx/onXxxCapture 这样的属性,不会给当前元素直接做事件绑定,只是把绑定的方法赋值给元素的相关属性!!例如:

// 这不是DOM0级事件绑定「小写的 onclick 才是」
outer.onClick=() => {console.log('outer 冒泡「合成」');}  
outer.onClickCapture=() => {console.log('outer 捕获「合成」');}
inner.onClick=() => {console.log('inner 冒泡「合成」');}
inner.onClickCapture=() => {console.log('inner 捕获「合成」');}

然后对#root这个容器做了事件绑定「捕获和冒泡都做了」

  • 因为组件中所渲染的内容,最后都会插入到#root容器中,这样点击页面中任何一个元素,最后都会把#root的点击行为触发!!
  • 而在给#root绑定的方法中,把之前给元素设置的onXxx/onXxxCapture属性,在相应的阶段执行!!

关于react合成事件相关的笔试题,只需记住以下几个特性,然后去推断,就一定能做出来!

  • 16版本是在document上做的事件委托,18是在root上面做的事件委托!
  • 不管冒泡还是捕获,走到root/document的时候,都会先执行react通过onXxx绑定的事件,再执行手动操作DOM添加的事件!
  • 在捕获执行到root/document节点的时候,会一次性把所有react绑定的捕获事件执行完,再执行手动操作DOM添加的事件!
  • 在冒泡执行到root/document节点的时候,也会再次执行react绑定的冒泡事件!
  • 所以如果你在某个子节点上操作com手动添加了事件,那么在冒泡时会先找到你的,冒泡到root/document时,又会执行一遍react,这样就会造成冒泡顺序错乱!

三、扩展

1.移动端click的300ms延迟问题

双击缩放的延迟判断造成的

解决方式一:自行封装方法,通过touchstart、touchmove、touchend实现

import React from "react";

class Demo extends React.Component {
    // 手指按下:记录手指的起始坐标
    touchstart = (ev) => {
        let finger = ev.changedTouches[0]; //记录了操作手指的相关信息
        this.touch = {
            startX: finger.pageX,
            startY: finger.pageY,
            isMove: false
        };
    };
    // 手指移动:记录手指偏移值,和误差值做对比,分析出是否发生移动
    touchmove = (ev) => {
        let finger = ev.changedTouches[0],
            { startX, startY } = this.touch;
        let changeX = finger.pageX - startX,
            changeY = finger.pageY - startY;
        if (Math.abs(changeX) > 10 || Math.abs(changeY) > 10) {
            this.touch.isMove = true;
        }
    };
    // 手指离开:根据isMove判断是否是点击
    touchend = () => {
        let { isMove } = this.touch;
        if (isMove) return;
        // 说明触发了点击操作
        console.log('点击了按钮');
    };
    render() {
        return <div>
            <button onTouchStart={this.touchstart}
                onTouchMove={this.touchmove}
                onTouchEnd={this.touchend}>
                提交
            </button>
        </div>;
    }
}
export default Demo;

解决方式二:使用 FastClick 插件

$ npm i fastclick

index.jsx中使用

import FastClick from 'fastclick';
FastClick.attach(document.body);