事件绑定
事件的绑定是在组件的挂载过程中完成的,具体来说,是在 completeWork 中完成的。关于 completeWork,我们已经在第 15 讲中学习过它的工作原理,这里需要你回忆起来的是 completeWork 中的以下三个动作:
completeWork 内部有三个关键动作:创建 DOM 节点(createInstance)、将 DOM 节点插入到 DOM 树中(appendAllChildren)、为 DOM 节点设置属性(finalizeInitialChildren)。
- 原生事件: 在 componentDidMount生命周期里边进行addEventListener绑定的事件
- 合成事件: 通过 JSX 方式绑定的事件,比如 onClick={() => this.handle()}
其中“为 DOM 节点设置属性”这个环节,会遍历 FiberNode 的 props key。当遍历到事件相关的 props 时,就会触发事件的注册链路。整个过程涉及的函数调用栈如下图所示:
事件触发
事件触发的本质是对 dispatchEvent 函数的调用。
- 循环收集符合条件的父节点,存进 path 数组中
- 模拟事件在捕获阶段的传播顺序,收集捕获阶段相关的节点实例与回调函数。path 数组中子节点在前,祖先节点在后。从后往前遍历 path 数组,模拟事件的捕获顺序,收集事件在捕获阶段对应的回调与实例。
- 模拟事件在冒泡阶段的传播顺序,收集冒泡阶段相关的节点实例与回调函数。从后往前遍历 path 数组,模拟事件的冒泡顺序,收集事件在捕获阶段对应的回调与实例。
简单的讲挂载的时候,通过listenerBank把事件存起来了,触发的时候document进行dispatchEvent,找到触发事件的最深的一个节点,向上遍历拿到所有的callback放在eventQueue. 根据事件类型构建event对象,遍历执行eventQueue
事件工作流
- 事件捕获
- 事件目标
- 事件冒泡
- 事件委托
- 先绑定再执行
事件差异
合成事件
React 实现了一个合成事件层,就是这个事件层,把 IE 和 W3C 标准之间的兼容问题给消除了。
- React 把事件委托到document 对象上
- 当真实dom元素触发事件,先处理原生事件,然后冒泡到document 对象上,再处理React 事件
- React 事件绑定的时刻是在reconciliation 阶段,会在原生事件绑定前执行
- 目的和优势:
- 进行浏览器兼容,React 采用的是顶层事件代理机制,能保证冒泡的一致性
- 事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或者释放事件对象(React17 中被废弃)
React17 之前(绑定到document上)
使用
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
class Event extends React.Component{
parentRef = React.createRef()
childRef = React.createRef()
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)
// React 会执行一个document.addEventListener('click', dispatchEvent)
// 这个注册是在React注册之后注册的,所以后执行
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'))
实现
```javascript <!DOCTYPE html>
react 16 中,如果同时注册了原生和react的事件冒泡和事件捕获,执行就会混乱,一会儿冒泡一会儿捕获,为了解决这个问题 react 17 分开注册。
<a name="A22iq"></a>
## React17 之后(绑定到根元素上)
事件委托的对象不再是document了,而是挂载容器。这样的话可以让一个页面上使用多个react版本共存。<br />useCapture 为true 将事件捕获和事件冒泡分开挂载。
<a name="HdVmV"></a>
### 实现
```javascript
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id='root'>
<div id='parent'>
<button id='parent'></button>
<button id='child'></button>
</div>
</div>
<script>
let root = document.getElementById('root')
let parent = document.getElementById('parent')
let child = document.getElementById('child')
// 注册react事件委托
root.addEventListener('click', dispatchEvent(event, true), true); // 捕获
root.addEventListener('click', dispatchEvent(event, false));
// useCapture 为true 表示事件捕获
function dispatchEvent(event, useCapture) {
let paths = []; // [child, parent, body]
let current = event.target;
//循环收集符合条件的父节点,存进 path 数组中
while (current) {
paths.push(current)
current = current.parentNode;
}
if (useCapture) {
// 模拟捕获 从外往内
for (let i = paths.length - 1; i >= 0; i--) {
let handler = paths[i].onClickCapture
handler && handler()
}
} else {
// 模拟冒泡 从内往外
for (let i = 0; i < paths.length; i++) {
let handler = paths[i].onClick
handler && handler()
}
}
}
parent.addEventListener('click', () => {
console.log('父元素原生事件捕获')
}, true)
parent.addEventListener('click', () => {
console.log('父元素原生事件冒泡')
})
child.addEventListener('click', () => {
console.log('子元素原生事件捕获')
}, true)
child.addEventListener('click', () => {
console.log('子元素原生事件冒泡')
})
parent.onClick = function () {
console.log('父元素React事件冒泡')
}
parent.onClickCapture = function () {
console.log('父元素React事件捕获')
}
child.onClick = function () {
console.log('子元素React事件冒泡')
}
child.onClickCapture = function () {
console.log('子元素React事件捕获')
}
</script>
</body>
</html>
捕获先处理 委托在 root上的捕获事件,再处理原生事件捕获,然后处理原生冒泡,冒泡到 root上处理委托到root上的冒泡。
事件系统变更
示例
点击button显示模态框,点击窗口关闭。
这个点击button 并不会显示,因为在 react 16 里,事件是被绑定到了document上,所以相当于在document上触发了两次事件,第一次事件会让show变为true,第二次事件马上会让show变为 false,所以就不会显示。要解决这个问题就要使用event.stopImmediatePropagation,会将绑定在同一个元素上的其他监听函数不再执行。