React教程 - 事件

673 阅读7分钟

合成事件

合成事件是围绕浏览器原生事件,充当跨浏览器包装器的对象;它们将不同浏览器的行为合并为一个API,这样做是为了确保事件在不同浏览器中显示一致的属性

基本操作

基本语法 在JSX元素上,直接基于 onXxx={函数} 进行事件绑定;浏览器标准事件,在React中大部分都支持

合成事件对象

合成事件对象:SyntheticBaseEvent,在React合成事件触发的时候,也可以获取到事件对象,只不过此对象是合成事件对象【React内部经过特殊处理,把各个浏览器的事件对象统一化后,构建的一个事件对象】

import React, { Component } from 'react';

export default class Demo01 extends Component {
    handleClick1() {
        console.log(this); // undefined
    }
    handleClick2(x, y, ev) {
        // 只要方法经过bind处理了,那么最后一个实参,就是传递的合成事件对象
        console.log(this, x, y, ev); // 实例
    }
    handleClick3 = (ev) => {
        console.log(this); // 实例
        console.log(ev); // SyntheticBaseEvent 合成事件对象【React内部经过特殊处理,把各个浏览器的事件对象统一化后,构建的一个事件对象】
    };
    render() {
        return (
            <div>
                <button onClick={this.handleClick1}>按钮1</button>
                <button onClick={this.handleClick2.bind(this, 10, 20)}>按钮2</button>
                <button onClick={this.handleClick3}>按钮3</button>
            </div>
        );
    }
}
  • 基于React内部的处理,如果给合成事件绑定一个普通函数,当事件行为触发,绑定的函数执行;方法中的this会是undefined。解决方案:
  • 可以基于JS中的bind方法:预先处理函数中的this和实参
  • 也可以把绑定的函数设置为箭头函数,让其使用上下文中this【也就是实例】,推荐此方式

bind在React事件绑定的中运用

绑定的方法是-一个普通函数,需要改变函数中的this是实例,此时需要用到bind「一般都是绑定箭头函数」

想给函数传递指定的实参,可以基于bind预先处理「bind会把事件对象以最后一个实参传递给函数」

事件传播机制

当事件源触发点击行为的时候,会从最外层向最里层逐一查找【捕获阶段:分析出路径】;把事件源(点击的元素)的点击行为触发【目标阶段】;按照捕获阶段分析出来的路径,从里到外,把每一个元素的点击行为也触发【冒泡阶段】

QQ截图20230330101237.png e.stopPropagation() 和 # e.stopImmediatePropagation() 比较

  • 共同点
    都可以阻止事件冒泡,父节点无法接受到事件。

  • 不同点
    stopPropagation可以阻止事件冒泡,但不会影响该事件的其他监听方法执行,而stopImmediatePropagation不仅阻止事件冒泡,还会阻止该事件后面的监听方法执行

事件和事件绑定

事件是浏览器赋予元素的默认行为

事件绑定是给这个行为绑定一个方法

即便没有给body的点击事件绑定方法,当点击body的时候,其点击行为也会被触发,只不过是什么都不做而已

事件委托

事件委托:利用事件的传播机制,实现的一套事件绑定处理方案

  • 例如: 一个容器中,有很多元素都要在点击的时候做一些事情
    • 传统方案:首先获取需要操作的元素,然后逐一做事件绑定
    • 事件委托:只需要给容器做一个事件绑定「点击内部的任何元素,根据事件的冒泡传播机制,都会让容器的点击事件也触发;我们在这里根据事件源,做不同的事情就可以了」
      • 优势:
        • 提高JS代码运行的性能,并且把处理的逻辑都集中在一起! !
        • 某些需求必须基于事件委托处理,例如:除了点击xxx外,点击其余的任何东西,都做其他处理...
        • 给动态绑定的元素做事件绑定
      • 限制:
        • 当前操作的事件必须支持冒泡传播机制才可以;例如: mouseenter/mouseleave等 事件是没有冒泡传播机制的
        • 如果单独做的事件绑定中,做了事件传播机制的阻止,那么事件委托中的操作也不会生效! !

合成事件原理

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

  • 在React17及以后版本,都是委托给root这个容器「捕获和冒泡都做了委托」;
  • 在17版本以前,都是为委托给document容器的 「而且只做了冒泡阶段的委托」;
  • 对于没有实现事件传播机制的事件,才是单独做的事件绑定「例如: onMouseEnter/onMouseLeave...」
  • 在组件渲染的时候,如果发现JSX元素属性中有onXxx/onXxxCapture 这样的属性,不会给当前元素直接做事件绑定,只是把绑定的方法赋值给元素的相关属性! !例如:
    • outer. onClick=() => {console.log('outer 冒泡「合成」');} //这不是DOMO级事件绑定 「这样的才是outer . onclickJ
    • outer. onClickCapture=() => {console. log('outer捕获「合成」');}
    • inner.onClick=() => {console. log('inner冒泡「合成」');}
    • inner . onClickCapture=() => {console.log( ' inner捕获「合成」');}
  • 然后对#root这个容器做了事件绑定「捕获和冒泡都做了」
    • 原因:因为组件中所渲染的内容,最后都会插入到#root容器中,这样点击页面中任何一个元素,最后都会把 #root 的点击行为触发! !
    • 而在给 #root 绑定的方法中,把之前给元素设置的onXxx/ onXxxCapture属性,在相应的阶段执行!!

image.png

import React, { Component } from 'react';

export default class Demo02 extends Component {
    render() {
        return (
            <div
                className="outer"
                onClick={() => {
                    console.log('outer 冒泡【合成】');
                }}
                onClickCapture={() => {
                    console.log('outer 捕获【合成】');
                }}
            >
                <div
                    className="inner"
                    onClick={() => {
                        console.log('inner 冒泡【合成】');
                    }}
                    onClickCapture={() => {
                        console.log('inner 捕获【合成】');
                    }}
                ></div>
            </div>
        );
    }

    componentDidMount() {
        /*addEventListener()基本上有三个参数
          「事件名称」
          「事件的处理程序」(事件触发时执行的function)
          「Boolean」值,由这个Boolean决定事件是以「捕获」还是「冒泡」机制执行,若不指定则预设为「冒泡」
        */
        document.addEventListener('click',() => {
          console.log('document 捕获');
        },true);
        document.addEventListener('click',() => {
          console.log('document 冒泡');
        },false);

        document.body.addEventListener('click',() => {
          console.log('body 捕获');
        },true);
        document.body.addEventListener('click',() => {
          console.log('body 冒泡');
        },false);

        let root = document.querySelector('#root');
        root.addEventListener('click',() => {
          console.log('root 捕获');
        },true);
        root.addEventListener('click',() => {
          console.log('root 冒泡');
        },false);

        let outer = document.querySelector('.outer');
        outer.addEventListener('click',() => {
          console.log('outer 捕获【原生】');
        },true);
        outer.addEventListener('click',() => {
          console.log('outer 冒泡【原生】');
        },false);

        let inner = document.querySelector('.inner');
        inner.addEventListener('click',() => {
          console.log('inner 捕获【原生】');
        },true);
        inner.addEventListener('click',() => {
          console.log('inner 冒泡【原生】');
        },false);
    }
}

image.png

ev.stopPropagation 合成事件中的"阻止事件传播":不仅阻止原生的事件传播,还阻止合成事件中的事件传播;ev.nativeEvent.stopPropagation 原生事件对象中的"阻止事件传播":只能阻止原生事件

React16与React17及以后 合成事件比较

  • 在16版本中,合成事件的处理机制,不再是把事件委托给 root 元素,而是委托给 document 元素,并且只做了冒泡阶段的委托;在委托的方法中,把 onXxx/onXxxCapture 合成事件属性进行执行
  • React16中,关于合成事件对象的处理,React内部是基于事件对象池,做了一个缓存机制;React17及以后,是去掉了这套事件对象池缓存机制
    • 当每一次事件触发的时候,如果传播到了委托的元素上 document/#root ,在委托的方法中,我们首先会对内置事件对象做统一处理,生成合成事件对象
      • 在React16版本中;为了防止每一次都是重新创建出新的合成事件对象,它设置了一个事件对象池「缓存池」
        • 本次事件触发,获取到事件操作的相关信息后,我们从事件对象池中获取存储的合成事件对象,把信息赋值给相关的成员
        • 等待本次操作结束,把合成事件对象中的成员信息都清空掉,再放入到事件对象池中

QQ截图20230403141522.png

QQ截图20230403142534.png

事件池对象

QQ截图20230403143804.png

事件其他细节知识

解决移动端使用click事件的300ms延迟问题

方法一 禁用缩放

// 禁用缩放 user-scalable=no
<meta name="viewport" content="width=device-width, initial-scale=1.0,user-scalable=no">

user-scalable=no表明这个页面不可缩放,也就是浏览器禁用的双击缩放事件并且同时会去掉300ms点击延迟
但这个方案也有缺点,就是完全禁用了双击缩放,当我们需要放大文字或者图片时无法满足我们的需求

方案二 封装一个处理函数

//封装tap解决click 300ms 延时
function tap (obj,callback) {
	var isMove = false;//记录手指是否移动
	var startTime = 0;//记录手指触摸的时间
	obj.addEventListener('touchstart',function(e){
		startTime = Date.now();//记录触摸时间
	})
	obj.addEventListener('touchmove',function(e){
		isMove = true;//查看手指是否滑动
	})
	obj.addEventListener('touchend',function(e){
		if(!isMove && (Date.now()-statrTime) < 150){
		callback && callback();
		}
		isMove = false;//取反 重置
		startTime = 0;
	})
};
tap(div,function(){ //执行代码 });

这个代码可以监测元素点击发生时的状态,从而避免300ms的延迟。
但这个方法有一个弊端,一次只能给一个元素去解决问题

方案三 fastclick插件解决问题

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App.js';

// 引入 fastclick
import fastclick from 'fastclick';

// 解决点击事件延迟300毫秒的问题
fastclick.attach(document.body); 

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

在React中,我们给循环「创建」的元素做“循环事件绑定",好还是不好?

在React中,我们循环给元素绑定的合成事件,本身就是基于事件委托处理 的;所以无需我们自己再单独的设置事件委托的处理机制