合成事件
合成事件是围绕浏览器原生事件,充当跨浏览器包装器的对象;它们将不同浏览器的行为合并为一个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会把事件对象以最后一个实参传递给函数」
事件传播机制
当事件源触发点击行为的时候,会从最外层向最里层逐一查找【捕获阶段:分析出路径】;把事件源(点击的元素)的点击行为触发【目标阶段】;按照捕获阶段分析出来的路径,从里到外,把每一个元素的点击行为也触发【冒泡阶段】
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属性,在相应的阶段执行!!
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);
}
}
ev.stopPropagation 合成事件中的"阻止事件传播":不仅阻止原生的事件传播,还阻止合成事件中的事件传播;ev.nativeEvent.stopPropagation 原生事件对象中的"阻止事件传播":只能阻止原生事件
React16与React17及以后 合成事件比较
- 在16版本中,合成事件的处理机制,不再是把事件委托给 root 元素,而是委托给 document 元素,并且只做了冒泡阶段的委托;在委托的方法中,把 onXxx/onXxxCapture 合成事件属性进行执行
- React16中,关于合成事件对象的处理,React内部是基于
事件对象池,做了一个缓存机制;React17及以后,是去掉了这套事件对象池和缓存机制的- 当每一次事件触发的时候,如果传播到了委托的元素上 document/#root ,在委托的方法中,我们首先会对内置事件对象做统一处理,生成合成事件对象
- 在React16版本中;为了防止每一次都是重新创建出新的合成事件对象,它设置了一个事件对象池「缓存池」
- 本次事件触发,获取到事件操作的相关信息后,我们从事件对象池中获取存储的合成事件对象,把信息赋值给相关的成员
- 等待本次操作结束,把合成事件对象中的成员信息都清空掉,再放入到事件对象池中
- 在React16版本中;为了防止每一次都是重新创建出新的合成事件对象,它设置了一个事件对象池「缓存池」
- 当每一次事件触发的时候,如果传播到了委托的元素上 document/#root ,在委托的方法中,我们首先会对内置事件对象做统一处理,生成合成事件对象
事件池对象
事件其他细节知识
解决移动端使用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中,我们循环给元素绑定的合成事件,本身就是基于事件委托处理 的;所以无需我们自己再单独的设置事件委托的处理机制