一、Fiber架构:React渲染的核心基石

1.1 为什么需要Fiber?

React 15及之前采用“栈协调器”(Stack Reconciler),渲染过程是同步且不可中断的:一旦开始调和(Reconciliation),会持续占用主线程,若组件树庞大,会导致UI卡顿(如滚动、输入等高频交互无响应)。

Fiber架构是React 16推出的“增量协调器”(Incremental Reconciler),核心目标是将同步渲染拆分为可中断、可恢复、优先级可控的小任务,实现主线程空闲时执行渲染任务,有高优先级任务(如用户输入)时暂停渲染,释放主线程。

1.2 Fiber核心设计:数据结构与工作循环

1.2.1 核心数据结构:Fiber节点

Fiber本质是“工作单元”,每个DOM元素/组件对应一个Fiber节点,通过链表结构串联,替代传统栈结构,支持中断后恢复。核心属性如下(简化版):

const FiberNode = {
  tag: 0, // 节点类型(元素/函数组件/类组件等)
  key: null, // 用于Diff算法复用节点
  type: null, // 组件类型或DOM标签名
  return: null, // 父Fiber节点(链表回溯指针)
  child: null, // 第一个子Fiber节点
  sibling: null, // 下一个兄弟Fiber节点
  index: 0, // 子节点在父节点中的索引
  pendingProps: null, // 待应用的props
  memoizedProps: null, // 已缓存的props(当前渲染用)
  memoizedState: null, // 已缓存的状态(函数组件存Hooks链表,类组件存实例状态)
  alternate: null, // 双缓存Fiber树的对应节点
  flags: 0, // 节点标记(更新/删除/新增等)
  nextEffect: null, // 副作用链表指针
};

核心链表关系:父节点通过child指向第一个子节点,子节点通过sibling指向兄弟节点,所有节点通过return指向父节点,形成完整的Fiber树遍历链路(深度优先遍历的非递归实现)。

1.2.2 双缓存Fiber树

React通过“双缓存”机制避免DOM操作冲突,提升渲染性能,存在两棵Fiber树:

  • Current树:当前渲染在页面上的Fiber树,与真实DOM一一对应。

  • WorkInProgress树:正在调和(Reconciliation)的Fiber树,基于Current树拷贝生成,所有更新操作在这棵树上进行,避免直接修改Current树导致UI不稳定。

切换逻辑:当WorkInProgress树调和完成后,通过root.current = workInProgress替换Current树,再批量执行DOM操作(提交阶段),实现无感知更新。

1.2.3 Fiber工作循环(两大阶段)

Fiber工作循环由react-reconciler包主导,分为“调度阶段(Reconciliation)”和“提交阶段(Commit)”,仅调度阶段可中断。

  1. 调度阶段(可中断、可恢复)

    1. 核心任务:遍历WorkInProgress树,执行Diff算法、更新Fiber节点状态、收集副作用(如DOM增删改、Hooks副作用)。

    2. 流程:从根Fiber开始,通过“child→sibling→return”链表遍历所有节点,对每个节点执行“beginWork”(处理节点更新,生成子节点)和“completeWork”(完成节点处理,收集副作用)。

    3. 中断机制:依赖react-scheduler的时间切片(Time Slicing),每执行一个工作单元后,检查剩余时间(默认16ms,匹配屏幕刷新率),若超时或有高优先级任务,暂停遍历,记录当前进度(通过链表指针),待主线程空闲后恢复。

  2. 提交阶段(不可中断)

    1. 核心任务:执行调度阶段收集的副作用,更新真实DOM,触发组件生命周期(类组件)或Hooks回调(函数组件)。

    2. 流程:分为“before mutation”(执行DOM操作前,触发getSnapshotBeforeUpdate)、“mutation”(执行DOM增删改)、“layout”(DOM更新后,触发componentDidMount/Update、useLayoutEffect回调)三个子阶段。

    3. 不可中断原因:DOM操作是同步的,中断会导致UI撕裂、状态不一致。

1.3 核心包协同:Fiber的实现依赖

  • react-reconciler:实现Fiber树的调和逻辑(beginWork、completeWork)、Diff算法、副作用收集。

  • react-scheduler:提供时间切片、优先级调度能力,决定Fiber工作单元何时执行、何时中断。

  • react-dom:提供宿主环境适配(浏览器DOM),在提交阶段执行真实DOM操作,触发生命周期/Hooks回调。

1.4 高频面试题

  1. Q:Fiber架构解决了React 15的什么问题?核心设计思路是什么? A:解决了同步渲染导致的主线程阻塞问题。核心思路:用链表结构替代栈结构,将渲染任务拆分为可中断的工作单元,通过双缓存机制和时间切片,实现渲染与高优先级任务的并发调度。

  2. Q:Fiber工作循环的两大阶段是什么?各自特点和任务是什么? A:调度阶段(可中断):遍历Fiber树、Diff更新、收集副作用;提交阶段(不可中断):执行副作用、更新DOM、触发回调。

  3. Q:双缓存Fiber树的作用是什么?切换逻辑是什么? A:避免DOM操作冲突,提升渲染稳定性。切换逻辑:WorkInProgress树调和完成后,通过root.current指向WorkInProgress树,替换Current树。

二、并发模式(Concurrent Mode)

2.1 核心定义

并发模式不是具体API,而是React的一种渲染策略,基于Fiber架构实现,允许React“同时”处理多个任务(实际是通过时间切片和优先级调度实现的伪并发),核心特性:可中断、可恢复、优先级驱动

注意:React 18中已用“并发特性”(如useTransition、Suspense)替代旧的Concurrent Mode标志,不再需要手动开启模式,而是通过API按需启用并发能力。

2.2 优先级调度机制(react-scheduler核心能力)

并发模式的核心是“优先级”,react-scheduler将任务分为不同优先级,高优先级任务中断低优先级任务,确保用户交互(如点击、输入)优先响应。优先级从高到低:

  1. ImmediatePriority:立即执行(同步),如flushSync触发的更新。

  2. UserBlockingPriority:用户阻塞级(250ms超时),如点击、输入、滚动等用户交互。

  3. NormalPriority:普通级(5000ms超时),如普通状态更新。

  4. LowPriority:低优先级(10000ms超时),如列表滚动时的非关键更新。

  5. IdlePriority:空闲级,仅在主线程完全空闲时执行,如日志上报。

实现原理:react-scheduler通过requestIdleCallback(降级为setTimeout)实现时间切片,每轮时间切片结束后,检查是否有更高优先级任务,若有则切换任务执行。

2.3 核心并发API(React 18+)

  • useTransition:将低优先级更新标记为“过渡更新”,不阻塞高优先级任务(如输入框输入时,列表筛选可作为过渡更新)。

  • Suspense:配合React.lazy实现组件懒加载,或等待数据请求完成后渲染,支持并发渲染中的“暂停-恢复”。

  • useDeferredValue:延迟更新非关键值,确保高优先级任务优先完成。

2.4 高频面试题

  1. Q:React并发模式的核心特性是什么?与同步模式的区别? A:核心特性:可中断、可恢复、优先级驱动。区别:同步模式渲染不可中断,阻塞主线程;并发模式通过时间切片和优先级调度,实现渲染与高优先级任务的协同,避免卡顿。

  2. Q:useTransition的作用和底层原理? A:作用:标记低优先级更新,不阻塞高优先级任务。原理:将更新拆分为“紧急更新”(如输入)和“过渡更新”,过渡更新被标记为低优先级,若有高优先级任务则中断,空闲后恢复。

  3. Q:react-scheduler如何实现优先级调度? A:通过划分任务优先级,结合时间切片机制,每轮时间切片后检查优先级,高优先级任务中断低优先级任务,确保关键任务优先执行。

三、可中断渲染

3.1 核心原理

可中断渲染是Fiber和并发模式的核心能力,本质是“将渲染任务拆分为细粒度工作单元,通过时间切片和优先级调度,实现任务的暂停、中断与恢复”。

关键支撑:

  • 链表结构:Fiber节点的return/child/sibling指针,使遍历可随时中断,下次恢复时能通过指针找到上一次的位置。

  • 状态缓存:WorkInProgress树缓存当前渲染状态,中断后无需重新计算,恢复时直接基于缓存继续。

  • 时间切片:react-scheduler控制每轮工作单元执行时间(≤16ms),超时则暂停,释放主线程。

3.2 中断与恢复流程

  1. React调度器从任务队列取出高优先级任务,开始执行Fiber调和(调度阶段)。

  2. 每执行一个Fiber节点的beginWork/completeWork(一个工作单元),检查剩余时间:

    1. 若有剩余时间,继续遍历下一个Fiber节点。

    2. 若超时或有更高优先级任务,记录当前遍历位置(当前Fiber节点),暂停调和,将控制权交还给主线程。

  3. 主线程执行完高优先级任务后,空闲时调度器恢复调和,从上次暂停的Fiber节点继续遍历。

  4. 调和完成后进入提交阶段,执行DOM更新(不可中断)。

3.3 注意事项:副作用与可中断性

调度阶段(可中断)不能执行有副作用的操作(如DOM修改、网络请求),否则中断后会导致副作用重复执行或遗漏。因此:

  • 调度阶段仅收集副作用(标记在Fiber节点的flags中),不执行。

  • 副作用统一在提交阶段(不可中断)执行,确保执行顺序和完整性。

3.4 高频面试题

  1. Q:React可中断渲染的实现基础是什么? A:基于Fiber链表结构(可恢复遍历)、WorkInProgress树(状态缓存)、react-scheduler时间切片(控制执行时长)三大核心。

  2. Q:为什么调度阶段可中断,提交阶段不可中断? A:调度阶段仅处理Fiber节点的计算和状态更新,无真实DOM操作,中断无副作用;提交阶段执行DOM操作和副作用回调,DOM操作同步且不可中断,否则会导致UI不一致。

四、Hooks底层原理

4.1 核心设计目标

Hooks解决函数组件无状态、无生命周期、状态逻辑复用复杂(如高阶组件嵌套)的问题,使函数组件能拥有状态管理、副作用处理能力,同时简化逻辑复用。

4.2 底层数据结构:Hooks链表

函数组件的Hooks存储在对应Fiber节点的memoizedState中,以单向链表形式组织,每个Hook对应一个链表节点(Hook对象)。核心结构:

const Hook = {
  memoizedState: null, // Hook缓存的状态(如useState的state)
  baseState: null, // 基础状态(用于处理更新队列)
  baseQueue: null, // 更新队列(如useState的setState回调队列)
  queue: null, // 待处理的更新队列
  next: null, // 下一个Hook节点(链表指针)
};

关联逻辑:

  • 函数组件渲染时,会从Fiber节点的memoizedState取出Hooks链表,通过“指针遍历”依次处理每个Hook。

  • 每次调用useState、useEffect等Hooks API,本质是创建/复用Hook节点,更新链表状态。

4.3 核心Hooks实现细节

4.3.1 useState

  1. 初始化阶段

    1. 调用useState时,创建Hook节点,将初始值作为memoizedState,初始化空更新队列(queue)。

    2. 将Hook节点加入Hooks链表,返回[memoizedState, dispatchSetState](dispatch是更新触发函数)。

  2. 更新阶段

    1. 调用dispatchSetState时,将更新操作(新值或更新函数)加入Hook的更新队列。

    2. React调度器触发重新渲染,遍历Hooks链表时,处理该Hook的更新队列:合并更新、计算新状态,更新Hook的memoizedState。

    3. 函数组件重新执行,返回新状态。

  3. 批量更新:同一事件循环中多次调用dispatchSetState,React会合并更新队列,仅执行一次渲染(由react-dom的批量更新机制实现)。

4.3.2 useEffect

  1. 初始化阶段

    1. 调用useEffect时,创建Hook节点,缓存回调函数、依赖数组。

    2. 将副作用信息(回调、依赖、清理函数)标记在Fiber节点的flags中,等待提交阶段执行。

  2. 更新阶段

    1. 重新执行useEffect,对比新旧依赖数组(浅比较)。

    2. 若依赖变化:先执行上一轮的清理函数(提交阶段的layout之前),再缓存新回调和依赖,标记新副作用。

    3. 若依赖不变:复用旧副作用,不执行回调和清理。

  3. 执行时机:useEffect回调在DOM更新后执行(提交阶段的layout之后),且是异步的,不阻塞DOM渲染;清理函数在组件卸载或依赖变化时执行。

4.3.3 useRef

useRef返回一个不变的ref对象({ current: ... }),底层实现:

  • 初始化时,Hook节点的memoizedState存储ref对象。

  • 更新阶段,复用该Hook节点,ref对象引用不变,仅current属性可修改。

  • 特性:ref变化不会触发组件重新渲染,可用于存储DOM元素、跨渲染周期的变量。

4.4 Hooks使用限制的底层原因

Hooks不能在条件语句、循环、嵌套函数中调用,核心原因:Hooks链表的遍历依赖调用顺序,顺序错乱会导致链表匹配错误,状态丢失或错乱

示例:

// 错误示例
if (condition) {
  useState(0); // 条件语句导致调用顺序不稳定
}
useEffect(() => {}, []);
// 若condition为false,Hooks链表遍历到useEffect时,会匹配到错误的Hook节点(原本属于useState的节点),导致状态异常。

4.5 高频面试题

  1. Q:Hooks的底层数据结构是什么?为什么Hooks不能在条件语句中调用? A:底层是单向链表,存储在Fiber节点的memoizedState中。原因:Hooks依赖调用顺序遍历链表,顺序错乱会导致链表匹配错误,状态异常。

  2. Q:useState的更新队列是如何工作的?批量更新的原理? A:useState的更新会加入Hook节点的更新队列,重新渲染时合并队列计算新状态。批量更新:react-dom在事件回调、生命周期中合并同一事件循环的更新,仅执行一次渲染,flushSync可打破批量更新。

  3. Q:useEffect和useLayoutEffect的区别?执行时机分别是什么? A:useEffect异步执行,在DOM更新后(提交阶段layout之后),不阻塞渲染;useLayoutEffect同步执行,在DOM更新后、浏览器绘制前(提交阶段layout阶段),阻塞渲染。适合需要操作DOM后立即获取布局信息的场景。

  4. Q:useRef的ref对象为什么能跨渲染周期保持不变? A:useRef的ref对象存储在Hook节点的memoizedState中,更新阶段复用Hook节点,ref对象的引用不变,仅current属性可修改,且ref变化不触发重新渲染。

五、重新渲染内部流程

5.1 重新渲染的触发条件

  1. 状态更新:函数组件useState/useReducer触发,类组件setState触发。

  2. Props变化:父组件重新渲染,子组件props发生浅变化(若子组件未用memo包裹,即使props不变也会重新渲染)。

  3. 强制更新:调用forceUpdate(类组件),或通过useState更新一个无关状态(函数组件)。

  4. 上下文变化:组件使用的Context.Provider值变化,会触发所有消费该Context的组件重新渲染。

5.2 完整重新渲染流程(基于Fiber架构)

以函数组件useState更新为例,流程如下(涉及react、react-reconciler、react-scheduler、react-dom协同):

  1. 触发更新:调用useState返回的dispatchSetState,将更新操作加入对应Hook的更新队列,调用react-reconciler的scheduleUpdateOnFiber,触发调度。

  2. 任务调度:react-scheduler根据更新优先级,将任务加入调度队列,等待主线程空闲(时间切片)。

  3. 调度阶段(Reconciliation)

    1. react-reconciler创建WorkInProgress树(基于Current树拷贝),从根Fiber开始遍历,执行beginWork。

    2. 遍历到触发更新的组件Fiber节点,处理Hook更新队列,计算新状态,更新Hook的memoizedState。

    3. 执行Diff算法,对比新旧虚拟DOM,标记Fiber节点的更新类型(新增/删除/修改),收集副作用。

    4. 若有高优先级任务或时间切片超时,中断遍历,待空闲后恢复;否则继续遍历至所有节点处理完成。

  4. 提交阶段(Commit)

    1. before mutation阶段:执行getSnapshotBeforeUpdate(类组件),清理上一轮useEffect副作用。

    2. mutation阶段:react-dom执行DOM操作(根据Fiber节点的副作用标记),更新真实DOM。

    3. layout阶段:更新Current树(root.current = workInProgress),触发componentDidMount/Update(类组件)、useLayoutEffect回调,执行useEffect回调(异步)。

  5. 完成渲染:浏览器绘制更新后的DOM,组件呈现新状态。

5.3 批量更新机制

React默认在“合成事件回调”“生命周期函数”“useEffect回调”中开启批量更新,即多次状态更新合并为一次渲染,减少DOM操作次数,提升性能。

实现原理:react-dom维护一个“更新批次”标志,在上述场景中,将更新加入队列,待回调执行完成后,统一触发调度和渲染;若用flushSync包裹更新,会强制立即执行更新,打破批量更新。

5.4 高频面试题

  1. Q:React组件重新渲染的完整流程是什么?涉及哪些核心包? A:流程:触发更新→任务调度→调度阶段(调和Fiber树、Diff、收集副作用)→提交阶段(执行副作用、更新DOM、触发回调)。涉及包:react(API定义)、react-reconciler(调和)、react-scheduler(调度)、react-dom(DOM操作)。

  2. Q:父组件重新渲染,子组件一定会重新渲染吗?为什么? A:不一定。若子组件用memo包裹,且props浅比较不变,则子组件不会重新渲染;若未用memo,即使props不变,子组件也会跟随父组件重新渲染(React默认行为)。

  3. Q:如何打破React的批量更新? A:用flushSync包裹状态更新,强制立即执行更新;或在异步回调(如setTimeout、Promise.then)中触发更新,React 18前异步回调中不开启批量更新(React 18后通过createRoot开启全局批量更新,异步回调也支持)。

六、Hooks的意义

6.1 解决函数组件的局限性

React早期函数组件是“无状态组件”,仅能接收props渲染UI,Hooks使函数组件具备:

  • 状态管理能力(useState、useReducer)。

  • 副作用处理能力(useEffect、useLayoutEffect)。

  • 生命周期等价能力(useEffect模拟componentDidMount/Update/Unmount)。

  • DOM操作能力(useRef)。

6.2 简化状态逻辑复用

传统状态逻辑复用依赖高阶组件(HOC)、Render Props,但会导致“组件嵌套地狱”,代码可读性差。Hooks通过“自定义Hooks”实现逻辑复用,无需修改组件结构,代码更简洁。

示例:自定义useRequest Hooks封装请求逻辑,多个组件可直接调用,复用请求状态(加载中、成功、失败)和回调。

6.3 统一组件模型,降低学习成本

Hooks使函数组件成为React的主流组件模型,替代类组件的复杂生命周期、this绑定、状态管理逻辑,减少“类组件与函数组件并存”的心智负担,新开发者可快速上手。

6.4 更好地支持并发模式

函数组件更适合并发模式的可中断渲染:无类组件的this状态依赖,Hooks链表的状态存储更易被中断和恢复,配合useTransition等并发API,能更好地实现高性能交互。

6.5 高频面试题

  1. Q:Hooks相比类组件有哪些优势? A:① 简化状态逻辑复用(自定义Hooks替代HOC/Render Props);② 消除类组件的this绑定、生命周期嵌套问题;③ 函数组件更简洁,学习成本低;④ 更好地支持并发模式。

  2. Q:自定义Hooks的核心原则是什么?如何实现一个自定义Hooks? A:核心原则:① 命名以use开头;② 内部可调用其他Hooks;③ 仅用于逻辑复用,不返回JSX。实现:封装通用逻辑(如请求、防抖),通过Hooks管理状态和副作用,暴露结果和操作函数。

七、性能优化

7.1 组件级优化

7.1.1 React.memo(函数组件)/PureComponent(类组件)

作用:浅比较props,若props未变化,阻止组件重新渲染。

实现原理:memo接收组件和自定义比较函数(可选),返回一个包装组件,重新渲染前对比新旧props,浅比较相等则跳过调和阶段。

注意:若props包含引用类型(对象、数组),浅比较可能失效,需配合useMemo/useCallback优化。

7.1.2 useMemo(缓存计算结果)

作用:缓存耗时计算的结果,依赖不变时复用结果,避免每次渲染重复计算。

原理:将计算逻辑和依赖数组传入useMemo,重新渲染时若依赖不变,返回缓存结果;依赖变化则重新计算并缓存。

注意:仅用于耗时计算,普通变量无需缓存,避免额外性能开销。

7.1.3 useCallback(缓存函数引用)

作用:缓存函数引用,避免每次渲染生成新函数,导致子组件(接收该函数作为props)不必要的重新渲染。

原理:与useMemo类似,依赖不变时复用函数引用,配合memo使用,确保子组件props浅比较不变。

7.2 渲染流程优化

7.2.1 避免不必要的状态更新

  • setState时对比新旧状态,相同则不触发更新(类组件需手动判断,函数组件useState会自动忽略相同值)。

  • 拆分状态:将不相关的状态拆分为多个useState,避免一个状态更新导致无关逻辑重新执行。

7.2.2 合理使用并发API

用useTransition将低优先级更新(如列表筛选、数据加载)标记为过渡更新,避免阻塞高优先级任务(输入、点击),提升交互流畅度。

7.2.3 虚拟列表(长列表优化)

对于万级以上数据的长列表,直接渲染所有节点会导致Fiber树过大、调和耗时,需用虚拟列表(如react-window、react-virtualized):仅渲染当前可视区域的节点,滚动时动态复用节点,减少DOM数量和调和成本。

7.3 副作用优化

7.3.1 精准控制useEffect依赖

useEffect依赖数组需精准配置,避免依赖冗余(导致不必要的副作用执行)或缺失(导致闭包陷阱,获取旧状态)。

闭包陷阱解决:用useRef存储最新状态,或拆分useEffect为多个,分别控制依赖。

7.3.2 清理副作用

在useEffect回调中返回清理函数,清除定时器、事件监听、网络请求等,避免内存泄漏(如组件卸载前取消未完成的请求)。

7.4 其他优化手段

  • 代码分割:用React.lazy和Suspense实现组件懒加载,减少首屏加载体积,提升首屏渲染速度。

  • Context优化:拆分Context为多个细粒度Context,避免一个Context变化导致所有消费组件重新渲染。

  • 避免使用index作为key:列表渲染时,用唯一ID作为key,确保Diff算法正确复用节点,避免不必要的DOM更新。

7.5 高频面试题

  1. Q:useMemo和useCallback的区别和使用场景? A:useMemo缓存计算结果,用于耗时计算场景;useCallback缓存函数引用,用于避免子组件因函数props变化而不必要渲染的场景。两者均依赖数组控制缓存失效。

  2. Q:React.memo的局限性是什么?如何解决? A:局限性:仅浅比较props,若props包含引用类型,浅比较可能失效。解决:配合useMemo/useCallback缓存引用类型props,确保新旧props浅比较相等。

  3. Q:长列表优化的核心思路是什么?常用方案有哪些? A:核心思路:减少渲染的节点数量,避免Fiber调和和DOM操作耗时。方案:虚拟列表(react-window)、分页加载、按需渲染。

  4. Q:如何排查React组件的不必要渲染? A:① 用React DevTools的Highlight Updates功能,标记重新渲染的组件;② 在组件中打印日志,判断是否触发不必要渲染;③ 检查props是否变化、状态更新是否必要,针对性用memo/useMemo/useCallback优化。

八、虚拟DOM原理

8.1 核心定义与价值

虚拟DOM(Virtual DOM)是用JS对象模拟真实DOM的结构和属性,作为React的“中间层”,核心价值:

  • 跨平台适配:虚拟DOM与宿主环境无关,react-dom(浏览器)、react-native(移动端)可基于虚拟DOM生成对应平台的真实节点。

  • 减少DOM操作:通过Diff算法对比新旧虚拟DOM,仅更新变化的部分,避免全量DOM更新(DOM操作是昂贵的)。

  • 抽象渲染逻辑:将DOM操作抽象为虚拟DOM的更新,简化组件渲染逻辑。

8.2 虚拟DOM的结构(React Element)

React.createElement创建的React Element就是虚拟DOM的核心结构,简化版如下:

const ReactElement = {
  $$typeof: Symbol.for('react.element'), // 标记为React元素
  type: 'div', // 元素类型(DOM标签名/组件)
  key: null, // 用于Diff复用
  ref: null, // 用于获取DOM元素
  props: { children: [], className: '' }, // 元素属性和子元素
  _owner: null, // 所属组件
};

函数组件返回的JSX会被Babel编译为React.createElement调用,生成虚拟DOM树。

8.3 Diff算法:虚拟DOM的核心

Diff算法是虚拟DOM的核心,目标是“高效对比新旧虚拟DOM树,找出最小更新集”,React Diff基于三大假设(减少对比复杂度):

  1. 同层节点对比:只对比同一层级的虚拟DOM节点,不跨层级对比(若节点跨层级移动,直接删除旧节点,创建新节点)。

  2. 类型相同复用节点:若新旧节点type相同、key相同,复用旧节点,仅更新props;类型不同则直接替换。

  3. key唯一标识节点:列表渲染时,key用于标识节点的唯一性,帮助Diff算法快速找到可复用节点,避免错位。

8.3.1 Diff算法流程(基于Fiber)

  1. 调和阶段(beginWork)中,针对每个Fiber节点,获取新旧虚拟DOM(oldProps/newProps对应的React Element)。

  2. 对比新旧虚拟DOM:

    1. 若type不同:标记旧节点为删除,创建新Fiber节点替换。

    2. 若type相同、key相同:复用旧Fiber节点,更新props,递归对比子节点。

  3. 子节点对比(列表Diff):

    1. 无key时:按索引对比,若列表顺序变化,会导致大量节点更新(效率低)。

    2. 有key时:通过key建立新旧子节点的映射,快速找到可复用节点,仅移动位置或更新变化节点(效率高)。

8.4 虚拟DOM与Fiber的关系

虚拟DOM是“数据描述”,Fiber是“工作单元”,两者协同工作:

  • 虚拟DOM描述组件的期望状态(what to render)。

  • Fiber基于虚拟DOM创建/更新工作单元,执行Diff算法,调度渲染任务(how to render)。

  • 调和阶段,Fiber节点的更新依赖虚拟DOM的Diff结果,最终将虚拟DOM的变化转化为真实DOM操作。

8.5 高频面试题

  1. Q:虚拟DOM的核心价值是什么?为什么虚拟DOM能提升性能? A:核心价值:跨平台、减少DOM操作、抽象渲染逻辑。性能提升原因:通过Diff算法找到最小更新集,避免全量DOM更新(DOM操作耗时远高于JS计算)。

  2. Q:React Diff算法的三大假设是什么?key的作用是什么? A:三大假设:同层对比、类型相同复用、key唯一标识。key的作用:帮助Diff算法快速建立新旧节点映射,复用节点,避免列表渲染错位,提升Diff效率。

  3. Q:虚拟DOM和真实DOM的区别? A:① 虚拟DOM是JS对象,存储在内存中,操作成本低;真实DOM是浏览器渲染树节点,操作成本高。② 虚拟DOM与宿主环境无关,可跨平台;真实DOM依赖具体宿主环境(浏览器)。③ 虚拟DOM是中间层,需通过Diff和提交阶段转化为真实DOM。

九、底层数据结构与算法汇总

9.1 核心数据结构

  • 链表:Fiber树(return/child/sibling)、Hooks链表(next),支持中断后恢复、顺序遍历。

  • 队列:Hook更新队列(useState/useReducer的更新任务)、调度任务队列(react-scheduler的优先级任务队列),采用先进先出(FIFO)原则。

  • 哈希表:列表Diff时,通过key建立新旧子节点的哈希映射,快速查找可复用节点。

  • :Fiber树(Current/WorkInProgress)、虚拟DOM树,用于描述组件层级结构。

9.2 核心算法

  • 深度优先遍历(DFS):Fiber树调和阶段的遍历方式,通过child→sibling→return指针实现非递归DFS,支持中断。

  • Diff算法:基于三大假设的同层对比算法,最小化DOM更新。

  • 时间切片算法:react-scheduler基于requestIdleCallback实现,控制任务执行时长,避免阻塞主线程。

  • 优先级调度算法:react-scheduler基于任务优先级和超时时间,调度任务执行顺序,高优先级任务优先执行。

十、四大核心包协同逻辑总结

  1. react:定义核心API(useState、useEffect、React.createElement等),描述组件和状态逻辑。

  2. react-reconciler:基于react定义的API,构建Fiber树,执行调和逻辑(Diff、副作用收集),是渲染的核心协调器。

  3. react-scheduler:为react-reconciler提供任务调度和时间切片能力,实现优先级驱动的并发渲染。

  4. react-dom:适配浏览器环境,在提交阶段执行真实DOM操作,触发生命周期/Hooks回调,将Fiber调和结果转化为可视UI。

四大包协同:react定义“做什么”,react-reconciler+react-scheduler决定“如何高效做”,react-dom负责“最终落地到浏览器”。