React 面试准备
基础
为什么在渲染列表时应该使用 key prop
React 通过 key 对比新旧虚拟 DOM 树中的 DOM 节点项,仅更新发生变化的项,避免全量重渲染
React 的 Diff 算法默认按同级索引对比节点。若无 key,列表排序或增删时,React 误判为“旧节点销毁 + 新节点创建”,导致不必要的挂载/卸载;若有稳定 key,React 能识别节点只是位置变化,直接移动 DOM 并保留状态 。
useCallback 和 useMemo 的区别
useMemo 和 memo 的区别
Hooks
Hooks 的规则
Zustand
为什么需要 Zustand
- Prop Drilling:通过
props传状态传递状态会导致代码机器臃肿 - 使用 React Context 方案时一旦 Context 的值发生了变化,所有消费了这个 Context 的组件都会被迫重新渲染
Zustand 样板代码
import { create } from 'zustand'
interface BearState {
bears: number
increasePopulation: () => void
removeAllBears: () => void
}
const useBearStore = create<BearState>((set) => ({
bears: 0, // States
// Actions
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
在组件中使用它:
function BearCounter() {
// 正确姿势:使用 Selector 精准订阅 bears
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here ...</h1>
}
function Controls() {
// 获取修改状态的方法
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
Zustand 最佳实践
- 永远使用 Selector
// 错误:这会导致当前组件订阅了整个 Store 的所有变化
const state = useBearStore()
而是使用 Selector 来精准订阅 store
const bears = useBearStore((state) => state.bears)
- 处理多状态订阅:使用
useShallow
如果一个组件需要同时引入 Store 里的多个状态,为了避免写好几行 Hooks,你可以写在一行,但必须加上 useShallow:
import { useShallow } from 'zustand/react/shallow'
// 选出多个状态,并进行浅比较
const { bears, color } = useBearStore(
useShallow((state) => ({ bears: state.bears, color: state.color }))
)
为什么要用
useShallow? 因为每次执行(state) => ({ bears: ..., color: ... })都会返回一个新的字面量对象{}。在 JS 中,{} !== {}。如果不加useShallow,Zustand 会认为状态每次都变了,从而引发无意义的重复渲染。
3. 将 Actions 结构化(推荐方案)
当项目变大时,Store 里的 Action 会非常多。把它们打包到一个 actions 对象中,可以让组件调用时清晰度翻倍:
const useUserStore = create((set) => ({
username: 'Gamer',
level: 1,
// 把所有修改状态的方法聚合在一起
actions: {
updateName: (name) => set({ username: name }),
levelUp: () => set((state) => ({ level: state.level + 1 })),
}
}))
// 💡 在组件中消费时:
const username = useUserStore((state) => state.username)
const { updateName } = useUserStore((state) => state.actions) // 单独拉取 actions
4. 配合 Immer 处理复杂的嵌套对象
在 React 和 Zustand 中,状态是不可变的(Immutable)。如果你要修改一个深层嵌套的对象,原生写法很痛苦:
// 原生写法:需要疯狂解构
set((state) => ({
user: {
...state.user,
profile: {
...state.user.profile,
age: state.user.profile.age + 1
}
}
}))
Zustand 完美支持 immer 中间件,让你能用“修改指针”的直观写法去写不可变数据:
import { create } from 'zustand'
import { immer } from 'zustand/middleware/immer'
const useTodoStore = create()(
immer((set) => ({
todos: [{ id: 1, text: 'Learn Zustand', done: false }],
toggleTodo: (id) =>
set((state) => {
// 仿佛在直接修改数据,immer 会在底层帮你生成新的不可变对象
const todo = state.todos.find((t) => t.id === id)
if (todo) todo.done = !todo.done
}),
}))
)
5. 数据持久化(Middleware)
工业级应用经常需要把一部分状态(如 Token、用户偏好设置)存到 localStorage 中。Zustand 内置了 persist 中间件,一行代码搞定自动同步:
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
const useAuthStore = create()(
persist(
(set) => ({
token: null,
setToken: (token) => set({ token }),
}),
{
name: 'auth-storage', // localStorage 中的 key 名称
}
)
)
总结:你的心智模型
你可以把 Zustand 想象成在你的 React 应用旁边盖了一座“数据中央仓库”。
- 组件想改数据?发个信使(调用 Action)去仓库改。
- 组件想用数据?拉一根专线(Selector Hook)连到仓库对应的货架上。货架的东西变了,专线才会响。
你目前正在开发的 React 项目中,有哪些状态是打算尝试用 Zustand 来管理的吗?我们可以顺着你的具体业务场景来聊聊怎么设计这个 Store。
Zustand 的解法:独立于组件树的“状态发布中心”
Zustand 的核心设计理念非常简单:把状态从 React 的组件树里抽离出来,存放在一个独立的、全局的 JavaScript 对象中(我们称之为 Store)。
任何组件需要数据,直接去 Store 里“订阅”那一部分数据。当数据变动时,只有真正使用了该数据的组件才会重新渲染。高效、干净。
2. Zustand 的核心设计理念:大道至简
Zustand(德语中“状态”的意思)的设计哲学可以概括为三点:
- 单一数据源(Single Store)且不污染组件: 所有的全局状态和修改状态的方法(Actions),都写在同一个地方。
- 基于 Hooks 的原子化订阅(Selectors): 它是通过 React Hooks 的方式提供给组件使用的。组件可以通过“选择器”只精准订阅它关心的那一个变量。
- Actions 与 State 共存: 在 Zustand 中,数据(State)和改变数据的函数(Actions)是写在一起的,这让业务逻辑非常内聚。
从零写一个 Zustand Store
我们来看一下它有多直观:
import { create } from 'zustand'
// 1. 定义状态的类型(TypeScript 最佳实践)
interface BearState {
bears: number
increasePopulation: () => void
removeAllBears: () => void
}
// 2. 创建一个全局的 Store
const useBearStore = create<BearState>((set) => ({
bears: 0, // 状态(State)
// 改变状态的方法(Actions)
increasePopulation: () => set((state) => ({ bears: state.bears + 1 })),
removeAllBears: () => set({ bears: 0 }),
}))
在组件中使用它:
function BearCounter() {
// 正确姿势:使用 Selector 精准订阅 bears
const bears = useBearStore((state) => state.bears)
return <h1>{bears} around here ...</h1>
}
function Controls() {
// 获取修改状态的方法
const increasePopulation = useBearStore((state) => state.increasePopulation)
return <button onClick={increasePopulation}>one up</button>
}
3. Zustand 的高级设计:精准控制重新渲染
这是 Zustand 最牛的地方,也是它性能强大的秘密。
当你写 const bears = useBearStore((state) => state.bears) 时,Zustand 在底层做了一件事:它会监听整个 Store 的变化,但每次 Store 变化时,它会拿旧的 bears 和新的 bears 做对比(默认是 === 严格相等对比)。
- 如果
bears没变,即使 Store 里的其他变量(比如user)变了,BearCounter组件也绝对不会重新渲染。
我们经常在 React 社区听到一个词:“不可变数据”(Immutability)。如果你觉得它模糊,是非常正常的,因为这和我们平时直觉上的“改数据”习惯完全相反。
用最通俗的一句话来解释:在 React 的世界里,你一旦创建了一个对象或数组,就永远不要去修改它的内部成员。如果你想改变它的值,你就必须用一块全新的内存,去重新生成一个全新的对象。
为了让你彻底搞懂,我们由浅入深,从底层原理到 React 为什么要这么做来拆解。
1. 现实生活中的比喻:改账本 vs 撕新纸
假设你有一张记事纸,上面写着你今天的待办事项:
- 可变(Mutable): 你拿橡皮擦把第一行擦掉,改写成新的内容。纸还是那张纸(内存地址没变),但内容变了。
- 不可变(Immutable): 你绝对不碰原来那张纸。你拿出一张全新的干净白纸,把不需要改的内容原封不动抄过来,把需要改的那一行写上新的内容。最后,你把旧纸扔了,手里拿的是新纸(内存地址彻底变了)。
在 React 中,你必须当那个“拿新纸”的人。
2. 核心区别:代码直观对比
我们在 JavaScript 里看看这两种操作的区别:
❌ 错误做法:可变操作(直接修改原对象)
const user = { name: 'Alex', age: 25 };
// 直接修改内部属性
user.age = 26;
console.log(user); // { name: 'Alex', age: 26 }
在底层:user 指向的内存地址(比如 0x001)没有发生任何变化,改变的只是这块内存内部的数据。
正确做法:不可变操作(生成新对象)
const user = { name: 'Alex', age: 25 };
// 使用展开运算符(...)复制老数据,并覆盖需要修改的属性
const newUser = {
...user,
age: 26
};
console.log(user); // { name: 'Alex', age: 25 } (老对象完好无损)
console.log(newUser); // { name: 'Alex', age: 26 } (产生了一个全新的对象)
在底层:newUser 开辟了一块全新的内存地址(比如 0x002)。
3. React 为什么要强迫我们使用“不可变对象”?
这其实是 React 为了极致的性能而做出的设计权衡。
核心原因:浅比较(Shallow Comparison)的速度是极快的
React 的核心机制是:当状态(State)改变时,重新渲染组件。
那么 React 怎么知道状态有没有变呢?它需要对比“老状态”和“新状态”。
如果状态是一个拥有上百个属性的深度嵌套对象,React 如果要去挨个遍历每个属性看谁变了(深比较),那性能代价太高了,页面直接卡死。
所以 React 玩了个花招:它只做浅比较,也就是只对比对象的内存地址(Object.is(oldState, newState) 或者 oldState === newState)。
- 如果你用了可变操作(直接改内部):
oldState.age = 26;
// React 检查:oldState === oldState 结果是 true!地址没变!
// 结论:React 认为数据没变,拒绝重新渲染界面。你的界面就“卡死”不更新了。
- 如果你用了不可变操作(传了新对象):
const newState = { ...oldState, age: 26 };
// React 检查:oldState === newState 结果是 false!地址变了!
// 结论:React 秒懂数据变了,立刻高效地重新渲染界面。
划重点: 不可变数据让 React 不需要关心对象里面到底有什么,它只需要看一眼“对象的引用地址变了没有”,就能决定要不要更新界面。这种比对只需要 1 毫秒。
4. Zustand / React 里的常见不可变操作套路
在实际写代码时,由于不能直接用 = 修改数组和对象,我们需要熟练使用以下几种语法:
1. 修改对象属性(使用 ... 展开运算符)
// Zustand 的 set 语法要求返回一个新对象
set((state) => ({
user: { ...state.user, age: 26 }
}))
2. 向数组添加一项(不要用 push,要用 ...)
push 会改变原数组,不可用。
// ❌ 错误:state.list.push(newItem)
// 正确:展开老数组,放入新项,生成新数组
set((state) => ({
list: [...state.list, newItem]
}))
3. 删除数组中的一项(使用 filter)
filter 默认会返回一个全新的数组,完美符合不可变要求。
set((state) => ({
// 过滤掉 id 为 2 的项,返回一个新数组
list: state.list.filter(item => item.id !== 2)
}))
4. 修改数组中的某一选项(使用 map)
map 也会返回一个新数组。
set((state) => ({
list: state.list.map(item =>
item.id === targetId
? { ...item, done: true } // 找到目标,用新对象替换
: item // 不是目标,原封不动保留引用
)
}))
5. 解放双手:Immer 是如何解决这个痛苦的?
正如你在学习 Zustand 最佳实践时看到的,当对象层级很深时,每一层都要写 { ...state } 会让人抓狂。
这时候 Immer 出现了。Immer 提供了一个暂存盘(我们称为 draft,即草稿)。它允许你像写原生 JS 那样去直接修改草稿,等修改完了,Immer 会在底层自动帮你把这些修改转化为一个全新的不可变对象。
// 有了 Immer,你不需要写任何 ... 运算符
set((state) => {
// 在这里,你可以违背 React 的常理,直接去修改内部属性
state.user.profile.settings.theme = 'dark';
// Immer 会在后台默默地帮你搞定“撕新纸、抄数据”的全过程!
})
现在,你对“不可变对象”这个概念,心里有没有感觉更清晰一点了?
在 React 开发中,决定一个状态或逻辑应该写成 Custom Hook(自定义 Hook)、Zustand Store(全局状态) 还是 Context(上下文),是每个开发者都会面临的核心架构问题。
我们可以从两个维度来拆解:“数据的生命周期(谁在用它)” 和 “逻辑的本质(它是数据还是行为)”。
为了让你在选型时不再纠结,我们梳理出了一个决策模型:
1. 核心定位与应用场景对比
我们先用一张表来看清它们的本质区别:
| 工具 | 它的本质是什么? | 核心痛点/解决什么问题? | 典型应用场景 |
|---|---|---|---|
| Custom Hook(自定义 Hook) | 逻辑的复用封装(状态是隔离的) | 提取重复的代码逻辑(如窗口大小监听、请求发送)。不共享状态。 | 封装表单验证、封装 WebSocket 连接、封装倒计时逻辑。 |
| Zustand Store(全局状态) | 独立于组件树的****单例数据中心 | 跨多层级、高频更新、需要极致性能的全局共享数据。 | 用户登录信息、全站主题配置、复杂 3D 场景的节点数据、实时定位轨迹数据。 |
| Context(局部上下文) | 依赖注入工具(依然强依赖组件树) | 某一个组件树局部分支内的数据共享,或作为组件库封装的纽带。 | 复杂低码表单内的跨组件联动、下拉菜单(Select/Option)组件库的内部通信。 |
2. 场景深度拆解:什么时候用哪个?
场景一:我有一段逻辑,很多地方都要用,但它们的数据不需要共享
- 选择:Custom Hook
- 设计理念: 自定义 Hook 的核心是复用逻辑,而不是复用数据。每次你在组件里调用
useMyHook(),它内部的useState都是完全独立的,互不影响。 - 工程实例: 假设你要写一个“获取当前鼠标在 3D 画布中世界坐标”的逻辑。A 组件要用它来做悬浮提示,B 组件要用它来做点击判定。
// useMousePosition.ts (Custom Hook)
export function useMousePosition() {
const [pos, setPos] = useState({ x: 0, y: 0 });
useEffect(() => {
const handleMove = (e) => setPos({ x: e.clientX, y: e.clientY });
window.addEventListener('mousemove', handleMove);
return () => window.removeEventListener('mousemove', handleMove);
}, []);
return pos; // 返回逻辑处理后的结果
}
A 组件和 B 组件各自调用这个 Hook,它们在内存里会创建两个独立的事件监听器,数据互不干扰。
场景二:我有一堆数据,全站到处都在用,或者更新极其频繁
- 选择:Zustand Store
- 设计理念: 只要数据是全局的(全站只需要一份),或者需要精准控制重新渲染(高性能),毫不犹豫选择 Zustand。
- 工程实例: 比如一个工业数字孪生大屏项目,里面包含:
- 顶部的用户个人信息栏。
- 左侧的设备实时数据列表。
- 中央的 3D 渲染画布(包含大量的坐标、选中状态更新)。
这时候,如果这些设备轨迹数据、选中状态放在 React Context 里,一秒钟更新 60 次,整个大屏的所有 UI 组件全都会卡死。而放在 Zustand Store 里,利用 Selector(选择器),只有负责渲染 3D 轨迹的组件会高频更新,左侧和顶部的文字组件纹丝不动。
场景三:我有一组数据,只在某一个局部页面或某一组嵌套组件内共享
- 选择:React Context
- 设计理念: Context 的本质是动态依赖注入。它的生命周期是跟随 Provider 组件的挂载和卸载的。如果你希望“当这个页面关闭时,里面的状态全部自动销毁”,或者“同一个组件在页面里实例化多次,每次内部都有独立的上下文”,这就是 Context 的主场。
- 工程实例: 假设你在做一个复杂的“流程图配置编辑器”组件:
这个编辑器由<EditorProvider>、<Sidebar>、<Canvas>等子组件组成。用户可能在同一个页面里同时打开两个流程图编辑器(左边对比右边)。 - 如果你用 Zustand,它是全局单例,两个编辑器会抢同一份数据,直接乱套。
- 如果你用 Context,左边和右边的编辑器分别被包裹在各自的
<EditorProvider>里,它们的数据完美隔离,互不干扰,且关闭编辑器时内存自动释放。
3. 终极决策黄金法则(避坑指南)
如果你在写代码时卡住了,可以用这三个问题来做决定:
- 它到底要不要共享数据?
- 不需要(只是代码长,想抽离复用) \(\rightarrow\) Custom Hook。
- 需要 \(\rightarrow\) 走向问题 2。
- 这个数据的范围是全站唯一,还是局部多实例?
- 局部多实例(比如一个复杂的表格组件、一个弹窗内的多步骤表单) \(\rightarrow\) Context。
- 全站唯一(或绝大多数页面都要调取) \(\rightarrow\) 走向问题 3。
- 这个数据更新频繁吗?对性能敏感吗?
- 更新不频繁,且纯属于配置型(如国际化语言包
ThemeContext、轻量级的全局权限认证) \(\rightarrow\) Context 或 Zustand 均可(若项目已有 Zustand,推荐直接用 Zustand 统一管理)。 - 更新频繁,或者有很多组件订阅它 \(\rightarrow\) 坚决使用 Zustand Store。
💡 进阶组合拳:Custom Hook + Store / Context
在实际大厂规范中,我们经常将它们组合使用。对外只暴露 Hook,对内隐藏 Store 或 Context 的实现细节。
例如,不要在组件里直接读 Store:
// ❌ 不推荐:组件直接感知到全局 store 的存在
const username = useUserStore(state => state.username);
// 推荐做法:封装成一个 Custom Hook
// useAuth.ts
export function useAuth() {
const user = useUserStore(state => state.currentUser);
const login = useUserStore(state => state.actions.login);
const isAuthenticated = !!user;
return { user, isAuthenticated, login };
}
// 在组件中直接使用,未来哪怕把 Zustand 换成别的,组件也无需修改
const { user, isAuthenticated } = useAuth();
你目前手头上的项目里,有没有哪个功能让你在“该用 Context 还是 Zustand”之间产生了动摇?你可以把具体的组件嵌套关系和数据流向告诉我,我们一起来做个就地拆解。
这几年 React 团队的动作非常大,甚至可以说是对 React 诞生以来开发习惯的一次颠覆。他们核心的进化方向非常明确:把心智负担从开发者身上拿走,交给机器和底层架构。
既然你对底层设计理念感兴趣,那我们就用通俗的语言,把这几年最轰动的几个核心概念(RFC、Compiler、StrictMode 进化,以及大名鼎鼎的 RSC)由浅入深地盘明白。
1. Compiler(React 编译器):再见,useMemo 和 useCallback
这可能是这两年 React 社区最兴奋的变革。
痛点:手动写性能优化太痛苦了
在过去,为了防止组件无脑地重复渲染,我们必须小心翼翼地用 useMemo 缓存复杂计算,用 useCallback 缓存函数:
// 以前的写法:不仅代码丑陋,而且新手极易漏掉依赖项数组
const expensiveValue = useMemo(() => doSomething(a), [a]);
const handleClick = useCallback(() => console.log(b), [b]);
这种“手动挡”的操作不仅增加了开发者的心智负担,而且一旦依赖项写错,就会引发难以排查的 Bug。
进化:React Compiler(代号 Forget)
React 团队认为:既然“不可变对象”的规则是确定的,那么代码怎么优化,机器应该比人类更清楚。
React Compiler 本质上是一个 Babel/Vite 插件。它在你的代码编译阶段(也就是打包成浏览器运行的代码之前),自动分析你的抽象语法树(AST),并在底层自动帮你注入缓存逻辑。
- 现在的体验: 你只需要像写普通 JS 那样自由地写代码,不需要写任何
useMemo。 - 编译后: 编译器会自动帮你把组件里的每一行表达式、每个对象都变成自动缓存的。
- 总结: React 正在从“手动挡”走向“自动挡”。(注:截至 2026 年,React Compiler 已经在 React 19 中作为核心生态体系被广泛生产实践)。
2. StrictMode(严格模式):为“瞬移”和“并发”做准备
你可能在应用入口处见过 <StrictMode> 标签。这几年它的存在感越来越强,甚至让很多新手崩溃,因为它会让你的组件在开发环境下执行两次。
为什么在开发环境下,useEffect 会触发两次?
很多同学发现,在 StrictMode 下,组件一挂载,useEffect 里的请求会发两次。这其实是 React 故意在“找茬”。
React 未来(以及现在正在普及的 Concurrent Mode 并发模式)有一个终极愿景:组件的渲染是可以被随时中断、恢复、甚至“离屏缓存(Offscreen)”的。
比如:用户在一个标签页和另一个标签页之间切换,React 希望能把不可见的页面组件“冻结”在内存里,切换回来时“秒开”,而不是重新销毁再创建。
为了实现这个愿景,你的组件必须具备极高的确定性:挂载(Mount)和卸载(Unmount)必须是干净、可预测、无副作用的。
useEffect(() => {
const window = startTracking(); // 1. 创建了某种副作用
return () => {
window.stopTracking(); // 2. 必须在清理函数里彻底杀掉它!
};
}, []);
StrictMode 的监控逻辑:
在开发模式下,StrictMode 会故意执行:挂载 \(\rightarrow\) 卸载 \(\rightarrow\) 重新挂载。
如果你在“卸载”时没有写好清理函数(Cleanup),第二次挂载时就会暴露 Bug(比如定时器叠加、内存泄漏)。
- 总结: StrictMode 不是一个功能,而是一个“考官”。它在开发阶段疯狂折磨你,是为了确保你的代码在线上高并发、动态切页面时稳如泰山。
3. RFC(Request for Comments):开源世界的“立法会”
RFC 并不是 React 独有的技术名词,它是开源社区(如 Rust, Vue, React, Ember 等)用来制定重大变更的“征求意见稿机制”。
当 React 团队(或者社区大牛)想要给 React 增加一个破坏性的新特性(比如当年的 Hooks,后来的 Server Components,或者全新的 use API)时,不能直接一拍脑袋就把代码合并了。他们需要走 RFC 流程:
- 写提案: 详细阐述“为什么要加这个功能”、“解决了什么痛点”、“API 长什么样”、“对老项目有什么破坏性”。
- 社区对线: 提案公开在 GitHub 的
reactjs/rfcs仓库里,全世界的开发者(包括各大框架的作者、大厂架构师、普通开发者)都会在下面评论、吐槽、辩论。 - 修定与通过: 经过几轮撕扯和修改后,如果达成共识,该提案就会转为“已接受(Accepted)”,正式进入排期开发。
- 总结: 了解 RFC 能让你看清 React 的未来。很多时候,一个新特性从 RFC 提出来到最后稳定落地,可能需要 2~3 年的时间。
4. 补充一个最重要的时代跨越:RSC(React Server Components)
聊最近几年的 React,绝对绕不开 RSC(服务端组件)。这是 React 诞生以来最大的架构范式转变。
以前的模式:客户端渲染(CSR)
浏览器下载一个极其精简的 HTML 和一堆巨大的 JS 文件,然后在用户的手机/电脑里运行 JS,动态生成 DOM 节点。
- 缺点: 首屏加载慢,对 SEO 不友好;组件为了拿数据,要在前端发一堆请求(造成瀑布流延迟)。
现在的模式:RSC(Server Components)
React 将组件分成了两类:
- Server Components(服务端组件,默认): 它们只在服务器上运行。它们可以直接连接数据库、读取服务器文件。运行完后,生成的不是 HTML 字符串,而是一种轻量级的流式数据,传给浏览器。
- Client Components(客户端组件,带
"use client"指令): 传统的 React 组件,在浏览器里运行,可以有useState、useEffect和点击事件。
为什么这个概念很震撼?
- 零打包体积(Zero Bundle Size): 如果你在 Server Component 里引用了一个巨大的 Markdown 解析库(比如 5MB),这个库完全不会被下载到用户的浏览器里。服务器运行完就扔了,用户端不需要多下载 1KB 的 JS。
- 安全与速度: 你的数据库连接密码、鉴权逻辑安全地留在了服务器,前端不需要暴露任何内部接口。
梳理:这几年 React 的演进心智模型
如果把这几年的全景图串起来,你会发现 React 团队在下一盘大棋:
- 通过 RFC 与全球顶级开发者切磋,规划出通往未来的路;
- 通过 RSC 让框架把重活、脏活(读数据库、解压大文件)留在服务器上做;
- 通过 Compiler 解决前端臭名昭著的重新渲染性能问题,解放开发者的双手;
- 通过 StrictMode 强迫开发者写出无副作用的纯净组件,为未来更疯狂的并发、离屏渲染铺平道路。
这些新概念里,哪一个最颠覆你之前对“前端开发就是写写页面”的固有印象?
性能
React 为什么要引入 Fiber
在 React 15 及之前,组件的 reconciliation(协调)过程是同步、递归、不可中断的:
- 一旦开始更新(比如
setState),React 会一口气遍历整个组件树,计算出 Virtual DOM 的差异。 - 这个过程会阻塞主线程,如果组件树很大或更新复杂,就会导致:
- 页面卡顿(掉帧)
- 用户交互无响应(如点击没反应)
🎯 问题本质:JavaScript 是单线程的,长时间运行的同步任务会阻塞 UI 渲染和用户输入。
解决方案:Fiber 架构(React 16+)
Fiber 是 React 16 引入的全新的协调引擎,核心目标是:
将渲染/更新过程拆分成可中断的小任务,让浏览器有机会处理高优先级任务(如用户输入、动画)。
Fiber 的关键特性:
-
增量渲染(Incremental Rendering)
把 reconciliation 拆成多个小单元(fiber 节点),每个单元执行完后检查是否该让出控制权。 -
可中断与恢复
如果有更高优先级任务(如用户点击),React 可以暂停当前更新,先处理紧急任务,之后再恢复。 -
优先级调度(Prioritization)
不同更新有不同优先级(如setStatevsuseTransition),Fiber 能动态调整执行顺序。 -
并发模式(Concurrent Mode)基础
Fiber 为后续的并发特性(如 Suspense、Streaming SSR)打下基础。
✅ Fiber 的本质:用“协作式调度”解决主线程阻塞问题。
浙公网安备 33010602011771号