序言
React为了抹平不同浏览器之间事件系统的差异,实现了一套自己的事件系统。本文将深入浅出讨论React事件系统,将详细解释事件委托、合成事件、事件池和批量更新的概念,以及结合面试常考点为大家梳理React事件系统
一张脑图全文重点概括。(PS: 最近发现脑图学习能够很好地帮助自己验证对知识的掌握理解,建议大家在学完东西的时候可以自己绘制一个脑图来加强认识。)
1. 特点
-
进行浏览器兼容,实现更好的跨平台 React采用的是顶层事件代理机制,能够保证冒泡一致性,可以跨浏览器执行。React提供的合成事件用来抹平不同浏览器事件对象之间的差异,(具体差异可以参见常见问题部分)将不同平台事件模拟合成事件.
-
避免垃圾回收(频繁创建和删除事件对象) 在大规模DOM操作、频繁事件触发的场景,事件对象可能会被频繁创建和回收,引发垃圾回收,React引入事件池,在事件池中获取或释放事件对象,避免频繁地去创建和销毁,提高性能。
-
方便事件统一管理和事务机制
2. React事件系统的关键点
2.1 合成事件(Synthetic Events)
-
事件委托:React 不会直接在 DOM 元素上添加事件监听器,而是在文档根级别(17前是
document
,17后是root节点)添加一个事件监听器。当事件发生时,React 会使用事件的目标(target
)和当前目标(currentTarget
)来确定应该如何处理事件。这种方法称为事件委托,它可以减少内存占用并提高性能,因为不需要为每个元素添加和删除事件监听器。 -
合成事件映射多个原生事件: 简化开发者对事件的处理,React 的合成事件可以对应多个原生事件,例如,React 的
onChange
事件对应原生的input
、textarea
和select
的change
事件,以及input
和textarea
的input
事件。 -
合成事件对象: 创建一个合成事件对象(e)。模拟原生浏览器事件的接口,包括
stopPropagation
和preventDefault
方法,这两个方法在所有浏览器中都可用。
2.2 模拟事件传播机制
大概过程如下:
-
通过事件委托在document上绑定事件,在事件触发时(React安装事件监听器是在事件冒泡到document上的时候),根据e.target寻找触发事件的DOM节点,找到其对应的FiberNode(即虚拟DOM节点)
-
收集从当前FiberNode到根FiberNode之间所有注册的该事件对应回调
-
模拟冒泡捕获阶段
-
React16中:
- 1. 捕获事件会被unshift插入在回调队列前面
- 2. 冒泡事件会被Push在回调队列后面
- 3. 依次执行队列中的事件,按顺序来模拟捕获=>冒泡
-
React17中
- 通过addEventListener第三个参数来控制事件是在冒泡/捕获阶段执行(不传默认是false,是冒泡)
2.3 批量更新和异步执行
React 还提供了一些优化,例如批量更新和异步执行,这可以提高性能。例如,如果你在一个事件处理器中多次调用 setState
,React 会将这些更新批量处理,从而减少重新渲染的次数。
[常见问题中有配置的题目和验证Demo](### 3.3 阻止原生事件,合成事件会执行吗?组织合成事件,原生事件会执行吗)
2.4 事件委托机制原理?
2.4.1 含义与原理
事件机制
在浏览器中,事件机制通常是先捕=>执行=>然后冒泡。这个过程可以通过下图进行理解:
![[Pasted image 20230709103111.png]]
这个图展示了事件从 document 开始,经过各级元素,到达目标元素(target),然后再从目标元素冒泡回 document 的过程。
事件委托
也称事件代理,是利用事件冒泡的原理,将事件监听器添加到一个父元素上,这个监听器会处理由子元素触发并冒泡到父元素的所有事件。
事件委托工作原理:
基于两个主要的 DOM 事件特性:事件冒泡和事件目标。
-
事件冒泡:当一个事件(如点击或键盘按键)在一个元素(如一个按钮)上触发时,这个事件不仅仅会在这个元素上触发,还会在这个元素的所有祖先元素上触发,一直冒泡到
document
对象。这就是所谓的事件冒泡。 -
事件目标:当一个事件冒泡时,你可以通过事件对象的
target
属性来获取到最初触发事件的元素。这就是事件目标。
事件委托就是利用这两个特性,只在一个父元素上设置一个事件监听器,来处理在其所有子元素上触发的事件。当事件触发并冒泡到父元素时,我们可以检查事件的目标,看看事件是在哪个子元素上触发的,然后相应地处理事件。
2.4.2 事件委托的优点:
-
内存效率:由于只需要在父元素上添加一个事件监听器,而不是在每个子元素上都添加,所以可以大大减少内存占用,提高性能。
-
处理动态元素:如果在运行时动态添加了新的子元素,那么无需为新的子元素添加新的事件监听器,因为事件监听器已经在父元素上,新的子元素触发的事件会自动冒泡到父元素上。
需要注意的是,不是所有的事件都会冒泡,例如:focus、blur、mouseenter、mouseleave等,这些事件不会冒泡,因此在这些事件上不能使用事件委托。
2.4.3 事件委托代码示例
// 获取父元素
let parent = document.getElementById('parent');
// 在父元素上添加事件监听器
parent.addEventListener('click', function(e) {
// 检查事件的目标元素是否是我们想要的元素
if (e.target && e.target.nodeName == 'li') {
// 处理事件
console.log('List item ', e.target.id.replace('post-', ''), ' was clicked!');
}
});
2.5 原生事件 ? 合成事件?
-
原生事件: 在 componentDidMount生命周期里边进行addEventListener绑定的事件
-
合成事件: 通过 JSX 方式绑定的事件,比如 onClick={() => this.handle()}
-
合成事件的触发是基于浏览器的事件机制来实现的,通过冒泡机制冒泡到最顶层元素,然后再由 dispatchEvent 统一去处理。在react中,我们绑定的事件onClick等,并不是原生事件,而是由原生事件合成的React事件,比如 click事件合成为onClick事件。比如blur , change , input , keydown , keyup等 , 合成为onChange。
-
原生事件与合成事件写法最好不要混用,在于如果你阻止了原生事件的冒泡,可能会影响到其他写合成事件的地方
2.6 React事件批量更新、异步执行原理
批量更新
批量更新是指在一次事件处理中,如果有多次状态的更新,React不会立即更新视图,而是将这些更新操作放入一个队列中,然后在事件处理结束后,一次性执行这个队列中的所有更新操作,然后再更新视图。这样,无论状态更新多少次,DOM的操作只会进行一次。
异步执行
异步执行是指React在更新视图时,不是立即执行,而是将这个操作放入一个任务队列中,然后在浏览器的空闲时间,再去执行这个任务。这样可以避免在忙碌的时候进行视图更新,从而阻塞浏览器的其他任务。
实现原理
React的批量更新和异步执行的实现,主要依赖于React的事务机制和调度机制。
事务机制(Transaction)
React 的事务机制是一种用于封装和控制代码执行过程的模式,它的主要目的是确保在执行一系列操作时,无论过程中是否出现错误,都能保证在操作开始前后,系统的状态是一致的。
事务机制的关键在于它的两个阶段:初始化阶段和关闭阶段。在初始化阶段,你可以设置系统到一个特定的状态,或者创建一些需要在事务过程中使用的资源。然后,你可以执行你的主要操作。无论这个操作是否成功,事务都会在操作结束后调用所有包装器的关闭方法。
关闭方法的作用是清理初始化方法创建的资源,或者将系统恢复到事务开始前的状态。这就是事务如何保证系统状态一致性的:无论主要操作的结果如何,事务都会尽力将系统恢复到一个稳定的状态。
以下是事务的主要步骤:
-
初始化:在事务开始时,会调用所有包装器的初始化方法。这些方法可以用来设置系统到一个特定的状态,或者创建一些需要在事务过程中使用的资源。
-
执行:在所有初始化方法执行完毕后,事务会执行主要的操作。这个操作通常是由用户提供的,例如更新 DOM、改变组件的状态等。
-
关闭:无论主要的操作是否成功,事务都会在操作结束后调用所有包装器的关闭方法。这些方法可以用来清理初始化方法创建的资源,或者将系统恢复到事务开始前的状态。
调度机制
是指React在执行更新操作时,会将这个操作放入一个任务队列中,然后在浏览器的空闲时间,再去执行这个任务。这个机制的实现,主要依赖于浏览器的requestIdleCallback
方法。
2.3.1 setState是同步还是异步?
(Ps: setState是class组件中的setState)
- 异步setState:React中的setState方法是异步的,在调用setState后,React并不会立即更新组件,而是将这个对象放到更新队列中,稍后进行批量更新。
- 同步执行的情况:虽然setState通常是异步的,但在某些情况下,React会同步执行更新。例如,在React的生命周期方法或React事件处理函数中,setState是异步的,但如果在setTimeout或者原生事件中调用setState,那么它是同步的。
解释同步的情况
React有自己的一套事件处理系统和调度机制。当你在React组件的生命周期方法或者React的事件处理器(例如onClick或者onChange)中调用setState
,这些调用会被React的事件系统捕获,并且React会根据需要进行批处理和调度,这通常会导致异步的状态更新。(
然而,当你在setTimeout
或者原生事件(例如window.addEventListener
)的回调函数中调用setState
,这些调用就不再被React的事件系统所控制,而是直接在JavaScript的运行环境中执行。在这种情况下,React无法对这些setState
调用进行批处理和调度,因此它们会立即执行,也就是同步的。
这是因为setTimeout
和原生事件处理器都是在浏览器的事件循环中运行的,而不是React的事件系统。当你在这些函数中调用setState
,你实际上是在告诉浏览器“在下一个事件循环中运行这个函数”。因此,这个函数的执行已经跳出了React的控制范围,进入了浏览器的事件循环,也就是JavaScript的运行环境。
(不推荐在类组件的生命周期之外直接调用setState
,因为这可能会导致意外的行为或错误。)
2.3.2 hook中的useState是同步还是异步?
React 的 useState
hook 不是异步的,但它的行为可能会让你误以为它是异步的。这是因为 React 有一个优化机制,称为批量更新(batching),它可以将多个状态更新合并到一个渲染周期中。
在函数组件中,当你调用 useState
的更新函数(例如 setCount
)时,React 会将新的状态值放入一个队列中,而不是立即更新状态。然后,React 会在稍后的时间(通常是下一个事件循环)进行一次渲染,这时所有的状态更新都会被应用。这就是为什么 useState
的更新函数可能会让人误以为它是异步的。
在 React 16 和 17 中,React 只在 React 的事件处理函数中进行批量更新。这意味着如果你在一个非 React 的事件处理函数(例如一个 setTimeout
或 Promise.then
的回调函数)中调用 useState
的更新函数,React 不会进行批量更新,而是立即更新状态并重新渲染组件。
在 React 18 中,React 引入了一个新的特性,叫做并发模式(Concurrent Mode)。在并发模式下,React 可以在任何时候进行批量更新,不仅仅是在 React 的事件处理函数中。这意味着即使你在一个非 React 的事件处理函数中调用 useState
的更新函数,React 也会进行批量更新。
3. 常见问题
3.1 React中如何绑定捕获事件,冒泡事件?
从React 17版本开始,React引入了新的事件系统,并且增加了对捕获阶段事件监听的支持。你可以通过在事件名称后面添加Capture
来绑定捕获阶段的事件监听。
例如,如果你想在捕获阶段监听一个点击事件,你可以这样做:
class ExampleComponent extends React.Component {
handleClickCapture(event) {
console.log('捕获阶段');
}
handleClick(event) {
console.log('冒泡阶段');
}
render() {
return (
<div
onClickCapture={this.handleClickCapture.bind(this)}
onClick={this.handleClick.bind(this)}
>
点击我
</div>
);
}
}
上述代码中的onClickCapture
就是在捕获阶段监听点击事件,onClick
则是在冒泡阶段监听点击事件。点击这个元素,会首先触发捕获阶段的事件监听(handleClickCapture
方法),然后触发冒泡阶段的事件监听(handleClick
方法)。
3.2 如何阻止事件默认行为和冒泡?
阻止事件的默认行为和冒泡则需要使用合成事件对象的 preventDefault
和 stopPropagation
方法。注意,简单地在事件处理函数中返回 false 是无法阻止默认行为和冒泡的。
3.3 阻止原生事件,合成事件会执行吗?组织合成事件,原生事件会执行吗
React 的合成事件系统和浏览器的原生事件是两个独立的系统,它们的执行顺序在 React 17 版本之前和之后是有所变化的。
React 16 :
原生事件捕获 -> 原生事件冒泡 (冒泡到外层的时候绑定合成事件的执行器)-> 合成事件捕获 -> 合成事件冒泡
整体结论:
- 原生、合成事件捕获、冒泡阶段都阻止冒泡: 只有原生事件的捕获执行
- 原生、合成事件冒泡阶段都阻止冒泡:原生事件的捕获冒泡都执行、合成事件不执行
demo验证
import * as React from "react";
import "./styles.css";
export class ExampleComponent extends React.Component {
componentDidMount() {
// 添加原生事件监听器
// 通过绑定时间时第三个参数传true在捕获阶段添加事件
document.getElementById("btn").addEventListener(
"click",
(e) => {
console.log("原生事件--捕获阶段");
e.stopPropagation();
console.log("阻止原生事件冒泡");
},
true
);
document.getElementById("btn").addEventListener("click", (e) => {
console.log("原生事件--冒泡阶段");
e.stopPropagation();
});
document.getElementById("btn2").addEventListener(
"click",
(e) => {
console.log("原生事件--捕获阶段");
},
true
);
document.getElementById("btn2").addEventListener("click", (e) => {
console.log("原生事件--冒泡阶段");
e.stopPropagation();
});
}
handleClickCaputure = (e) => {
console.log("合成事件捕获阶段执行");
e.stopPropagation();
};
handleClickCaputureNotStop = (e) => {
console.log("合成事件捕获阶段执行");
};
handleClick = (e) => {
console.log("合成事件冒泡阶段执行");
e.stopPropagation();
};
render() {
return (
<>
<button
id="btn"
onClick={this.handleClick}
onClickCapture={this.handleClickCaputure}
>
原生、合成事件捕获、冒泡阶段都阻止冒泡: 只有原生事件的捕获执行
</button>
<button
id="btn2"
onClick={this.handleClick}
onClickCapture={this.handleClickCaputureNotStop}
>
原生、合成事件冒泡阶段都阻止冒泡:原生事件的捕获冒泡都执行、合成事件不执行
</button>
</>
);
}
}
export default ExampleComponent;
React17:
合成事件捕获 -> 原生事件捕获 -> 原生事件冒泡 -> 合成事件冒泡
demo的验证和执行阶段是吻合的,整体结论是:
- 原生、合成事件捕获、冒泡阶段都阻止冒泡: 只有合成事件的捕获阶段执行
- 原生、合成事件冒泡阶段都阻止冒泡:原生冒泡阶段执行,合成事件冒泡不执行
DEMO验证
import * as React from "react";
import "./styles.css";
export class ExampleComponent extends React.Component {
componentDidMount() {
// 添加原生事件监听器
// 通过绑定时间时第三个参数传true在捕获阶段添加事件
document.getElementById("btn").addEventListener(
"click",
(e) => {
console.log("原生事件--捕获阶段");
e.stopPropagation();
console.log("阻止原生事件冒泡");
},
true
);
document.getElementById("btn").addEventListener("click", (e) => {
console.log("原生事件--冒泡阶段");
e.stopPropagation();
});
document.getElementById("btn2").addEventListener(
"click",
(e) => {
console.log("原生事件--捕获阶段");
},
true
);
document.getElementById("btn2").addEventListener("click", (e) => {
console.log("原生事件--冒泡阶段");
e.stopPropagation();
});
}
handleClickCaputure = (e) => {
console.log("合成事件捕获阶段执行");
e.stopPropagation();
};
handleClickCaputureNotStop = (e) => {
console.log("合成事件捕获阶段执行");
};
handleClick = (e) => {
console.log("合成事件冒泡阶段执行");
e.stopPropagation();
};
render() {
return (
<>
<button
id="btn"
onClick={this.handleClick}
onClickCapture={this.handleClickCaputure}
>
原生、合成事件捕获、冒泡阶段都阻止冒泡: 只有合成事件的捕获阶段执行
</button>
<button
id="btn2"
onClick={this.handleClick}
onClickCapture={this.handleClickCaputureNotStop}
>
原生、合成事件冒泡阶段都阻止冒泡:原生冒泡阶段执行,合成事件冒泡不执行
</button>
</>
);
}
}
export default ExampleComponent;
3.4 React 17 VS React 16 不同点
- 事件绑定: 17事件绑定在id为root的外层容器上,而不是document上。这样好处是有利于微前端的,微前端一个前端系统中可能有多个应用,如果继续采取全部绑定在document上,那么可能多应用下会出现问题。
- 模拟冒泡和捕获: React17是通过分别绑定原生的冒泡、捕获事件来模拟。React16是通过队列的顺序来模拟,16的冒泡捕获都是在原生事件冒泡之后触发的。React17的处理使得React事件系统和原生事件更具一致性。
- React17去事件池: 在现代浏览器中事件池性能优化不大,但是和原生事件的表现不够一致。(在事件之外访问e会出现问题,因为e会被复用和重置) React 17 removes the “event pooling” optimization from React. It doesn’t improve performance in modern browsers and confuses even experienced React users:
3.5 React中的事件为什么需要手动去绑定this指向?
JSX语法实际上是createElement的语法糖,在createElement的时候讲方法进行赋值,丢失了上下文,在调用的时候又因开启严格模式,this指向指向了undefiened
3.6 react如何处理事件的注册和绑定过程
React 使用合成事件(Synthetic Event)来对事件进行处理。事件注册通常在 JSX 中进行,例如 <button onClick={this.handleClick}>Click me</button>
。
但实际上,React 在内部为每种事件类型维护一个事件池,并未真正在 DOM 上注册事件。事件绑定则发生在根节点(React 17 之后为容器节点),不是具体的 DOM 元素上。React 采用事件代理的方式,只在顶层注册一个事件处理函数,然后在内部进行事件的管理和调度。具体流程如下:
- 在执行 diff 算法时,若发现存在 React 合成事件,例如 onClick,会按照事件系统逻辑单独处理。
- 根据合成事件类型,找到对应的原生事件类型,大部分事件按照冒泡逻辑处理,少数事件(如 scroll)按照捕获逻辑处理。
- 调用内部方法 addTrappedEventListener 进行真正的事件绑定。事件绑定在 document 上,dispatchEvent 为统一的事件处理函数。
3.7 React合成事件抹平了在浏览器间的差异,这些差异点?
-
事件模型:大部分现代浏览器使用的是W3C的事件模型,即先捕获后冒泡。但是IE8及更早版本的IE使用的是自己的事件模型,即只有冒泡阶段。
-
事件名称:例如,某些浏览器可能使用"transitionend"来表示CSS过渡结束的事件,而其他浏览器可能使用"webkitTransitionEnd"。
-
事件对象的属性和方法:例如,所有的浏览器都提供了一个名为"target"的属性来表示触发事件的元素,但是IE8及更早版本的IE使用的是"srcElement"。另一个例子是在阻止默认行为时,大部分浏览器使用的方法是"preventDefault",但是IE8及更早版本的IE使用的是"returnValue"。
参考文章
卡颂60行代码实现React的事件系统 一文读懂React合成事件 【React】786- 探索 React 合成事件 一文吃透react事件系统原理 由浅到深的React合成事件 React17事件机制