事件机制
WARNING
React 事件机制初步研究,机制流程部分写得较为笼统,后续还要继续研究。
React 基于浏览器的事件机制,自身实现了一套事件机制,称为合成事件。包括事件注册、事件合成、事件冒泡、事件分发等。 它根据 W3C 标准定义事件,兼容所有浏览器,拥有与原生事件相同的接口。
为什么要另外再实现一套事件机制呢?
- 磨平各浏览器之间的差异
- 方便事件统一管理
事件机制流程
React 事件机制主要可划分为两个部分:事件注册、事件分发。
- 事件注册:事件并没有注册到具体的
DOM节点上,而是注册到document根节点上(React v17开始注册到root容器节点上),由统一的事件处理函数dispatchEvent来执行事件分发。(利用事件冒泡原理,任何节点触发的事件都能冒泡到最外层元素) - 事件分发:首先生成合成事件,同一种事件类型只能生成一个合成事件。以
onClick事件为例,所有通过JSX绑定的onClick事件函数都会按序(冒泡或捕获)放到Event._dispatchListeners这个数组里,后面依次执行它。


原生事件 & 合成事件
主要区别
- 对于事件名称命名方式,原生事件为全小写,合成事件采用小驼峰
- 对于事件函数处理语法,原生事件使用引号,合成事件使用大括号
- 对于阻止浏览器默认行为,原生事件使用 return false,合成事件采用 event.prenventDefault()
// 原生事件
<button onclick="return false">按钮</button>
// 合成事件
<button onClick={e => e.prenventDefault()}>按钮</button>
执行顺序
原生和合成事件的执行顺序在 React v16 和 v17 版本有较大变化。
v16版本,原生事件执行顺序恒早于合成事件。v17版本,捕获模式,合成事件执行顺序早于原生事件。冒泡模式,反之。
举个栗子:
import React, { createRef } from "react"
export default class App extends React.Component {
parentRef = createRef()
childRef = createRef()
componentDidMount() {
// 原生事件捕获监听
this.parentRef.current.addEventListener('click', () => console.log("原生事件,捕获阶段,父元素事件监听!"), true)
this.childRef.current.addEventListener('click', () => console.log("原生事件,捕获阶段,子元素事件监听!"), true)
document.addEventListener('click', () => console.log("原生事件,捕获阶段,document 事件监听!"), true)
// 原生事件冒泡监听
this.parentRef.current.addEventListener('click', () => console.log("原生事件,冒泡阶段,父元素事件监听!"))
this.childRef.current.addEventListener('click', () => console.log("原生事件,冒泡阶段,子元素事件监听!"))
document.addEventListener('click', () => console.log("原生事件,冒泡阶段,document 事件监听!"))
}
// 合成事件捕获监听
handleParentCaptureClick = () => console.log("合成事件,捕获阶段,父元素事件监听!")
handleChildCaptureClick = () => console.log("合成事件,捕获阶段,子元素事件监听!")
// 合成事件冒泡监听
handleParentBubbleClick = () => console.log("合成事件,冒泡阶段,父元素事件监听!")
handleChildBubbleClick = () => console.log("合成事件,冒泡阶段,子元素事件监听!")
render() {
return (
<div ref={this.parentRef} onClick={this.handleParentBubbleClick} onClickCapture={this.handleParentCaptureClick}>
<div ref={this.childRef} onClick={this.handleChildBubbleClick} onClickCapture={this.handleChildCaptureClick}>
点击查看原生与合成事件执行顺序
</div>
</div>
)
}
}
React v16 版本输出结果如下:
原生事件,捕获阶段,document 事件监听!
原生事件,捕获阶段,父元素事件监听!
原生事件,捕获阶段,子元素事件监听!
原生事件,冒泡阶段,子元素事件监听!
原生事件,冒泡阶段,父元素事件监听!
合成事件,捕获阶段,父元素事件监听!
合成事件,捕获阶段,子元素事件监听!
合成事件,冒泡阶段,子元素事件监听!
合成事件,冒泡阶段,父元素事件监听!
原生事件,冒泡阶段,document 事件监听!
React v17 版本输出结果如下:
原生事件,捕获阶段,document 事件监听!
合成事件,捕获阶段,父元素事件监听!
合成事件,捕获阶段,子元素事件监听!
原生事件,捕获阶段,父元素事件监听!
原生事件,捕获阶段,子元素事件监听!
原生事件,冒泡阶段,子元素事件监听!
原生事件,冒泡阶段,父元素事件监听!
合成事件,冒泡阶段,子元素事件监听!
合成事件,冒泡阶段,父元素事件监听!
原生事件,冒泡阶段,document 事件监听!
为什么 v16 和 v17 版本执行顺序有这么大变化呢?
v16版本的捕获模式仅是模拟。实质是当事件冒泡到document时,遍历元素节点,模拟捕获和冒泡模式的处理方式,获取对应模式下的事件函数,然后调用它们。v17之后,加入了原生捕获模式的支持,对齐了浏览器原生标准。
TIP
v16 版本中,当原生和合成事件同时绑定在 document 上的时候,由于合成事件是先注册的,因而会先触发。
// 按照注册顺序依次执行
document.addEventListener('click', 合成事件)
document.addEventListener('click', 原生事件)
阻止冒泡
阻止原生事件冒泡,会影响合成事件冒泡
import React, { createRef } from "react"
export default class App extends React.Component {
parentRef = createRef()
childRef = createRef()
componentDidMount() {
// 原生事件冒泡监听
this.parentRef.current.addEventListener('click', () => console.log("原生事件,冒泡阶段,父元素事件监听!"))
this.childRef.current.addEventListener('click', e => {
console.log("原生事件,冒泡阶段,子元素事件监听!")
e.stopPropagation()
console.log("阻止原生事件冒泡!")
})
document.addEventListener('click', () => console.log("原生事件,冒泡阶段,document 事件监听!"))
}
// 合成事件冒泡监听
handleParentClick = () => console.log("合成事件,冒泡阶段,父元素事件监听!")
handleChildClick = () => console.log("合成事件,冒泡阶段,子元素事件监听!")
render() {
return (
<div ref={this.parentRef} onClick={this.handleParentClick}>
<div ref={this.childRef} onClick={this.handleChildClick}>
点击查看原生与合成事件执行顺序
</div>
</div>
)
}
}
// React v16 输出
原生事件,冒泡阶段,子元素事件监听!
阻止原生事件冒泡!
// React v17 输出
原生事件,冒泡阶段,子元素事件监听!
阻止原生事件冒泡!
阻止合成事件冒泡,不会影响原生事件冒泡(v17 版本会影响)
import React, { createRef } from "react"
export default class App extends React.Component {
parentRef = createRef()
childRef = createRef()
componentDidMount() {
// 原生事件冒泡监听
this.parentRef.current.addEventListener('click', () => console.log("原生事件,冒泡阶段,父元素事件监听!"))
this.childRef.current.addEventListener('click', () => console.log("原生事件,冒泡阶段,子元素事件监听!"))
document.addEventListener('click', () => console.log("原生事件,冒泡阶段,document 事件监听!"))
}
// 合成事件冒泡监听
handleParentClick = () => console.log("合成事件,冒泡阶段,父元素事件监听!")
handleChildClick = e => {
console.log("合成事件,冒泡阶段,子元素事件监听!")
e.stopPropagation()
console.log("阻止合成事件冒泡!")
// 阻止 documet 上其他事件监听函数的执行
// e.nativeEvent.stopImmediatePropagation()
}
render() {
return (
<div ref={this.parentRef} onClick={this.handleParentClick}>
<div ref={this.childRef} onClick={this.handleChildClick}>
点击查看原生与合成事件执行顺序
</div>
</div>
)
}
}
// React v16 输出
原生事件,冒泡阶段,子元素事件监听!
原生事件,冒泡阶段,父元素事件监听!
合成事件,冒泡阶段,子元素事件监听!
阻止合成事件冒泡!
原生事件,冒泡阶段,document 事件监听!
// React v17 输出
原生事件,冒泡阶段,子元素事件监听!
原生事件,冒泡阶段,父元素事件监听!
合成事件,冒泡阶段,子元素事件监听!
阻止合成事件冒泡!
TIP
v16版本中,所有事件将会被集中注册到document节点上,等到事件冒泡到document上时才会触发合成事件。当在合成事件上阻止冒泡时,原生事件已经冒泡完了,因而无法阻止原生事件冒泡。v17版本中,所有事件将会被集中注册到root节点上,等到事件冒泡到root上时才会触发合成事件。当在合成事件上阻止冒泡时,原生事件还没有冒泡完成(根节点root和最外层文档节点document之间还有一段冒泡距离),这段距离内是可以阻止原生事件冒泡的。