React合成事件是指将原生事件合成一个React事件,之所以要封装自己的一套事件机制,目的是为了实现全浏览器的一致性,抹平不同浏览器之间的差异性。比如原生onclick事件对应React中的onClick合成事件。我们先来看一下React事件和原生事件在使用上的区别:
const handleClick = (e) => {e.preventDefault();}
// 原生事件
<div onclick="handleClick()"></div>
// React合成事件
<div onClick={HandleCilck}></div>
从中可以看到,React合成事件驼峰的命名方式,而原生事件使用全小写的方式;另外,React事件处理函数使用事件对象形式,原生事件使用字符串的形式。UI Events从中可以看到,React合成事件使用驼峰的命名方式,而原生事件使用全小写的方式;另外,React事件处理函数使用事件对象形式,原生事件使用字符串的形式。
事件流
我们已经知道onClick事件是一个合成事件,那合成事件是如何跟原生事件产生关联的呢?首先我们来复习一下事件流原理:
图片引自:Event flow
如上图所示,所谓事件流包括三个阶段:事件捕获、目标阶段和事件冒泡。事件捕获是从外到里,对应图中的红色箭头标注部分window -> document -> html … -> target,目标阶段是事件真正发生并处理的阶段,事件冒泡是从里到外,对应图中的target -> … -> html -> document -> window。 React合成事件的工作原理大致可以分为两个阶段:
- 事件绑定
- 事件触发
在React17之前,React是把事件委托在document上的,React17及以后版本不再把事件委托在document上,而是委托在挂载的容器上了,本文以16.x版本的React为例来探寻React的合成事件。当真实的dom触发事件时,此时构造React合成事件对象,按照冒泡或者捕获的路径去收集真正的事件处理函数,在此过程中会先处理原生事件,然后当冒泡到document对象后,再处理React事件。 举个栗子:
import React from 'react';
import './App.less';
class Test extends React.Component {
parentRef: React.RefObject<any>;
childRef: React.RefObject<any>;
constructor(props) {
super(props);
this.parentRef = React.createRef();
this.childRef = React.createRef();
}
componentDidMount() {
document.addEventListener(
'click',
() => {
console.log(`document原生事件捕获`);
},
true,
);
document.addEventListener('click', () => {
console.log(`document原生事件冒泡`);
});
this.parentRef.current.addEventListener(
'click',
() => {
console.log(`父元素原生事件捕获`);
},
true,
);
this.parentRef.current.addEventListener('click', () => {
console.log(`父元素原生事件冒泡`);
});
this.childRef.current.addEventListener(
'click',
() => {
console.log(`子元素原生事件捕获`);
},
true,
);
this.childRef.current.addEventListener('click', () => {
console.log(`子元素原生事件冒泡`);
});
}
handleParentBubble = () => {
console.log(`父元素React事件冒泡`);
};
handleChildBubble = () => {
console.log(`子元素React事件冒泡`);
};
handleParentCapture = () => {
console.log(`父元素React事件捕获`);
};
handleChileCapture = () => {
console.log(`子元素React事件捕获`);
};
render() {
return (
<div
ref={this.parentRef}
onClick={this.handleParentBubble}
onClickCapture={this.handleParentCapture}
>
<div
ref={this.childRef}
onClick={this.handleChildBubble}
onClickCapture={this.handleChileCapture}
>
事件处理测试
</div>
</div>
);
}
}
export default Test;
上面案例打印的结果为:
注:React17中上述案例的执行会有所区别,会先执行所有捕获事件后,再执行所有冒泡事件。
事件绑定
通过上述案例,我们知道了React合成事件和原生事件执行的过程,两者其实是通过一个叫事件插件(EventPlugin
)的模块产生关联的,每个插件只处理对应的合成事件,比如onClick事件对应SimpleEventPlugin
插件,这样React在一开始会把这些插件加载进来,通过插件初始化一些全局对象,比如其中有一个对象是registrationNameDependencies
,它定义了合成事件与原生事件的对应关系如下:
{
onClick: ['click'],
onClickCapture: ['click'],
onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
...
}
registrationNameModule
对象指定了React事件到对应插件plugin的映射:
{
onClick: SimpleEventPlugin,
onClickCapture: SimpleEventPlugin,
onChange: ChangeEventPlugin,
...
}
plugins
对象就是上述插件的列表。在某个节点渲染过程中,合成事件比如onClick是作为它的prop的,如果判断该prop为事件类型,根据合成事件类型找到对应依赖的原生事件注册绑定到顶层document上,dispatchEvent
为统一的事件处理函数。
事件触发
当任意事件触发都会执行dispatchEvent
函数,比如上述事例中,当用户点击Child的div时会遍历这个元素的所有父元素,依次对每一级元素进行事件的收集处理,构造合成事件对象(SyntheticEvent
—也就是通常我们说的React中自定义函数的默认参数event,原生的事件对象对应它的一个属性),然后由此形成了一条「链」,这条链会将合成事件依次存入eventQueue
中,而后会遍历eventQueue
模拟一遍捕获和冒泡阶段,然后通过runEventsInBatch
方法依次触发调用每一项的监听事件,在此过程中会根据事件类型判断属于冒泡阶段还是捕获阶段触发,比如onClick是在冒泡阶段触发,onClickCapture是在捕获阶段触发,在事件处理完成后进行释放。 SyntheticEvent
对象属性如下:
boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent // 原生事件对象
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
void persist()
DOMEventTarget target
number timeStamp
string type
dispatchEvent
伪代码如下:
dispatchEvent = (event) => {
const path = []; // 合成事件链
let current = event.target; // 触发事件源
while (current) {
path.push(current);
current = current.parentNode; // 逐级往上进行收集
}
// 模拟捕获和冒泡阶段
// path = [target, div, body, html, ...]
for (let i = path.length - 1; i >= 0; i--) {
const targetHandler = path[i].onClickCapture;
targetHandler && targetHandler();
}
for (let i = 0; i < path.length; i++) {
const targetHandler = path[i].onClick;
targetHandler && targetHandler();
}
};
总结
由于事件对象可能会频繁创建和回收在React16.x中,合成事件SyntheticEvent
采用了事件池,合成事件会被放进事件池中统一管理,这样能够减少内存开销。React通过合成事件,模拟捕获和冒泡阶段,从而达到不同浏览器兼容的目的。另外,React不建议将原生事件和合成事件一起使用,这样很容易造成使用混乱。
最后
搜索公众号Eval Studio,关注获取更多动态
本文转自 https://zhuanlan.zhihu.com/p/395357493,如有侵权,请联系删除。