React事件机制
React基于浏览器事件机制
实现了一套自己的事件机制,包括:事件注册
、事件合成
、事件冒泡
、事件触发
等。
事件代理
React的事件并没有绑定到具体的dom节点上,而是绑定在了document上,然后由统一的事件监听器去监听事件的触发
React在内部维护了一个映射表来记录事件与组件的事件处理函数的对应关系。当某个事件触发时,React根据映射表将时间分派给指定的事件处理函数。当一个组件挂载与卸载时,相应的事件处理函数会自动被添加到事件监听器的内部映射表中或从表中删除。这样做简化了事件处理和回收机制,效率也提升很大。
合成事件(SyntheticEvent)
合成事件是React模拟DOM原生事件的一个事件对象,这些合成事件并没有绑定到对应的真实DOM
上,而是通过事件代理
的方式,将所有的事件绑定到了document
上。其优点如下:
- 兼容所有浏览器,兼容性好
- 方便React进行统一管理和进行事件处理。对于原生事件来说,浏览器会监听事件是否被触发,当事件触发时会创建一个事件对象,当多个事件被触发时就会创建多个事件对象,这样存在内部分配的问题。对于合成事件来说,有一个专门事件池来管理事件的创建和销毁,当需要使用事件时,就会在事件池中复用对象,事件回调结束后,再销毁事件对象上的属性,以便于下次再复用对象。这样做就不会为每个事件都创建一个事件对象,减少了内存的消耗,提升性能。
SyntheticEvent
是React合成事件的基类,定义了合成事件的基础公共属性和方法。
React会根据当前的事件类型来使用不同的合成事件对象,比如单击事件SyntheticMouseEvent
,焦点事件SyntheticFocusEvent
等,这些事件都继承SyntheticEvent
。
合成事件原理
- 当用户在为onClick添加函数时,React并没有将Click绑定到DOM上
- 而在document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装交给中间层
SyntheticEvent
(负责所有事件合成) - 然后使用统一的分发函数
dispatchEvent
将封装的事件内容交由真正的处理函数执行
事件注册
React事件注册过程要进行两件事:事件注册;事件存储。
在document上注册事件
在React组件挂载阶段,根据组件内声明的事件类型(onClick、onChange等),在document上通过addEventListener
注册事件,并指定统一的回调函数dispatchEvent
。
在document上不管注册什么事件,都具有统一的回调函数dispatchEvent
。所以对于同一种事件类型,不论在document上注册了几次,最终也只会保留一个有效实例,这样就可以减少内存消耗。
function TestComponent() {
handleFatherClick=()=>{
// ...
}
handleChildClick=()=>{
// ...
}
return <div className="father" onClick={this.handleFatherClick}>
<div className="child" onClick={this.handleChildClick}>child </div>
</div>
}
在上述代码中,事件类型是onClick
,由于React事件代理机制,会指定统一的回调函数dispatchEvent
,所以最终只会在document
上保留一个click
事件,类似document.addEventListener('click', dispatchEvent)
,可以看出React的事件是在DOM事件流的冒泡阶段被触发执行。
存储事件
React为了在触发事件时可以查到对应的回调去执行,会把组件内的所有事件统一存放到一个对象中(映射表)。首先根据事件类型分类存储,例如click
事件相关的统一存储到一个对象中,回调函数的存储采用键值对的形式,key代表组件的唯一标识,value对应的就是事件的回调函数。
React把所有事件和事件类型以及React组件进行了关联,在事件触发的时候根据当前的组件id
与事件类型
找到对应的回调函数
。
事件注册关键步骤
- 首先React生成要挂载的组件的虚拟DOM(通过babel对jsx进行词法分析,然后调用React.createElement()方法返回一个对象,这个对象就是虚拟DOM)
- 然后处理组件的props,判断props内是否有声明为事件的属性比如 onClick,onChange,这个时候得到事件类型 click,change 和对应的事件处理程序 fn
- 将这些事件在document上注册
- 在组件挂载完成后,将事件处理函数存储到
listenerBank
(映射表)中
React的事件注册的关键步骤如下图:
事件触发
React的事件触发只会发生在DOM事件流的冒泡阶段,因为在document
上注册时就默认是在冒泡阶段被触发执行。
其大致流程如下:
- 触发事件,开始DOM事件流:事件捕获阶段、处于目标阶段、事件冒泡阶段
- 当事件冒泡到
document
时,触发统一的事件回调函数ReactEventListener.dispatchEvent
- 根据原生事件对象(nativeEvent)找到事件触发节点对应的组件
- 开始事件的合成
- 根据当前事件类型生成指定的合成对象
- 封装原生事件和冒泡机制
- 查找当前元素以及它所有父级
- 在
listenerBank
查找事件回调并合成到event
- 批量执行合成事件内的回调函数
- 如果没有阻止冒泡,会将继续进行 DOM 事件流的冒泡(从 document 到 window),否则结束事件触发
dispatchEvent 进行事件触发
handleFatherClick=(e)=>{
console.log('father click');
}
handleChildClick=(e)=>{
console.log('child click');
}
render(){
return <div className="box">
<div className="father" onClick={this.handleFatherClick}> father
<div className="child" onClick={this.handleChildClick}>child </div>
</div>
</div>
}
当我们点击child div
时,会同时触发father
的click
事件。
在点击了child div
时,浏览器会捕获到这个事件,然后经过冒泡,事件被冒泡到document
上,交给统一事件处理函数dispatchEvent
对事件进行分发,根据之前在listenerBank
存储的键值对找到触发事件的组件,获取到触发这个事件的元素,遍历这个元素的所有父元素,依次对每一级元素进行处理。构造合成事件,将每一级的合成事件存储在eventQueue
事件队列中,然后批量执行
存储的回调函数。
react事件的执行顺序
事件的执行顺序为原生事件先执行,合成事件再执行。合成事件会冒泡到document上,所以尽量避免原生事件和合成事件混用。如果原生事件阻止冒泡,那么就会导致合成事件不执行。
react事件阻止冒泡的方式
react合成事件不能直接采用return false
的方式来阻止浏览器的默认行为,而必须明确调用event.preventDefault()
来阻止默认行为。
不能使用event.stopPropagation()
来阻止React事件冒泡,必须调用event.preventDefault()
来阻止React事件冒泡。
合成事件与原生事件的区别
- 写法不同。原生事件使用全部小写;React事件是用小驼峰
- 事件函数处理语法不同。原生事件使用字符串定义事件;React使用函数的形式
- 阻止冒泡的方式不同。原生事件可以通过
return false
或event.stopPropagation()
来阻止冒泡;React事件只能过event.preventDefault()
事件处理函数中的 event 能否异步访问
不能。在包含回调函数的当前事件循环执行完后,所有的event属性都会失效。
function handleClick(event) {
console.log(event.type) // => 有值
setTimeout(function() {
console.log(event.type) // => null
}, 0)
this.setState({
clickEvent: event // => null,因为在回调函数当前事件循环执行完后,所有event会变成null
})
this.setState){
clickEventType: event.type // => 有值
}
}
所有的原生事件都有对应的合成事件吗?
不是。「几乎」所有事件都代理到了 document,说明有例外,比如audio、video标签的一些媒体事件(如 onplay、onpause 等),是 document 所不具有,这些事件只能够在这些标签上进行事件进行代理,但依旧用统一的入口分发函数(dispatchEvent)进行绑定。