React合成事件
React17版本开始,对事件系统的两个重要变更:
- React17以前将事件(包括捕获及冒泡)委托到document的冒泡阶段触发。React17开始将冒泡事件委托到容器root的冒泡阶段触发,将捕获事件委托到容器root的捕获阶段触发
- 移除事件池
本文会以原生js模拟实现react合成事件的绑定时机,以帮助理解合成事件的绑定及执行时机
合成事件的基础
- 合成事件的执行时机。
- React17以前,由于合成事件委托在document的冒泡事件上,因此合成事件在document的冒泡阶段执行。 先执行document原生捕获事件,然后是原生的捕获事件,原生的冒泡事件,react捕获事件,react冒泡事件,document原生冒泡事件
- React17及以后,合成事件捕获以及合成事件冒泡分别委托在root容器的捕获事件以及冒泡事件上。
- 目的和优势
- 进行浏览器兼容,React采用的是顶层事件代理机制,能够保证冒泡一致性
- 事件对象可能会被频繁创建和回收,因此React引入事件池,在事件池中获取或释放事件对象(React17后被废弃)
- 如果对事件委托不熟悉的可以自行复习一下
React17以前版本代码示例
合成事件委托绑定到document上,在document的冒泡阶段执行。以react@16.14.0,react-dom@16.14.0为例,观察控制台输出可以看出,先打印所有的原生事件,其次在document的冒泡阶段才执行完所有的捕获以及冒泡事件
import React from 'react'
import ReactDOM from 'react-dom'
class App extends React.Component {
parentRef = React.createRef();
childRef = React.createRef();
constructor(props){
super(props)
}
componentDidMount(){
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('子元素原生冒泡')
})
document.addEventListener('click', () => {
console.log('document捕获')
}, true)
document.addEventListener('click', () => {
console.log('document冒泡')
})
}
parentBubble = () => {
console.log('父元素React事件冒泡')
}
childBubble = () => {
console.log('子元素React事件冒泡')
}
parentCapture = () => {
console.log('父元素React事件捕获')
}
childCapture = () => {
console.log('子元素React事件捕获')
}
render(){
return (
<div ref={this.parentRef} onClick={this.parentBubble} onClickCapture={this.parentCapture}>
<p ref={this.childRef} onClick={this.childBubble} onClickCapture={this.childCapture}>
事件执行顺序
</p>
</div>
)
}
}
ReactDOM.render(<App />, document.getElementById('root'))
// 打印顺序:
// document捕获
// 父元素原生捕获
// 子元素原生捕获
// 子元素原生冒泡
// 父元素原生冒泡
// 父元素React事件捕获
// 子元素React事件捕获
// 子元素React事件冒泡
// 父元素React事件冒泡
// document冒泡
模拟React17以前合成事件实现
合成事件绑定在document的冒泡阶段。点击目标元素时,在document的冒泡阶段获取目标元素上所有的父元素及祖先元素。然后执行这些元素的捕获及冒泡事件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>React16 Event</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="parent">
<p id="child">事件执行</p>
</div>
<script>
function dispatchEvent(event){
const paths = [];
let current = event.target;
// 获取路径上的所有元素
while(current){
paths.push(current)
current = current.parentNode
}
// 执行合成的捕获事件
for(let i = paths.length-1; i >= 0; i--){
const handler = paths[i].onClickCapture;
handler && handler()
}
// 执行合成的冒泡事件
for(let i = 0; i < paths.length; i++){
const handler = paths[i].onClick;
handler && handler()
}
}
// React合成事件绑定到document的冒泡阶段执行
document.addEventListener('click', dispatchEvent)
const parent = document.getElementById('parent')
const child = document.getElementById('child')
parent.addEventListener('click', () => {
console.log('父元素原生捕获')
}, true)
parent.addEventListener('click', () => {
console.log('父元素原生冒泡')
})
child.addEventListener('click', () => {
console.log('子元素原生捕获')
}, true)
child.addEventListener('click', () => {
console.log('子元素原生冒泡')
})
document.addEventListener('click', () => {
console.log('document捕获')
}, true)
document.addEventListener('click', () => {
console.log('document冒泡')
})
// 模拟合成事件
parent.onClickCapture = () => {
console.log('父元素React事件捕获')
}
parent.onClick = () => {
console.log('父元素React事件冒泡')
}
child.onClickCapture = () => {
console.log('子元素React事件捕获')
}
child.onClick = () => {
console.log('子元素React事件冒泡')
}
</script>
</body>
</html>
React17及以后
事件委托不再绑定到document上,而是绑定到react挂载的容器上,即render方法挂载的容器root。在root的捕获阶段执行react的合成的捕获事件,在root的冒泡阶段执行react的合成的冒泡事件。还是以上面的例子为例。将react,react-dom依赖更改为17.0.1并重新安装。
此时打印顺序:
document捕获
父元素React事件捕获
子元素React事件捕获
父元素原生捕获
子元素原生捕获
子元素原生冒泡
父元素原生冒泡
子元素React事件冒泡
父元素React事件冒泡
document冒泡
模拟React17合成事件实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>React16 Event</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<div id="root">
<div id="parent">
<p id="child">事件执行</p>
</div>
</div>
<script>
function dispatchEvent(event, useCapture){
const paths = [];
let current = event.target;
while(current){
paths.push(current)
current = current.parentNode
}
// 模拟捕获和冒泡,其实在这个时候,原生的捕获阶段已经结束了
if(useCapture){
// 捕获阶段
for(let i = paths.length-1; i >= 0; i--){
const handler = paths[i].onClickCapture;
handler && handler()
}
} else {
// 冒泡阶段
for(let i = 0; i < paths.length; i++){
const handler = paths[i].onClick;
handler && handler()
}
}
}
const root = document.getElementById('root')
const parent = document.getElementById('parent')
const child = document.getElementById('child')
// 注册React事件的事件委托
root.addEventListener('click', event => dispatchEvent(event, true), true) // 捕获阶段监听
root.addEventListener('click', dispatchEvent) // 冒泡阶段监听
parent.addEventListener('click', () => {
console.log('父元素原生捕获')
}, true)
parent.addEventListener('click', () => {
console.log('父元素原生冒泡')
})
child.addEventListener('click', () => {
console.log('子元素原生捕获')
}, true)
child.addEventListener('click', () => {
console.log('子元素原生冒泡')
})
document.addEventListener('click', () => {
console.log('document原生事件捕获')
}, true)
document.addEventListener('click', () => {
console.log('document原生事件冒泡')
})
root.addEventListener('click', () => {
console.log('root原生事件捕获')
}, true)
root.addEventListener('click', () => {
console.log('root原生事件冒泡')
})
parent.onClickCapture = () => {
console.log('父元素React事件捕获')
}
parent.onClick = () => {
console.log('父元素React事件冒泡')
}
child.onClickCapture = () => {
console.log('子元素React事件捕获')
}
child.onClick = () => {
console.log('子元素React事件冒泡')
}
</script>
</body>
</html>
stopPropagation以及stopImmediatePropagation
- event.stopPropagation阻止向上冒泡,但是本元素其余的监听函数还是会执行
document.addEventListener('click', e => {
e.stopPropagation() // 不再向上冒泡,但还是会打印2
console.log(1)
})
document.addEventListener('click', e => {
console.log(2)
})
- event.stopImmediatePropagation阻止向上冒泡,同时本元素其余的监听函数不会执行
document.addEventListener('click', e => {
e.stopImmediatePropagation() // 不再向上冒泡,同时2不会打印
console.log(1)
})
document.addEventListener('click', e => {
console.log(2)
})