react的事件是合成事件((Synethic event),不是原生事件
合成事件与原生事件的区别
我们来看个例子深入了解下,如果我们需要实现一个组件,这个组件点击按钮会显示一个二维码,点击二维码之外的区域可以隐藏二维码,但是点击二维码本身却不会关闭,代码如下:
//代码来源于《深入React技术栈》2.1.4节
class QrCode extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
this.handleClickQr = this.handleClickQr.bind(this);
this.state = {
active: false,
};
}
componentDidMount() {
document.body.addEventListener('click', e => {
this.setState({
active: false,
});
});
}
componentWillUnmount() {
document.body.removeEventListener('click');
}
handleClick() {
this.setState({
active: !this.state.active,
});
}
handleClickQr(e) {
e.stopPropagation();
}
render() {
return (
);
}
}
上面代码从感官上感觉确实可以实现要求的组件,但事实上我们运行上述代码可以发现,点击二维码本身也会导致二维码的隐藏,现在就有意思了,我们来仔细分析一下。
其实React事件并没有原生的绑定在真实的DOM上,而是使用了行为委托方式实现事件机制。
如上图所示,在JavaScript中,事件的触发实质上是要经过三个阶段:事件捕获、目标对象本身的事件处理和事件冒泡,假设在div中触发了click事件,实际上首先经历捕获阶段会由父级元素将事件一直传递到事件发生的元素,执行完目标事件本身的处理事件后,然后经历冒泡阶段,将事件从子元素向父元素冒泡。正因为事件在DOM的传递经历这样一个过程,从而为行为委托提供了可能。通俗地讲,行为委托的实质就是将子元素事件的处理委托给父级元素处理。React会将所有的事件都绑定在最外层(document),使用统一的事件监听,并在冒泡阶段处理事件,当挂载或者卸载组件时,只需要在通过的在统一的事件监听位置增加或者删除对象,因此可以提高效率。
并且React并没有使用原生的浏览器事件,而是在基于Virtual DOM的基础上实现了合成事件(SyntheticEvent),事件处理程序接收到的是SyntheticEvent的实例。SyntheticEvent完全符合W3C的标准,因此在事件层次上具有浏览器兼容性,与原生的浏览器事件一样拥有同样的接口,可以通过stopPropagation()和preventDefault()相应的中断。如果需要访问当原生的事件对象,可以通过引用nativeEvent获得。
上图为大致的React事件机制的流程图,React中的事件机制分为两个阶段:事件注册和事件触发:
事件注册
React在组件加载(mount)和更新(update)时,其中的ReactDOMComponent会对传入的事件属性进行处理,对相关事件进行注册和存储。document中注册的事件不处理具体的事件,仅对事件进行分发。ReactBrowserEventEmitter作为事件注册入口,担负着事件注册和事件触发。注册事件的回调函数由EventPluginHub来统一管理,根据事件的类型(type)和组件标识(_rootNodeID)为key唯一标识事件并进行存储。
事件执行
事件执行时,document上绑定事件ReactEventListener.dispatchEvent会对事件进行分发,根据之前存储的类型(type)和组件标识(_rootNodeID)找到触发事件的组件。ReactEventEmitter利用EventPluginHub中注入(inject)的plugins(例如:SimpleEventPlugin、EnterLeaveEventPlugin)会将原生的DOM事件转化成合成的事件,然后批量执行存储的回调函,回调函数的执行分为两步,第一步是将所有的合成事件放到事件队列里面,第二步是逐个执行。需要注意的是,浏览器原生会为每个事件的每个listener创建一个事件对象,可以从这个事件对象获取到事件的引用。这会造成高额的内存分配,React在启动时就会为每种对象分配内存池,用到某一个事件对象时就可以从这个内存池进行复用,节省内存。
再回到我们刚开始的问题,现在看起来就很没有很费解了,之所以会出现上面的问题是因为我们混用了React的事件机制和DOM原生的事件机制,认为通过:
handleClickQr(e) {
e.stopPropagation();
}
就能阻止原生的事件传播,其实在事件委托的情形下是不能实现这一点的。当然解决的办法也不复杂,不要将React事件和DOM原生事件混用。
componentDidMount() {
document.body.addEventListener('click', e => {
this.setState({
active: false,
});
});
document.querySelector('.code').addEventListener('click', e => {
e.stopPropagation();
})
}
componentWillUnmount() {
document.body.removeEventListener('click');
document.querySelector('.qr').removeEventListener('click');
}
或者通过事件原件对象中的target进行判断:
componentDidMount() {
document.body.addEventListener('click', e => {
if (e.target && e.target.matches('div.code')) {
return;
}
this.setState({
active: false,
});
});
}
都可以解决异常关闭的问题。
阻止事件冒泡
事实上阻止合成事件的方法有很多,但是有个原则:阻止合成事件的冒泡不会阻止原生事件的冒泡,但是阻止原生事件的冒泡会阻止合成事件的冒泡。
-----------------------以上是不是没明白,我们继续看下面解释---------------------------
React不会将事件处理函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件监听器。
这个监听器维持了一个映射,保存所有组件内部的事件监听和处理函数。当事件发生时,首先被这个统一的事件监听器处理,
然后在映射里找到真正的事件处理函数并调用。
合成事件分为以下三个主要过程:
一 事件注册
所有事件都会注册到document上,拥有统一的回调函数dispatchEvent来执行事件分发
二 事件合成
从原生的nativeEvent对象生成合成事件对象,同一种事件类型只能生成一个合成事件Event,如onclick这个类型的事件,dom上所有带有通过jsx绑定的onClick的回调函数都会按顺序(冒泡或者捕获)会放到Event._dispatchListeners 这个数组里,后面依次执行它
三 事件派发
每次触发事件都会执行根节点上 addEventListener 注册的回调,也就是 ReactEventListener.dispatchEvent 方法,事件分发入口函数。该函数的主要业务逻辑如下:
React合成事件的冒泡并不是真的冒泡,而是节点的遍历。
----------------------------来对比下与原生事件区别----------------------------------
React事件处理的特性
React的事件系统和浏览器事件系统相比,主要增加了两个特性:事件代理和事件自动绑定
1、事件代理
2、事件自动绑定
注意:事件的回调函数被绑定在React组件上,而不是原始的元素上,即事件回调函数中的
this所指的是组件实例而不是DOM元素;
3、合成事件
-------------------说了这么多,来总结下react合成事件优点------------------------
React在事件处理的优点