一、事件传播机制
1.事件机制概念
-
主流浏览器:捕获阶段 > 目标阶段 > 冒泡阶段
-
非主流浏览器:冒泡阶段 > 目标阶段 > 捕获阶段
-
传播路径:
window > document > html > body > target > …… 或者反过来
-
阻止冒泡:stopPropgation || cancelBubble
-
阻止默认事件:preventDefault || returnValue
-
事件监听:addEventListenner || attcathEvent
-
零级事件:直接绑定在dom元素上面的onclick
<div onclick="()=>{ console.log(111) }">点我</div>
- 一级事件:dom.onclick
document.body.onclick = function (){}
- 二级事件:事件监听
/*
第三个参数:
默认是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);