Appearance
React 18
导航目录
什么是 React?
React 是一个用于构建用户界面的 JavaScript 库,核心思想是通过组件化和声明式编程描述 UI。
它具有以下特点:
- 组件化:将页面拆分为独立、可复用的组件,便于开发、测试和维护。
- 声明式:开发者只需描述状态对应的界面,React 负责完成视图更新。
- 高效更新:通过虚拟 DOM、协调过程和 Diff 策略,尽量减少不必要的真实 DOM 操作。
- 单向数据流:数据通常由父组件传递给子组件,使状态变化更加清晰、可预测。
- 生态丰富:可结合路由、状态管理和服务端渲染方案构建完整应用。
- 跨平台:除 Web 外,还可借助 React Native 等技术开发移动端应用。
需要注意:React 主要负责视图层,并不是一个开箱即用的完整前端框架。
React 能干什么?
React 主要用于构建交互式用户界面,常见应用场景包括:
- 构建大型 Web 应用:通过组件化拆分复杂页面,提高代码的复用性和可维护性。
- 开发单页应用:配合 React Router 等工具实现页面路由与无刷新切换。
- 实现复杂交互界面:利用状态驱动视图,处理表单、列表、弹窗和实时数据等场景。
- 服务端渲染与全栈开发:结合 Next.js 等框架实现 SSR、SSG 和全栈应用。
- 跨平台应用开发:通过 React Native 构建 Android 和 iOS 应用。
组件化
- 组件化是将复杂页面拆分为多个独立、可复用组件的开发方式。每个组件负责特定的界面、状态和交互逻辑,再通过组合形成完整应用。
组件化具有以下优势:
- 职责清晰:每个组件只关注自身功能,有利于实现高内聚。
- 降低耦合:组件通过 Props、事件或 Context 等明确接口协作,减少内部实现之间的依赖。
- 提高复用性:通用组件可以在不同页面和业务场景中重复使用。
- 便于维护:修改某个组件时,通常不会影响其他无关模块。
- 易于测试:可以单独测试组件的渲染结果和交互行为。
- 支持组合:通过组合小型组件逐步构建复杂界面。
diff 算法时间复杂度
传统 diff 算法:
- 通用树 Diff 需要比较两棵树中节点之间的多种对应关系,并计算插入、删除、移动等操作。
- 时间复杂度可达到 O(n³);当节点数量较多时,计算成本很高,不适合频繁更新的 UI 场景。
React diff:
- React 只对同层节点进行比较,不进行跨层级匹配。
- 通过同层比较、元素类型判断和 key 匹配,将常见场景下的 Diff 时间复杂度降低到O(n)。
- 如果同一位置的新旧元素类型相同,React 会复用已有节点,并继续比较属性和子节点。
- 如果元素类型不同,React 会卸载旧子树并创建新子树,不再继续比较两棵不同类型的子树。
- 在列表比较中,React 使用 key 判断节点能否复用,并识别新增、删除和移动的节点。
什么是 JSX
TIP
在 React17 以前,babel 是调用的 React.createElement,并且页面需要引入
在 React17 之后,babel 是调用的(react/jsx-runtimejsx)函数,并且页面无需引入
- Jsx(JavaScript XML)是一个语法糖,是对 JavaScript 的扩展,即可以写 html 也可以写 js
- 浏览器支持的是 ECMAScript 标准,所以代码在编译阶段会通过babel-preset-react转换成 ReactElement
- ReactElement 就是我们所说的虚拟节点
createPortal
- 将组件渲染到对应的容器中
ReactDOM.createPortal(child, container) - 常用于弹窗、抽屉、全局提示和 Tooltip 等需要脱离父元素布局或层叠上下文的场景。
虚拟的 dom 优缺点
优点
- 简化开发:通过声明式方式描述 UI,减少手动操作真实 DOM,并由 React 统一处理属性、事件及部分浏览器兼容问题。
- 差异化更新:状态变化后通过 Diff 找出差异,只提交必要的真实 DOM 更新。
- 跨平台:虚拟 DOM 不直接依赖浏览器,可通过不同渲染器支持 Web、Android、iOS 等平台。
缺点
- 不利于 SEO:虚拟 DOM 是在浏览器中运行的,无法直接用于服务端渲染,影响首屏加载速度和 SEO。
- 首屏加载速度慢:首次渲染时需要创建完整的虚拟 DOM 树,再生成真实 DOM,增加了加载时间。
函数组件和类组件的区别
| 特性 | 函数组件 | 类组件 |
|---|---|---|
| 定义方式 | JavaScript 函数 | ES6 类继承 React.Component |
| 状态管理 | useState/useReducer Hooks | this.state + this.setState |
| 生命周期 | useEffect Hook | 生命周期方法(componentDidMount等) |
this 绑定 | 无 this,避免绑定问题 | 需要处理 this 绑定 |
| 代码复用 | 自定义 Hooks | 高阶组件(HOC)或 Render Props |
| 性能优化 | React.memo + useMemo/useCallback | PureComponent/shouldComponentUpdate |
| 错误边界 | ❌ 不支持(需配合类组件) | ✅ 支持(componentDidCatch) |
| 实例引用 | useRef + useImperativeHandle | 直接通过 ref 获取实例 |
| 心智模型 | 函数式编程(无副作用) | 面向对象编程(实例化) |
状态管理机制
jsx
// 函数组件(Hooks)
const Counter = () => {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c+1)}>{count}</button>;
}
// 类组件
class Counter extends React.Component {
state = { count: 0 };
render() {
return <button onClick={() => this.setState({count: this.state.count+1})>
{this.state.count}
</button>
}
}- 核心差异:函数组件状态是闭包环境中的独立值,类组件状态是实例的
this.state属性
生命周期实现
jsx
// 函数组件(useEffect)
useEffect(() => {
// componentDidMount + componentDidUpdate
fetchData();
return () => { /* componentWillUnmount */ }
}, [dependencies]);
// 类组件
componentDidMount() { fetchData(); }
componentDidUpdate(prevProps) {
if (prevProps.id !== this.props.id) fetchData();
}
componentWillUnmount() { cleanup(); }- 核心差异:函数组件通过声明式依赖数组控制副作用,类组件需要手动比较
prevProps
this 绑定问题
jsx
// 类组件典型问题
class Button extends React.Component {
handleClick() {
// this 可能为 undefined
console.log(this.props);
}
// 解决方案1: 构造函数绑定
constructor() {
this.handleClick = this.handleClick.bind(this);
}
// 解决方案2: 箭头函数
handleClick = () => { ... }
}- 函数组件优势:天然规避
this绑定问题,所有值通过闭包获取
逻辑复用方式
jsx
// 函数组件(自定义 Hook)
const useLogger = (value) => {
useEffect(() => {
console.log("Value changed:", value);
}, [value]);
};
// 类组件(HOC)
const withLogger = (Comp) => {
return class extends React.Component {
componentDidUpdate(prevProps) {
console.log("Props changed:", this.props);
}
render() {
return <Comp {...this.props} />;
}
};
};React 错误边界处理
- 目前还没有办法将错误边界编写为函数式组件。但是你不必自己编写错误边界类。例如,你可以使用 react-error-boundary 包来代替。
js
import React, { Component } from "react";
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error };
}
componentDidCatch(error, info) {
// 这里可以添加错误日志上报逻辑
console.error("Error caught by ErrorBoundary:", error, errorInfo);
}
render() {
if (this.state.hasError) {
// 显示备用 UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
export default ErrorBoundary;
function App() {
return (
<div>
<ErrorBoundary>
<h1>Hello, world!</h1>
</ErrorBoundary>
</div>
);
}React 单向数据流
React 单向数据流是 React 的核心原则之一,它描述了数据的流动方向:从父组件通过 props 流向子组件,子组件不能直接修改 props,只能通过父组件传递的回调函数来请求父组件修改数据。
- 优点:
- 1、可预测性:数据流动方向单一,容易跟踪和调试。
- 2、易于维护:组件的状态变化有明确的来源和改变方式。
- 3、组件隔离:每个组件只关心自己的状态和接收的 props,降低了组件之间的耦合。
- 优点:
受控组件与非受控组件
受控组件与非受控组件的核心区别是:表单数据由谁管理。
- 受控组件:表单数据由 React 的状态管理。
- 非受控组件:表单数据由 DOM 自身,React 通过
ref在需要时读取。
在大多数业务表单中,建议优先使用受控组件。文件上传、接入第三方 DOM 库等需要直接访问 DOM 的场景,可以使用非受控组件。
受控组件
受控组件是指表单元素的当前值由 React 状态决定。
React 将状态通过 value 或 checked 传递给表单元素;用户操作表单时 onChange 更新状态,再由新的状态更新页面。
它的数据流如下:
text
用户输入 → 触发 onChange → 更新 state → React 重新渲染 → 更新表单值tsx
import { useState } from "react";
const Controlled = () => {
const [value, setValue] = useState<string>("hulei");
const getValue = () => {
console.log(value);
};
return (
<>
<div>受控组件</div>
<input
type="text"
value={value}
onChange={(event) => setValue(event.target.value)}
/>
<p>当前值:{value}</p>
<button onClick={getValue}>获取值</button>
</>
);
};受控组件具有以下特点:
- 表单元素的值保存在 React 状态中。
- React 状态是表单数据的唯一数据源。
- 用户输入时,需要通过
onChange更新状态。 - 状态更新后,React 会重新渲组件并更新表单值。
- 便于实现实时校验、输入格式化和表单联动。
常见使用场景:
- 登录、注册等普通业务表单。
- 实时校验用户输入。
- 根据输入内容控制其他组件。
- 根据表单状态决定是否允许提交。
- 对输入内容进行格式化。
非受控组件
非受控组件是指表单元素的当前值由 DOM 自身维护,React 不使用状态控制它的每一次变化。
React 通常通过 ref 获取表单元素,并在提交或其他特定时机读取它的当前值。
它的数据流如下:
text
用户输入 → DOM 保存数据 → React 通过 ref 读取数据tsx
import { useRef } from "react";
const Uncontrolled = () => {
const inputRef = useRef<HTMLInputElement>(null);
const getValue = () => {
console.log(inputRef.current?.value);
};
return (
<>
<div>非受控组件</div>
<input
ref={inputRef}
type="text"
defaultValue="hulei"
/>
<button onClick={getValue}>获取值</button>
</>
);非受控组件具有以下特点:
- 表单元素的值由 DOM 自身维护。
- React 不会同步保存用户的每一次输入。
- 通常使用
ref获取表单元素的当前值。 - 使用
defaultValue或defaultChecked设置初始值。 - 修改
defaultValue不会更新已经挂载的表单元素。
常见使用场景:
- 文件上传。
- 接入需要直接操作 DOM 的第三方库。
- 只需要在提交时获取表单数据。
- 将已有的非 React 表单迁移到 React。
文件上传
在 React 中,文件输入框始终属于非受控组件。
这是因为文件输入框的值只能由用户选择,不能通过 React 状态直接设置。React 需要通过 ref 读取用户选择的文件。
tsx
import { useRef } from "react";
const FileUpload = () => {
const fileRef = useRef<HTMLInputElement>(null);
const upload = () => {
const file = fileRef.current?.files?.[0];
if (file) {
console.log(file);
}
};
return (
<>
<input ref={fileRef} type="file" />
<button onClick={upload}>获取文件</button>
</>
);
};React 时间分片的大概原理
React 时间分片的核心,是把一次耗时的渲染任务拆分成多个小的工作单元,分批执行,避免长时间占用浏览器主线程。 以 60Hz 屏幕为例,每帧大约只有 16.6ms。在这段时间内,浏览器不仅要执行 JavaScript,还要完成样式计算、布局和绘制。如果 JavaScript 长时间占用主线程,浏览器就无法及时渲染和响应用户操作,页面会出现卡顿。
React 在Fiber 架构下把渲染任务拆成多个工作单元,并通过 Scheduler 进行调度。执行过程中,React 会判断是否需要让出主线程;如果有更高优先级的任务,例如用户输入,就可以暂停当前渲染,优先处理紧急任务,之后再继续或重新执行。 需要注意:时间分片不会减少总计算量,只是把连续执行改成分段执行;而且只有 Render 阶段可以中断,操作真实 DOM 的 Commit 阶段仍然是同步且不可中断的。

为什么会出现 fiber
Fiber 出现的主要原因,是解决旧版 React 同步递归渲染无法中断的问题。
React 16 以前采用 Stack Reconciler。组件树一旦开始更新,就会通过递归持续执行,直到整棵树计算完成。如果组件树很大,JavaScript 会长时间占用主线程,浏览器无法及时处理用户输入、动画和页面绘制,从而出现卡顿。
React 16 引入 Fiber 架构,将每个节点的渲染工作拆成独立的 Fiber 工作单元,并通过 child、sibling、return 等指针组成可遍历的数据结构。React 因此可以用循环逐个处理工作单元,并保存执行进度,使 Render 阶段能够暂停、恢复、放弃或重新执行。
结合 Scheduler,React 还可以根据任务优先级进行调度:高优先级任务可以优先执行,低优先级渲染可以延后,从而提高页面响应性。
一句话总结:Fiber 将不可中断的递归渲染改造成可调度的工作单元,为时间分片、优先级调度和并发渲染提供了基础。
fiber 数据结构
- fiber 是一个链表的数据结构,他的 child 指向第一个子节点,sibling 指向最近弟弟,return 指向父 fiber,即使是执行被打断,也很容易恢复

优先级
scheduler 优先级
- scheduler 优先级有 5 种,每一个优先级对应一个
过期时间,过期时间越短,对应优先级越高
js
// 无优先级
export const NoPriority = 0;
// 立即执行(如点击按钮触发的同步操作)
export const ImmediatePriority = 1;
//阻塞用户的操作(如输入框输入、拖拽) ,用户输入(等待时间250ms)
export const UserBlockingPriority = 2;
// 普通操作(如网络请求完成后的回调)(等待时间5000ms)
export const NormalPriority = 3;
// 低优先级(如非紧急的后台计算)(等待时间10000ms)
export const LowPriority = 4;
// 空闲时执行(如日志上报、非关键的统计)(没有过期时间)
export const IdlePriority = 5;Lane
- Lane 一共有 31 条车道,对应 5 种事件优先级
js
/**
* 一共有31条车道
*/
const TotalLanes = 31;
//没有车道,所有位都为0
const NoLanes = 0b0000000000000000000000000000000;
const NoLane = 0b0000000000000000000000000000000;
//同步车道,优先级最高
const SyncLane = 0b0000000000000000000000000000001;
const SyncBatchedLane = 0b0000000000000000000000000000010;
//离散用户交互车道 click
const InputDiscreteHydrationLane = 0b0000000000000000000000000000100;
const InputDiscreteLanes = 0b0000000000000000000000000011000;
//连续交互车道 mouseMove
const InputContinuousHydrationLane = 0b0000000000000000000000000100000;
const InputContinuousLanes = 0b0000000000000000000000011000000;
//默认车道
const DefaultHydrationLane = 0b0000000000000000000000100000000;
const DefaultLanes = 0b0000000000000000000111000000000;
//渐变车道
const TransitionHydrationLane = 0b0000000000000000001000000000000;
const TransitionLanes = 0b0000000001111111110000000000000;
//重试车道
const RetryLanes = 0b0000011110000000000000000000000;
const SomeRetryLane = 0b0000010000000000000000000000000;
//选择性水合车道
const SelectiveHydrationLane = 0b0000100000000000000000000000000;
//非空闲车道
const NonIdleLanes = 0b0000111111111111111111111111111;
const IdleHydrationLane = 0b0001000000000000000000000000000;
//空闲车道
const IdleLanes = 0b0110000000000000000000000000000;
//离屏车道
const OffscreenLane = 0b1000000000000000000000000000000;事件优先级
js
//离散事件优先级 click onchange
export const DiscreteEventPriority = SyncLane; //1
//连续事件的优先级 mousemove
export const ContinuousEventPriority = InputContinuousLane; //4
//默认事件车道
export const DefaultEventPriority = DefaultLane; //16
//空闲事件优先级
export const IdleEventPriority = IdleLane;React 18 渲染流程核心思想
React 18 渲染流程的核心是基于 Fiber 架构,将一次更新拆分为 Render(协调)和 Commit(提交)两个阶段。
在并发渲染模式下,React 可以根据更新优先级调度 Render 工作,在浏览器需要处理更高优先级任务时暂停、恢复或放弃当前渲染结果。Commit 阶段则始终同步,以避免用户看到不完整的界面。
一句话速记:调度更新(Lane 定优先级)→ Render(beginWork 向下、completeWork 向上,可打断)→ Commit(Before Mutation → Mutation → Layout,同步不可中断)→ Passive Effects(useEffect,绘制后异步执行)。
一、调度更新
状态更新产生后,React 会执行以下工作:
- 创建 Update,并通过 Lane 记录更新优先级;
- 将更新加入对应 Fiber 的更新队列;
- 从当前节点向根节点标记待处理的 Lane;
- 根据根节点中待处理任务的优先级安排同步或并发渲染。
高优先级更新可以打断低优先级的并发渲染。被打断的工作可能稍后继续,也可能由于数据发生变化而被放弃并重新计算。
二、Render 阶段:计算下一棵 Fiber 树
Render 阶段的目标是基于当前 Fiber 树和最新状态,构建 workInProgress Fiber 树,并记录需要在 Commit 阶段执行的副作用。
双缓存(Double Buffering)机制
React 在内存中同时维护两棵 Fiber 树,通过
alternate指针互相引用:
- current 树:当前已渲染到屏幕上的树,root.current 指向它;
- workInProgress 树:正在内存中构建的下一棵树。
Render 阶段所有的计算、diff、打 flag 都发生在 workInProgress 树上,不影响屏幕上的 current 树。构建时会尽量复用 current 树对应节点的
alternate,避免重复创建对象。当 workInProgress 树构建完成并在 Commit 阶段应用后,React 只需一步将
root.current指向它,即完成「树的切换」——旧的 current 树则作为下一次更新的 workInProgress 缓存复用。这种「两棵树来回切换」的方式就是双缓存,好处是在内存中完成构建、一次性切换,避免用户看到中间的不完整状态。
该阶段主要包含两个过程:
1. beginWork:递阶段,向下遍历
React 从根节点开始深度优先遍历,负责构建 Fiber、生成子 Fiber,并执行 diff:
- 执行函数组件或类组件的 render,得到新的 React Element;
- 将新 Element 与旧 Fiber 进行 diff,根据节点类型和 key 判断复用、新建或删除;
- 给 Fiber 打上 Placement(新增 / 移动)、Deletion(删除)这类节点结构相关的 flags;
- 生成子 Fiber,继续向下处理。
注意:beginWork 只关心节点结构,不处理原生节点自身的属性(属性变更留到 completeWork)。
JSX 通常已在编译阶段转换为创建 React Element 的代码,并不是在 Render 阶段临时解析。
2. completeWork:归阶段,向上归并
当一个节点的子节点全部处理完后,React 回头「完成」这个 Fiber:
- 碰到原生的 HostComponent / HostText 节点时,对比新旧 props、文本;
- 若属性或文本发生变化 → 给 Fiber 打上 Update flag;
- 执行 bubbleProperties,把子树所有 flags 聚合,向上冒泡更新父节点的 subtreeFlags;
- 逐层返回,直到完成整棵 workInProgress 树。
Flags 表示当前 Fiber 自身需要执行的副作用,subtreeFlags 表示其子树是否包含副作用,Commit 阶段可以据此跳过没有副作用的子树。
Render 阶段只进行计算,不应该产生可观察的外部副作用。并发渲染可能暂停、重新执行或放弃尚未提交的结果,因此组件渲染逻辑必须保持纯粹。
三、Commit 阶段:提交更新
当 workInProgress 树构建后,React 进入 Commit 阶段。该阶段同步执行且不可中断,以保证界面不会处于部分更新状态。
Commit 阶段主要包含以下三个子阶段。
1. Before Mutation
在修改 DOM 之前执行:
- 调用类组件的 getSnapshotBeforeUpdate;
- 读取 DOM 更新前的状态;
- 处理部分与焦点、选区相关的准备工作。
该阶段适合读取修改前的 DOM 信息,但不执行主要的 DOM 变更。
2. Mutation
执行实际的宿主环境变更:
- 插入、更新和删除 DOM;
- 处理 ref 的解绑;
- 执行 useInsertionEffect;
- 执行上一轮 useLayoutEffect 的清理函数。
Mutation 完成后,新的 DOM 结构已经生成,但浏览器通常还没有机会绘制。
随后 React 将根节点的 current 指向 finishedWork,使刚完成的 workInProgress 树成为新的 current 树。该切换发生在 Mutation 之后、Layout Effects 之前,因此 Layout 阶段可以访问最新 Fiber 树和 DOM。
3. Layout
DOM 更新完成后 React 同步执行:
- 设置新的 ref;
- 执行 useLayoutEffect 的创建函数;
- 调用 componentDidMount;
- 调用 componentDidUpdate;
- 执行部分同步回调。
useLayoutEffect 会在浏览器获得绘制机会前执行。如果其中包含耗时操作,将直接延迟页面绘制。
Commit 阶段结束后,主线程才有机会进行样式计算、布局和绘制。React 本身不会直接调用浏览器的重绘流程。
四、Passive Effects
useEffect 属于 Passive Effects,不在主要 DOM Mutation 和 Layout Effects 中执行。
通常情况下,React 会在提交完成后调度 Passive Effects:
- 执行上一次 useEffect 的清理函数;
- 执行本次 useEffect 的创建函数。
Passive Effects 通常不会阻塞当前提交的页面绘制,适合处理数据请求、事件订阅、日志上报等不需要在绘制前完成的副作用。
但“useEffect 一定在浏览器绘制后执行”并不是严格保证。在离散交互、同步刷新或后续更新触发提前清理时,React 可能在绘制前处理 Passive Effects。因此,不应使用 useEffect 执行必须严格等待绘制完成的逻辑。
React 的 Diff 原理
React 的 Diff 算法主要分为以下几部分:
1、同层比较:仅比较同一层级的节点,不跨层级移动
2、复用优先:当 key 和 type 相同时复用老 Fiber 节点,否则创建新的 Fiber 节点
3、多场景优化:针对单节点和多节点不同处理策略

一句话速记:只比同层,key + type 相同就复用;单节点找到即复用、其余兄弟标删;多节点走「两轮遍历 + lastPlacedIndex 判断是否右移」。
一、单节点 Diff 流程

核心思想:新节点是「单个」,所以只要在老 Fiber 兄弟链中找到一个 key + type 都相同的即可复用,其余兄弟全部标删;找不到就新建。
- 1、循环老的 Fiber 兄弟节点,如果某个老 Fiber 的
key和类型(type)都与新虚拟节点一致,复用该老 Fiber,并把它剩下的所有兄弟节点标记为删除 - 2、如果当前老 Fiber 的
key与新节点不同,说明肯定不是同一个,把当前这个老 Fiber 标记删除,继续检查下一个兄弟 - 3、特殊情况:如果
key相同但type不同,因为 key 唯一,后面不可能再匹配,此时直接把当前及剩余所有兄弟全部标删并跳出循环 - 4、如果兄弟节点全部循环完毕仍未找到可复用的 Fiber,则根据新的虚拟 DOM 创建新的 Fiber 节点
记忆要点:单节点比较先比 key,key 相同再比 type。key 不同 → 删当前、看下一个;key 相同 type 不同 → 直接放弃剩余全部。
js
function reconcileSingleElement(returnFiber, currentFirstChild, element) {
let child = currentFirstChild;
while (child !== null) {
// 1. key 相同才有复用可能
if (child.key === element.key) {
if (child.type === element.type) {
// 1.1 key + type 都相同:复用,并删除它剩下的所有兄弟
let nextChild = child.sibling;
while (nextChild) {
deleteChild(returnFiber, nextChild);
nextChild = nextChild.sibling;
}
const existing = useFiber(child, element.props);
existing.return = returnFiber;
return existing;
}
// 1.2 key 相同但 type 不同:后面不可能再匹配,删掉当前及所有剩余兄弟并跳出
deleteRemainingChildren(returnFiber, child);
break;
}
// 2. key 不同:删除当前节点,继续找下一个兄弟
deleteChild(returnFiber, child);
child = child.sibling;
}
// 3. 未找到可复用节点:创建新节点
const created = createFiberFromElement(element);
created.return = returnFiber;
return created;
}二、多节点 Diff 流程(多节点分为三种情况)

核心思路:两轮遍历 React 认为「同位置更新」比「移动」更常见,所以先用第一轮遍历处理顺序不变的节点(逐个按位置对比,key/type 相同就复用,一旦不匹配立即跳出),再用第二轮遍历借助 map 处理剩下乱序的节点。
第一轮遍历结束后,会出现三种情况:
- 1、老的 Fiber 全部能被复用,新的节点还没有遍历完成(新的节点比老的多)
- 2、老的 Fiber 复用了部分,新的节点已经遍历完了(新的节点比老的少)
- 3、老的 Fiber 和新的虚拟 DOM 都没有循环完毕(根据老的 Fiber 生成 map 对象,遍历还未完成的虚拟 DOM,最大程度的复用)

- 第一轮比较 A 和 A,相同可以复用,更新,然后比较 B 和 C,key 不同直接跳出第一个循环
- 把剩下 oldFiber 的放入 existingChildren 这个 map 中
- 然后声明一个 lastPlacedIndex 变量,表示不需要移动的老节点的索引,默认为 0
- 继续循环剩下的虚拟 DOM 节点,从 C 开始
- 如果能在 map 中找到相同 key 相同 type 的节点则可以复用老 fiber,并把此老 fiber 从 map 中删除
- 如果能在 map 中找不到相同 key 相同 type 的节点则创建新的 fiber 节点
- 如果是复用老的 fiber,则判断老 fiber 的索引是否小于 lastPlacedIndex
- 如果小于 lastPlacedIndex 则需要移动老 fiber,lastPlacedIndex 不变
- 如果大于 lastPlacedIndex 则不需要移动老 fiber,更新 lastPlacedIndex 为老 fiber 的 index
- 虚拟 DOM 循环结束后把 map 中所有的剩下的 fiber 全部标记为删除
lastPlacedIndex 一句话记忆:它记录「已确定不用动的老节点的最大 index」。新序列从左往右扫,复用的老节点 index 比它大 → 相对位置没变(不动,更新 lastPlacedIndex);比它小 → 该节点在老树里靠前、却要排到后面,必须右移(打 Placement)。这也是 React「节点尽量往右移、避免整体前移」策略的由来。
为什么 key 很重要 / 别用 index 当 key 上面所有复用判断都依赖 key 定位老 fiber。若用数组 index 当 key,一旦列表发生插入、删除或排序,同一个 index 会对应到不同的数据,导致 React「错误复用」——状态错位、输入框内容串行、无谓的重新渲染。稳定唯一的 key 是 Diff 高效且正确复用的前提。
循环链表
实现思路:
- 1、当 fiber 更新队列为空时,让新的队列
queue1.next指向头节点,让头节点指向queue1 - 2、当
queue2进入队列时,让queue2.next指向头节点的next(queue1),让queue1.next指向queue2(形成循环链表结构),让头节点指向 queue2 - 3、当
queue3进入队列时,让queue3.next指向头节点(queue2)的next(queue1),让头节点(queue2.next)指向queue3,让头节点指向 queue3
- 1、当 fiber 更新队列为空时,让新的队列
目的:即可以保证链表的顺序也可以减少放入队列的循环次数,在更新状态的时候,需要从第一个进入的队列开始遍历,这里在添加队列的时候只需要改变 2 个指针,在遍历的时候找最后一个队列的下一个就是头节点,并且让最后一个 next 指向 null

合成事件
合成事件(SyntheticEvent)是 React 对原生 DOM 事件的一层跨浏览器封装。设计目的主要有四点:
- 1、跨浏览器一致性:统一不同浏览器的事件处理接口
- 2、性能优化:通过事件委托减少内存消耗
- 3、便捷开发:提供更符合 React 理念的事件处理方式
- 4、安全控制:防止事件滥用导致的 XSS 攻击
一句话速记:所有事件委托到根容器(16 是 document、17+ 是 #root)→ 触发时按 target 向上收集路径上的处理函数 → 包装成 SyntheticEvent → 依次模拟捕获、冒泡执行。
合成事件的实现原理
1. 事件注册
- React 采用事件委托:并不给每个 DOM 节点单独绑事件,而是把事件统一代理到一个根容器上。
- **注意委托目标的变化 **:React 16 及以前统一委托到
document;React 17 起改为委托到ReactDOM.render的根容器节点(#root)。这样做便于多个 React 版本共存、也更适配微前端等嵌套场景。 - React 并非一开始就注册所有事件,而是惰性注册:当组件中第一次用到某个事件(如
onClick)时,才在根容器上注册对应的原生事件类型。
2. 事件存储
- React 会维护一个映射,将事件类型与组件的事件处理函数关联起来。这个映射存储在组件的 fiber 节点上(具体是在 fiber 的
memoizedProps和pendingProps中)。
3. 事件触发
当事件在 DOM 上触发时(如用户点击),事件会冒泡到 root 节点。
- root 节点上绑定的事件监听器(由 React 注册)会被触发。
- React 通过事件对象(原生事件)的
event.target找到实际触发事件的 DOM 节点。 - 然后,React 从这个 DOM 节点向上遍历,收集所有与事件类型相关的处理函数(例如,在
onClick事件中,收集所有节点的onClick处理函数)。这个遍历过程会考虑事件是否被阻止冒泡(stopPropagation)。 - 接着,React 构造一个合成事件对象(
SyntheticEvent),并依次调用收集到的事件处理函数
4. 事件对象池(React 17 起已移除)
- 旧版本(≤ React 16):为减少 GC,React 使用事件池复用合成事件对象,回调执行完后会把其属性置空。因此想在异步(如
setTimeout、Promise)里访问事件属性,必须先调用event.persist()保留它。 - 新版本(≥ React 17):事件池已被移除,合成事件对象不再被复用、属性也不会被清空,可以直接在异步中访问。
event.persist()仍保留但已成为空操作(no-op),无需再调用。
记忆点:「为什么 setTimeout 里拿不到 event」——那是 React 16 及以前的事件池行为;17 之后已不存在这个坑。
合成事件与原生事件的区别:
- 合成事件的命名采用小驼峰(camelCase),而不是纯小写。
- 在合成事件中,返回
false不会阻止默认行为,必须显式调用preventDefault。 - 由于事件委托,在组件卸载后,React 会自动移除事件处理,避免内存泄漏
js
// 支持的事件类型
const allNativeEvents = ["click"];
const elementEventPropsKey = "__props";
const listeningMarker = `_reactListening` + Math.random().toString(36).slice(2);
// 事件映射
function getEventCallbackNameFromtEventType(eventType) {
return {
click: ["onClickCapture", "onClick"],
}[eventType];
}
// 收集从目标元素到HostRoot之间所有目标回调函数
const collectPaths = (targetElement, container, eventType) => {
const paths = {
capture: [],
bubble: [],
};
// 收集事件回调是冒泡的顺序
while (targetElement && targetElement !== container) {
// div.__props = {onClickCapture:fn,onClick:fn....}
const eventProps = targetElement[elementEventPropsKey];
if (eventProps) {
const callbackNameList = getEventCallbackNameFromtEventType(eventType);
// 取出映射的事件
if (callbackNameList) {
callbackNameList.forEach((callbackName, i) => {
// react事件
const eventCallback = eventProps[callbackName];
if (eventCallback) {
if (i === 0) {
// 由于捕获的执行顺序是从上往下,所以是反向收集
// 反向插入捕获阶段的事件回调
paths.capture.unshift(eventCallback);
} else {
// 由于冒泡的执行顺序是从下往上,所以是正向收集
// 正向插入冒泡阶段的事件回调
paths.bubble.push(eventCallback);
}
}
});
}
}
targetElement = targetElement.parentNode;
}
return paths;
};
const dispatchEvent = (rootContainerElement, eventType, e) => {
const targetElement = e.target; // 事件源
if (targetElement === null) {
console.error("事件不存在target", e);
return;
}
// 从当前的事件源向上查找所有的,扑捉、冒泡事件,用于模拟原生的事件
const { capture, bubble } = collectPaths(
targetElement,
rootContainerElement,
eventType
);
// 合成原生事件
const se = createSyntheticEvent(e);
triggerEventFlow(capture, se);
// 如果事件被阻止不需要在执行
if (!se.__stopPropagation) {
triggerEventFlow(bubble, se);
}
};
export function listenToAllSupportedEvents(rootContainerElement) {
//监听根容器,也就是div#root只监听一次
if (!rootContainerElement[listeningMarker]) {
rootContainerElement[listeningMarker] = true;
allNativeEvents.forEach((eventType) => {
// 注册代理事件
rootContainerElement.addEventListener(eventType, (e) => {
dispatchEvent(rootContainerElement, eventType, e);
});
});
}
}
function createSyntheticEvent(e) {
const syntheticEvent = e;
syntheticEvent.__stopPropagation = false;
// 阻止事件传递
const originStopPropagation = e.stopPropagation;
syntheticEvent.stopPropagation = () => {
syntheticEvent.__stopPropagation = true;
if (originStopPropagation) {
originStopPropagation();
}
};
return syntheticEvent;
}
// 执行react事件
const triggerEventFlow = (paths, se) => {
for (let i = 0; i < paths.length; i++) {
const callback = paths[i];
callback.call(null, se);
if (se.__stopPropagation) {
break;
}
}
};
// 将支持的事件回调保存在DOM中
export const updateEventProps = (node, props) => {
node[elementEventPropsKey] = node[elementEventPropsKey] || {};
allNativeEvents.forEach((eventType) => {
// 获取支持的事件类型
const callbackNameList = getEventCallbackNameFromtEventType(eventType);
if (!callbackNameList) {
return;
}
// 事件映射
callbackNameList.forEach((callbackName) => {
if (Object.hasOwnProperty.call(props, callbackName)) {
node[elementEventPropsKey][callbackName] = props[callbackName];
}
});
});
return node;
};react16 版本事件 bug
比如需要一个事件来控制一个元素的展示,在当前组件注册一个原生的 document 冒泡事件,在处理合成事件让元素展示的时候,如果原生事件处理的是隐藏,其结果就是元素无法展示
出现的原因是 react 冒泡事件先注册,注册在 document,所以 react 冒泡事件先执行,后执行原生的 document 冒泡事件,所以无法展示,通过阻止默认行为(冒泡),无效,因为是同级节点,只能阻止上级
js
// react16版本
function App() {
let [visibility, setVisibility] = useState(false);
// 注册冒泡事件
document.addEventListener("click", () => {
setVisibility(false);
});
let handlerClick = (e: any) => {
setVisibility(true);
// 阻止默认行为(冒泡),只能阻止上级,不能阻止同级
e.stopPropagation();
// 阻止所有
e.nativeEvent.stopImmediatePropagation();
};
return (
<div onClick={handlerClick}>
{visibility && "visibility"}
点击
</div>
);
}context 用作原理
用作:数据共享,可以避免 props 一层成传递,可以实现跨组件数据共享
原理
- 1、在创建 createContext 时,会返回一个 context 对象,包含内容提供者(Provider)和消费者(Consumer),他们都有一个_context 属性,都指向同一个引用地址
- 2、react 在渲染时,如果是 Provider 组件,会给组件
_context._currentValue进行赋值,在渲染当前的 children 组件 - 3、react 在渲染时,如果是 Consumer 组件,会取出
_context._currentValue的值,传递给子组件(因为指向同一个引用地址,所以值是一样的) - 4、如果是类组件,有 contextType 属性,会对类上的 context 进行赋值
js
// 渲染provider
function mountProviderComponent(vdom) {
let { type, props } = vdom; // {$$typeof: REACT_PROVIDER,_context: context,};
// 引用赋值
type._context._currentValue = props.value;
// 用于下次新老做对比(渲染的其实是子元素)
vdom.oldRenderVdom = props.children;
if (!vdom.oldRenderVdom) return null;
return createDOM(props.children);
}js
// 处理函数类件
function mountClassComponent(vdom) {
let { type: ClassComponent, props, ref } = vdom;
let classInstance = new ClassComponent(props);
vdom.classInstance = classInstance;
// 如果有contextType,把context绑定在实例上
if (ClassComponent.contextType) {
classInstance.context = ClassComponent.contextType._currentValue;
}
// .....
return dom;
}js
function createContext() {
let context = { $$typeof: REACT_CONTEXT };
// 提供者
context.Provider = {
$$typeof: REACT_PROVIDER,
_context: context,
};
// 消费者
context.Consumer = {
$$typeof: REACT_CONTEXT,
_context: context,
};
return context;
}
this.state = {
color: "red",
};
const Context = React.createContext();
<Context.Provider value={{red: this.state.color}}>
<div>{this.state.color}</div>
</Context.Provider>
<Context.Consumer>
{(props) => {
return (
<div>
<span style={{ color: props.red }}>main</span>
</div>
);
}}
</Context.Consumer>React 优化方案( shouldComponentUpdate,PureComponent,memo,useMemo,useCallback)
React 的性能优化核心是减少不必要的重新渲染和重复计算,手段分为两类:
- 拦截渲染:在组件更新前判断 props/state 是否变化,未变化则跳过(
shouldComponentUpdate/PureComponent/memo) - 缓存结果:把「值」或「函数」缓存下来,依赖不变就复用旧引用(
useMemo/useCallback)
一句话速记:类组件用
shouldComponentUpdate/PureComponent拦截更新;函数组件用memo拦截更新,再用useMemo缓存值、useCallback缓存函数(memo 需与 useMemo/useCallback 配套,才能保证传下去的 props 引用稳定)。
类组件
- shouldComponentUpdate(nextProps, nextState)
- 组件更新前触发的生命周期,接收即将生效的新 props、新 state,可与当前的
this.props/this.state对比。 - 返回
true执行更新,返回false跳过本次渲染(默认返回true)。
- 组件更新前触发的生命周期,接收即将生效的新 props、新 state,可与当前的
- PureComponent
- 内部自动实现了
shouldComponentUpdate,对 props 和 state 做浅比较(第一层引用比较)。 - 陷阱:若传入的是新创建的对象 / 数组 / 函数(引用每次都变),浅比较必然失败,优化会失效。
- 内部自动实现了
函数组件
- memo
- 高阶组件,包裹函数组件后,更新前对比新旧 props,未变化则跳过重新渲染。
- 未提供自定义对比函数时,走 memo 默认的
arePropsEqual,对 props 做浅层对比:父组件重渲染但传入相同的 props 引用时,子组件不会重渲染。 - 陷阱:父组件每次渲染都会重新创建内联对象 / 函数(引用变化),会让 memo 失效——因此常需配合
useMemo/useCallback稳定这些引用。
- useMemo:缓存计算结果的值,依赖不变则跳过重新计算(适合昂贵计算)。
- useCallback:缓存函数引用,依赖不变则复用同一个函数(常配合 memo 子组件,避免子组件因函数引用变化而重渲染)。
React.memo 默认比较策略
React.memo 默认的比较函数就是 shallowEqual(浅比较),对新旧 props 对象做逐键对比:
- 先判断新旧 props 是否为同一引用,相同直接判定相等 → 跳过更新;
- 引用不同时,再比较 key 的数量,数量不同 → 更新;
- 数量相同则遍历第一层每个 key,用
===比较对应的 value(即比较引用/基本值):- 所有 key 的 value 都相等 → 判定相等,跳过更新;
- 只要有一个不等 → 更新组件。
一句话速记:memo 只比 props 的第一层。第一层是基本类型时比值,是对象/数组/函数时比引用地址——所以深层数据变化(如
{b:{}}→{b:{c:10}}但b引用没变)不会触发更新;反之每次传新对象(引用变)必触发更新。
js
function shallowEqual(objA, objB) {
// 1. 相同引用检查 不更新
if (objA === objB) return true;
// 2. 如果有一个是 null,更新
if (
typeof objA !== "object" ||
objA === null ||
typeof objB !== "object" ||
objB === null
) {
return false;
}
// 3. 键数量检查
const keysA = Object.keys(objA);
const keysB = Object.keys(objB);
if (keysA.length !== keysB.length) return false;
// 4. 键值对检查(仅第一层)
for (let i = 0; i < keysA.length; i++) {
const key = keysA[i];
// 检查键是否存在
if (!Object.prototype.hasOwnProperty.call(objB, key)) {
return false;
}
// {a:10,b:{}}
// {a:10,b:{c:10}} // 不会触发更新,因为浅比较(第一层),比较的是引用地址
if (objA[key] !== objB[key]) {
return false;
}
}
return true;
}setState
核心概念
setState 是 React 中用于更新组件状态的方法,具有批量更新和异步执行的特性。
- 在 React 事件(合成事件、生命周期)中调用 setState,会走批量更新:同一次事件里的多个 setState 会被合并,只触发一次重新渲染。
- React 17 及以前:只有 React 事件里才批处理;在原生事件、
setTimeout、Promise等异步任务中调用 setState 不批处理,相当于每次都同步更新并立即重渲染。 - React 18 起:引入自动批处理(Automatic Batching),无论在合成事件、原生事件还是异步任务中,多个 setState 都会自动合并;如需强制同步更新,可用
flushSync包裹。
一句话速记:批处理 = 合并多次 setState、只渲染一次。React 17 只在合成事件里批处理,异步/原生事件里「同步生效」;React 18 全场景自动批处理,想跳出批处理用
flushSync。
注意:setState 的「异步」指的是不能同步拿到更新后的 this.state(批处理期间状态还没提交),并非真正的异步 API。想拿更新后的值,用第二个参数的回调,或改用函数式更新
setState(prev => ...)。
js
// 初始状态:count = 0
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
// 最终结果:count = 1 (不是3!)js
// 使用函数形式确保基于最新状态
this.setState((prevState) => ({ count: prevState.count + 1 }));
this.setState((prevState) => ({ count: prevState.count + 1 }));
this.setState((prevState) => ({ count: prevState.count + 1 }));
// 最终结果:count = 3 ✅js
handleClick = () => {
this.setState({ count: 42 });
console.log(this.state.count); // 输出旧值,不是42!
// 使用回调获取更新后状态
this.setState({ count: 43 }, () => {
console.log(this.state.count); // 输出43 ✅
});
};为什么会出现 hooks
核心概念
Hooks 是 React 16.8 引入的特性,使得函数组件可以拥有类组件的特性,如状态管理、生命周期方法等。
Hooks 的出现是为了解决类组件在逻辑复用、代码组织和心智负担上的痛点,让函数组件也能拥有状态和生命周期能力。
一句话速记:Hooks 让函数组件也能用 state 和生命周期,主要解决类组件的三大痛点——逻辑复用难(HOC/mixin 嵌套)、逻辑分散(同一逻辑被拆到不同生命周期)、this 指向易错。
- 让函数组件拥有状态与副作用能力:通过
useState、useEffect等,函数组件也能管理状态、处理生命周期,更灵活强大。 - 解决逻辑复用难题:用自定义 Hook 复用状态逻辑,替代类组件中层层嵌套的高阶组件(HOC)和已废弃的 mixin,避免「嵌套地狱」。
- 让相关逻辑聚合:类组件里同一功能的逻辑常被拆散在
componentDidMount/componentDidUpdate/componentWillUnmount中;Hooks 可按关注点把相关逻辑写在一起,更易读易维护。 - 规避 this 问题:函数组件没有
this,天然避免类组件中this绑定错误、事件回调需bind等繁琐问题,也更易于测试。
hooks 原理
核心概念
Hooks 原理基于链表结构和 dispatcher 机制,通过维护 hooks 链表和更新队列实现状态管理和生命周期功能。
TIP
类组件中的 setState 是状态合并策略 hooks 队列中有多个时,状态不会进行合并,是直接覆盖更新,后者覆盖前者
一句话速记:函数组件的所有 hook 按调用顺序串成一条链表挂在 fiber 上;渲染时通过全局 dispatcher(Mount / Update 两套)分发;每个 hook 自带更新队列,调用 setState 就是往队列里塞数据并触发重渲染;重渲染时按链表顺序依次读取——所以 hook 必须顶层、顺序稳定调用。
- 1、React 构建 Fiber 时,若是函数组件,会先调用
renderWithHooks做初始化准备。 - 2、根据渲染阶段设置全局 dispatcher(
ReactCurrentDispatcher):首次渲染(Mount)用HooksDispatcherOnMount,更新(Update)用HooksDispatcherOnUpdate。dispatcher 就是组件里实际调用的那套 hook 实现(如useState、useEffect…)。 - 3、执行函数组件时,若是初次渲染,会按 hook 的调用顺序依次创建 hook 对象,串成一条 hooks 链表。
- 4、每个 hook 都持有一个更新队列和一个
dispatchSetState函数(该函数已预先绑定好当前 fiber 和自身 hook 队列),并通过next指向下一个 hook。 - 5、调用 setState 本质是执行
dispatchSetState:把新的 state 放入该 hook 的更新队列,并触发组件重新渲染。 - 6、重新渲染时,React 依据 hooks 链表按原顺序依次执行,从队列中取出并计算最新状态,从而复原各 hook 的值。
hooks 注意事项
核心概念
Hooks 必须在函数组件的顶层调用,不能在条件、循环或嵌套函数中使用,否则会导致 hook 链表错乱。
- 因为函数组件所有的 hook,以链表的形式存储的 fiber 中,当函数组件在更新的时候,会按照链表 next 一个个进行查找
- 如果在条件中使用 hooks、或者在循环中使用 hooks,会导致无法获取到正确的 hook 链表,从而出现 bug
useMemo 和 useCallback 区别
核心概念
useMemo 和 useCallback 都是 React 中用于性能优化的 Hooks,用于缓存值和函数,减少不必要的计算和渲染。
一句话速记:两者都靠依赖数组做缓存——依赖没变就复用上次结果,变了才更新。
useMemo缓存值(factory 的返回值),useCallback缓存函数本身。等价关系:useCallback(fn, deps)===useMemo(() => fn, deps)。
为什么需要它们:函数组件里任何一个 state 变化都会导致整个组件重新执行,函数、计算结果每次都被重新创建。把不必变的「值」或「函数」缓存起来,可避免重复计算、并保持引用稳定。
注意:
useCallback通常要和React.memo配套使用——用 useCallback 稳定住传给子组件的函数引用,memo 才能靠浅比较跳过子组件渲染;只用其中一个往往无效,甚至因额外缓存开销导致性能不升反降。
相同点
- 1、都是 React 提供的性能优化 Hook,用于缓存、减少不必要的计算与渲染;
- 2、参数一致:第一个是回调函数,第二个是依赖数组
deps; - 3、只有依赖发生变化时才更新缓存,否则复用上一次的结果。
区别
- 1、
useMemo缓存的是回调函数 return 出来的值(会执行回调拿返回值),适合缓存昂贵的计算结果; - 2、
useCallback缓存的是回调函数本身(不执行它,只是返回同一个函数引用),适合把稳定的函数传给子组件或作为其他 Hook 的依赖。
- 1、
js
function useMemo(factory, deps) {
let hook =
workInProgressFiber.alternate &&
workInProgressFiber.alternate.hooks &&
workInProgressFiber.alternate.hooks[hookIndex];
let currentMemo = null;
if (hook) {
let [lastMemo, lastDeps] = hook;
// 检测依赖的属性是否发生变化
let same = deps.every((item, index) => item === lastDeps[index]);
if (same) {
// 依赖的属性未发生变化返回之前的值
currentMemo = lastMemo;
} else {
// 返回新的值
currentMemo = factory();
}
} else {
// 创建新的
currentMemo = factory();
}
workInProgressFiber.hooks[hookIndex++] = [currentMemo, deps];
return currentMemo;
}js
function useCallback(callback, deps) {
let hook =
workInProgressFiber.alternate &&
workInProgressFiber.alternate.hooks &&
workInProgressFiber.alternate.hooks[hookIndex];
let currentCallback = null;
if (hook) {
let [lastCallback, lastDeps] = hook;
let same = deps.every((item, index) => item === lastDeps[index]);
if (same) {
currentCallback = lastCallback;
} else {
currentCallback = callback;
}
} else {
currentCallback = callback;
}
workInProgressFiber.hooks[hookIndex++] = [currentCallback, deps];
return currentCallback;
}useEffect 和 useLayoutEffect
一句话速记:两者 API 用法一致,只是执行时机不同。
useEffect在浏览器绘制后异步执行,不阻塞渲染;useLayoutEffect在 DOM 变更后、浏览器绘制前同步执行,会阻塞绘制。默认优先用useEffect;只有当「读取/修改 DOM 布局又不能让用户看到闪烁」时才用useLayoutEffect。时机顺序:DOM 变更 → useLayoutEffect(同步)→ 浏览器绘制 → useEffect(异步)。
1. useEffect(异步执行)
- 触发时机:在浏览器完成 绘制(Paint)后异步执行
- 使用场景:
- 数据获取(API 请求)
- 事件监听/订阅
- 非紧急的 DOM 操作
- 第三方库集成
- 性能分析/埋点
- 特点:
- 不会阻塞浏览器渲染
- 用户可能先看到变化前的 UI,再看到更新后的 UI(可能产生闪烁)
jsx
// 典型用例:数据获取
useEffect(() => {
fetchData().then((data) => setData(data));
}, []);2. useLayoutEffect(同步执行)
- 触发时机:在 DOM 更新后、浏览器 绘制(Paint)前同步执行
- 使用场景:
- 读取/修改 DOM 布局(如元素尺寸、位置)
- 同步更新 UI 避免视觉闪烁
- 需要基于 DOM 计算的动画
- 特点:
- 阻塞浏览器绘制
- 保证用户看到的是最终 UI(避免中间状态)
jsx
// 典型用例:测量 DOM 元素
useLayoutEffect(() => {
const { width } = divRef.current.getBoundingClientRect();
setWidth(width); // 在绘制前更新尺寸
}, []);错误使用示例
jsx
// ❌ 错误:在 useEffect 中同步修改布局
// useEffect 在绘制后执行,用户会先看到旧布局、再看到新布局,产生闪烁
useEffect(() => {
buttonRef.current.style.padding = "10px 20px";
}, []);
// ✅ 正确:改用 useLayoutEffect,在绘制前同步改好布局,用户直接看到最终结果
useLayoutEffect(() => {
buttonRef.current.style.padding = "10px 20px";
}, []);js
function useEffect(callback, deps) {
let hook =
workInProgressFiber.alternate &&
workInProgressFiber.alternate.hooks &&
workInProgressFiber.alternate.hooks[hookIndex];
if (hook) {
let [lastCallback, lastDeps] = hook;
let same = deps && deps.every((item, index) => item === lastDeps[index]);
// 如果关联的deps未发生变化不做任何操作
if (same) {
workInProgressFiber.hooks[hookIndex++] = [lastCallback, deps];
} else {
// 如果发生变化,执行销毁的回调
lastCallback && lastCallback();
setTimeout(() => {
// 存储新的
workInProgressFiber.hooks[hookIndex++] = [callback(), deps];
});
}
} else {
setTimeout(() => {
// callback 返回的函数,在下次渲染前执行
workInProgressFiber.hooks[hookIndex++] = [callback(), deps];
});
}
}js
function useLayoutEffect(callback, dependencies) {
let hook =
workInProgressFiber.alternate &&
workInProgressFiber.alternate.hooks &&
workInProgressFiber.alternate.hooks[hookIndex];
if (hook) {
let [lastCallback, lastDeps] = hook;
let same =
dependencies &&
dependencies.every((item, index) => item === lastDeps[index]);
if (same) {
workInProgressFiber.hooks[hookIndex++] = [lastCallback, dependencies];
} else {
lastCallback && lastCallback();
// DOM 更新完成后,浏览器绘制之前
queueMicrotask(() => {
lastCallback = callback();
workInProgressFiber.hooks[hookIndex++] = [lastCallback, dependencies];
});
}
} else {
// DOM 更新完成后,浏览器绘制之前
queueMicrotask(() => {
workInProgressFiber.hooks[hookIndex++] = [callback(), dependencies];
});
}
}useRef&createRef 区别
一句话速记:
useRef引用稳定(跨渲染保持同一个对象),createRef在函数组件里每次渲染都会新建。
- 1、两者都会创建一个
ref 对象(形如{ current: xxx })。区别在于:createRef在类组件和函数组件中都能使用,useRef只能在函数组件中使用。 - 2、在函数组件里使用
createRef时,每次组件更新都会重新初始化,得到一个全新的ref 对象(指向不同的引用地址),无法在多次渲染间保存数据。 - 3、
useRef把ref 对象存储在对应hook的memoizedState上(挂在 fiber 的 hook 链表中),组件更新时直接复用之前的对象,不会重新初始化(始终指向同一个引用地址)。
补充:
useRef常用于两类场景 —— ① 保存 DOM 节点引用;② 保存一个「跨渲染保持、但修改时不触发重新渲染」的可变值。类组件中的createRef应写在constructor里,而不是render中,原因同上(避免每次渲染重建)。
js
// mount 阶段
function mountRef<T>(initialValue: T): {| current: T |} {
// 获取 hook 对象
const hook = mountWorkInProgressHook();
const ref = { current: initialValue };
if (__DEV__) {
Object.seal(ref);
}
// 存储
hook.memoizedState = ref;
return ref;
}
// update 阶段
function updateRef<T>(initialValue: T): {| current: T |} {
const hook = updateWorkInProgressHook();
// 返回之前的引用
return hook.memoizedState;
}useState 和 useReducer 使用场景
一句话速记:状态简单、独立、更新直接用
useState;状态之间相互关联、更新逻辑复杂、需要集中管理时用useReducer(本质是把「怎么更新」收敛到 reducer 里)。
1. useState - 简单状态管理
使用场景:
- 管理单个简单值(字符串、数字、布尔值)
- 处理独立状态(无复杂依赖关系)
- 组件内轻量级状态(少量状态变量)
- UI 元素状态(输入框值、开关状态等)
2. useReducer - 复杂状态逻辑
使用场景:
- 管理关联状态组(多个相互影响的状态)
- 处理复杂状态更新逻辑
- 需要状态历史追溯的场景
- 购物车、分步表单
使用原则指南
优先选择 useState 当:
- 状态是独立的基本类型(string/number/boolean)
- 状态更新逻辑简单(直接赋值)
- 组件内状态数量较少(≤3 个)
必须使用 useReducer 当:
- 状态是相互关联的对象(如用户资料{name, age, avatar})
- 下一个状态依赖前一个状态(如计数器队列)
- 需要处理复杂状态转换(如购物车、表单多步骤)
- 需要跨组件/钩子共享状态逻辑(可复用 reducer)
- 大型组件需要收敛更新逻辑(把分散的 setState 集中到 reducer,便于维护,并可把
dispatch透传给子组件而无需重建回调)
总结选择策略
| 场景 | 推荐 Hook |
|---|---|
| 表单输入控制 | useState |
| 开关/复选框状态 | useState |
| 简单计数器 | useState |
| 购物车/复杂业务对象 | useReducer |
| 多步骤表单 | useReducer |
| 需要状态历史记录的功能 | useReducer |
| 全局状态共享(中小型应用) | useReducer + Context |
useRef 和 useState 区别
一句话速记:
useState更新会触发重新渲染、且每次渲染拿到的是当次快照值;useRef更新不触发渲染、且始终读到最新值(同一引用)。
- useState 在更新的时候
会导致组件重新渲染 - useRef 在更新的时候
不会导致组件重新渲染
useRef 可以解决闭包陷阱问题
下面两段演示同一个操作序列的差异:
useState版本因闭包捕获的是「点击那一刻的快照值」,useRef版本因始终指向同一引用而读到最新值。
js
// 1、先点击第一个按钮6次
// 2、在点击第二个按钮1次
// 3、立马在点击第一个按钮4次
import { useState } from "react";
const App = () => {
// setState更新会导致组件更新
const [state, setState] = useState(0);
function handleClick() {
setTimeout(() => {
// 4、由于闭包的特性,当前的值是6,所以打印6
console.log(state);
}, 3000);
}
return (
<>
{/* setState更新会导致组件更新所以是10 */}
<button onClick={() => setState(state + 1)}>{state}</button>
<button onClick={handleClick}>print</button>
</>
);
};
export default App;js
// 1、先点击第一个按钮6次
// 2、在点击第二个按钮1次
// 3、立马在点击第一个按钮4次
import { useRef } from "react";
const App = () => {
const state = useRef(0);
function handleClick() {
setTimeout(() => {
// 4、由于都是指向同一个引用地址所以是10
console.log(state.current);
}, 3000);
}
return (
<>
<button
onClick={() => {
state.current++;
}}
>
{/* useRef值的改变不会导致组件更新,所以一直是0 */}
{state.current}
</button>
<button onClick={handleClick}>print</button>
</>
);
};
export default App;如何拿到 useState 更新的值
- 1、可通过
useEffect,将更新值作为依赖条件 - 2、在更新 useState 之前,先计算值在更新 useState
forwardRef(组件转发)
一句话速记:函数组件没有实例,不能直接接收
ref;用forwardRef把父组件传来的ref转发到子组件内部的 DOM 或(配合useImperativeHandle)暴露的方法上。
WARNING
- 如果对函数组件使用
ref,会抛出如下警告。因为函数组件和类组件不同,没有实例,所以对函数组件直接使用ref是毫无意义的。 - Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
- forwardRef 可以让父组件的
ref获取到子组件的 DOM,同时也可以配合useImperativeHandle,将子组件的函数暴露给父组件ref
js
import React, { useEffect, useRef, forwardRef } from "react";
// 子组件:普通函数组件本身无法接收 ref,需要用 forwardRef 包裹
// 包裹后 forwardRef 会把父组件传来的 ref 作为第二个参数 ref 传进来
const InputText = (props: any, ref: any) => {
return (
<>
<div>
{/* 把转发进来的 ref 绑定到真实 DOM 上,父组件即可拿到这个 input */}
<input ref={ref} />
</div>
</>
);
};
// 用 forwardRef 包裹子组件,得到一个可以接收 ref 的新组件
const ForwardInput = forwardRef(InputText);
const App: React.FC = () => {
// 父组件创建 ref,用来指向子组件内部的 input
const inputRef = useRef < HTMLInputElement > null;
useEffect(() => {
if (inputRef.current) {
// 获取到子组件的input,并让其聚焦
inputRef.current.focus();
}
});
return (
<>
{/* 把父组件的 ref 传给转发后的子组件 */}
<ForwardInput ref={inputRef}></ForwardInput>
</>
);
};
export default App;- 1、
forwardRef接收一个组件,返回一个对象,包含$$typeof(类型标记)和render(即外部传入的组件函数)。 - 2、渲染时,React 检测到该元素的
$$typeof是REACT_FORWARD_REF,就走转发逻辑,内部会保留父组件传递的ref引用。 - 3、调用
render渲染组件,并把props和ref一起传给对应组件,从而让父组件的ref拿到子组件内部的 DOM。
js
// forwardRef 只是把传入的组件包成一个带类型标记的对象
function forwardRef(render) {
return {
$$typeof: REACT_FORWARD_REF, // 类型标记:标识这是一个转发 ref 的组件
render, // 外部传入的真正组件函数
};
}
// 渲染阶段:识别到转发 ref 类型,走专门的挂载逻辑
if (type && type.$$typeof === REACT_FORWARD_REF) {
return mountForwardComponent(vdom);
}
function mountForwardComponent(vdom) {
// 从虚拟 DOM 上取出组件类型、props 和父组件传来的 ref
const { type, props, ref } = vdom;
// 关键:调用 render 时把 ref 作为第二个参数一起传进去,实现转发
const renderVdom = type.render(props, ref);
if (!renderVdom) return null;
// 把返回的虚拟 DOM 转成真实 DOM
return createDOM(renderVdom);
}useImperativeHandle
一句话速记:配合
forwardRef使用,让子组件自定义暴露给父组件ref的内容(通常是一组方法),而不是直接把整个 DOM 暴露出去。
- 1、
useImperativeHandle是一个 Hook,接受2 个参数(可选第 3 个):第一个是父组件转发进来的ref,第二个是一个工厂函数,返回要暴露的对象;第三个是依赖数组,依赖变化时才重新生成该对象。 - 2、当
useImperativeHandle被调用时,内部会执行第二个参数(工厂函数),把它返回的对象存储到父组件的ref.current上。 - 3、因为父子指向同一个引用地址,所以父组件通过
ref.current就能调用到子组件暴露出来的函数。
js
import React, {
useEffect,
useRef,
forwardRef,
useImperativeHandle,
} from "react";
// 定义子组件要暴露给父组件的方法类型
interface IInputText {
onClick: () => void;
}
// 子组件:通过 forwardRef 接收父组件传来的 ref
const InputText = (props: any, ref: any) => {
// useImperativeHandle 自定义暴露给父组件 ref 的内容
// 第一个参数:转发进来的 ref;第二个参数:返回要暴露对象的工厂函数
useImperativeHandle(ref, () => {
return {
// 只暴露 onClick 方法给父组件,而不是整个 DOM
onClick: () => {
console.log("子组件的函数");
},
};
});
return (
<>
<div></div>
</>
);
};
// 用 forwardRef 包裹子组件,使其能接收 ref
const ForwardInput = forwardRef(InputText);
const App: React.FC = () => {
// 父组件创建 ref,类型为子组件暴露的方法集合
const inputRef = useRef < IInputText > null;
useEffect(() => {
if (inputRef.current) {
// 通过 ref.current 调用子组件暴露出来的 onClick 方法
inputRef.current.onClick();
}
});
return (
<>
{/* 把 ref 传给子组件 */}
<ForwardInput ref={inputRef}></ForwardInput>
</>
);
};
export default App;js
// 简化实现:执行工厂函数 handler,把返回的对象挂到 ref.current 上
export function useImperativeHandle(ref, handler) {
// handler() 返回要暴露的对象,赋值给 ref.current
// 父子指向同一引用,父组件即可通过 ref.current 调用这些方法
ref.current = handler();
}react 新增了什么生命周期和删除了什么生命周期,为什么要删除
一句话速记:废弃 3 个
will开头的钩子(componentWillMount/componentWillReceiveProps/componentWillUpdate),新增 2 个更安全的钩子(getDerivedStateFromProps/getSnapshotBeforeUpdate)。根因:Fiber 异步可中断渲染下,render 前的阶段可能被打断并重复执行,will钩子里的副作用会被多次调用。
被废弃的三个(render 前、可能被重复执行):
componentWillMountcomponentWillReceivePropscomponentWillUpdate
新增的两个(用来替代上面的能力):
getDerivedStateFromProps:静态方法,在 render 前调用,根据 props 派生 state(替代componentWillReceiveProps),因是纯函数、无副作用,重复调用也安全。getSnapshotBeforeUpdate:在真实 DOM 更新前调用,捕获更新前的信息(如滚动位置),返回值传给componentDidUpdate。
为什么废弃:React 引入 Fiber 后,render 阶段(生成虚拟 DOM)变成异步可中断的——当有更高优先级的任务到来时,当前 render 会被打断,之后从头重来。这些 will 钩子正处于 render 之前的阶段,一旦里面写了副作用(如请求、订阅、setState),就可能被重复调用导致 bug,因此被废弃。
Vue vs React
一句话速记:Vue 用模板 + 响应式,组件级精确更新、渲染递归不可中断;React 用 JSX + 手动更新,从根节点调度、任务可拆分可中断(Fiber)。
Vue 使用模板,拥抱 HTML
Vue 真正做到了组件级更新:每个组件对应一个渲染
effect,由响应式数据去收集这个effect,数据变化时精确通知对应组件更新Vue 采用递归的方式渲染页面,过程不可中断
React 使用 JSX,拥抱 JS
React 更新都从根节点开始调度,会将一个大任务拆分成多个小的任务单元
React 更新策略是循环的方式,有任务优先级的概念,可中断执行(Fiber 架构)