Skip to content

[toc]

一、 深度理解 React Fiber

Fiber 是 React 16 引入的全新架构,旨在解决 V15 在处理大型组件树时产生的卡顿问题

1. 解决的问题

在 React V15 中,更新过程是同步且不可中断的。一旦开始比对 Virtual DOM,浏览器的主线程就会被一直占用,无法响应用户的输入或动画,导致掉帧和卡顿

2. 核心思想:可中断的异步渲染

Fiber 引入了“时间切片(Time Slicing)”的概念:

  • 任务拆分:将大的更新任务拆解为许多微小的“工作单元”
  • 优先级调度:浏览器在每一帧的空闲时间(requestIdleCallback)执行这些单元
  • 让出控制权:如果此时有高优先级的任务(如用户点击、输入),React 会暂停渲染,优先响应用户,等浏览器空闲后再恢复执行

二、 高阶组件(HOC)

高阶组件(HOC) 是参数为组件,返回值为新组件的函数。它是一种基于 React 组合特性的设计模式,而非 API。

1. 核心应用场景

  • 权限控制:通过 HOC 包裹页面,统一判断用户是否有权访问
  • 渲染劫持:根据 props 动态决定是否渲染原组件或渲染替代内容
  • 逻辑复用:将通用的数据获取(Fetching)或埋点统计逻辑抽离

2. HOC 的实现方式:属性代理 vs 反向继承

  • 属性代理:通过返回一个新的类组件来包裹原组件,可以操作 props
  • 反向继承:返回一个继承自 WrappedComponent 的类,可以访问原组件的 state 和生命周期(如上文中的 withTiming 例子)

三、 受控组件与非受控组件

在处理表单数据时,React 提供了两种不同的模型:

特性受控组件 (Controlled)非受控组件 (Uncontrolled)
数据源React 的 state真实的 DOM 节点
获取值state 中读取通过 ref 获取
优点数据流清晰,易于表单校验和即时反馈代码量少,方便集成第三方非 React 库
推荐度官方推荐(符合 React 单向数据流)仅在简单场景或集成旧库时使用

四、 重新渲染(Re-render)触发机制

React 决定是否更新 DOM 的流程如下:

  1. 触发源setState (非 null)、父组件重新渲染、forceUpdate
  2. 执行 Render:生成新的虚拟 DOM 树
  3. Diff 算法:深度优先遍历新旧树,找出差异(Patches)
  4. Commit 阶段:将差异最小代价地应用到真实 DOM

五、 类组件 vs 函数组件

随着 Hooks 的出现,函数组件已经成为了 React 开发的主流选择

1. 类组件 (Class)

  • 基于面向对象,拥有显式的生命周期(componentDidMount 等)
  • 通过 this 访问实例,逻辑复用主要靠 HOC 或 Render Props

2. 函数组件 (Function)

  • 基于函数式编程,心智模型更简单
  • 通过 Hooks 实现状态管理和副作用处理,逻辑复用更加细粒度
  • 未来趋势:并发模式(Concurrent Mode)对函数组件更友好,且性能优化更方便(React.memo, useMemo
特性类组件 (Class)函数组件 (Function)
逻辑复用使用 HOC (高阶组件) 或 Render Props使用 Custom Hooks (更简洁)
状态管理this.state / this.setStateuseState Hook
生命周期具体的生命周期钩子 (如 componentDidMount)useEffect 统一处理副作用
性能实例化开销略大轻量,闭包机制

this 绑定问题:

类组件需要频繁地使用 .bind(this) 或者箭头函数来确保 this 指向正确。而函数组件完全没有 this 的概念,数据通过作用域直接获取

生命周期方法

类组件有明确的生命周期方法,函数组件通过 useEffect() Hook 来替代生命周期方法

六、setState 到底是同步还是异步?

结论: 在 React 的设计中,setState 本身的代码执行是同步的,但它引起的状态更新和组件渲染在不同场景下表现出“异步”或“同步”的特征

1. 为什么表现为“异步”?(合成事件与生命周期)

在 React 可以管理的场景中(如 onClick 合成事件、componentDidMount 生命周期),React 会开启批量更新

  • 原理:React 在进入事件处理函数前,会将 isBatchingUpdates 标记为 true。此时所有的 setState 都会被放入一个队列中暂存,等函数执行完毕后,再统一合并 state 并触发一次 render
  • 目的:性能优化,避免频繁的 Diff 和 DOM 操作

2. 为什么表现为“同步”?(脱离 React 控制)

在 React 无法“监控”到的地方,批量更新机制会失效:

  • 原生 DOM 事件:使用 addEventListener 绑定的事件
  • 异步宏任务setTimeoutsetIntervalPromise.then
  • 原因:当这些异步代码执行时,React 的同步执行上下文已经结束,isBatchingUpdates 已被重置为 false,所以每次 setState 都会立即触发更新

注意:React 18 中,引入了 Automatic Batching(自动批处理)。无论是在 setTimeout 还是原生事件中,React 默认都会进行异步批处理

七、深度对比:state vs props

这两者共同构成了 React 的数据驱动模型,但它们的心智模型完全不同

维度State (内部状态)Props (外部属性)
来源组件自身内部定义父组件传递
可变性可变(通过 setState只读(不可在子组件修改)
所属权属于组件私有属于父组件
触发更新调用 setState 触发父组件传入新的 props 触发
类比函数内部声明的变量函数的形参

八、为什么 props 必须是只读的?

React 极力推崇函数式编程单向数据流props 只读是这一思想的体现

  1. 保证单向数据流:所有数据变化都能追溯到父组件,形成清晰的数据流向
  2. 避免副作用:组件不会意外修改外部数据,确保组件行为可预测
  3. 支持性能优化:props不可变,React才能安全进行浅比较,实现高效渲染

九、组件通信

1. 父子组件通信

这是最频繁、最直接的通信方式。

父传子:Props

父组件通过属性(Attributes)向下传递数据

javascript
const Child = ({ name }) => <p>项目名称:{name}</p>;

const Parent = () => <Child name="React Pro" />;

子传父:回调函数

父组件通过 props 传递一个函数给子组件,子组件通过调用该函数并传入参数,实现数据的“逆流”

JavaScript
const Child = ({ onShowMsg }) => (
  <button onClick={() => onShowMsg('来自子的问候')}>点击汇报</button>
);

const Parent = () => {
  const handleMsg = (msg) => console.log(msg);
  return <Child onShowMsg={handleMsg} />;
};

2. 跨级组件通信 (Context)

当嵌套层级过深时,逐层传递(Props Drilling)会导致中间组件充斥着大量无用属性。使用 Context 可以实现“跳跃式”传递

现代 Hooks 写法示例:

javascript
import React, { createContext, useContext } from 'react';

const ConfigContext = createContext();

const DeepGrandChild = () => {
  // 使用 useContext 直接获取,无需 Consumer 包裹
  const config = useContext(ConfigContext);
  return <div>配置项:{config.theme}</div>;
};

const App = () => (
  <ConfigContext.Provider value={{ theme: 'Dark' }}>
    <IntermediateComponent />
  </ConfigContext.Provider>
);

3. 水平维度:兄弟组件通信

由于 React 是单向数据流,兄弟组件之间没有直接联系

  • 状态提升(Lifting State Up):将共同的数据提升到它们最近的共同父组件中管理
  • 流程:A 组件触发父组件的回调修改状态 -> 父组件状态更新重新渲染 -> 新状态通过 Props 下发给 B 组件

4. 全局维度:非嵌套/复杂组件通信

当应用变得庞大,组件关系复杂时,需要引入第三方方案

方案特点适用场景
状态管理 (Redux/Zustand)维护单一事实来源(Store),逻辑集中大型应用、多组件共享复杂状态
发布订阅 (Event Bus)通过 EventEmitter 手动触发和监听简单的跨级消息、不希望引入沉重状态库

5. 如何解决 Props 层级过深?(深度总结)

  1. 组件组合 (Component Composition):将子组件作为 children 传入,或者将子组件直接在父组件实例化后作为 prop 传入
  2. Context API:React 原生支持,适合主题、用户信息等轻量级全局数据
  3. 状态管理库:如 Redux 或 MobX,适合频繁变动、多处共享的业务数据

十、useEffect与useLayoutEffect

两者的根本区别在于 “浏览器重绘(Repaint)” 发生的时机

1. 执行时机对比

  • useLayoutEffect
    • 时机:在 DOM 更新之后,浏览器绘制之前同步执行
    • 特点:它会阻塞浏览器的绘制。如果你在其中修改了 DOM 样式,浏览器会等待该任务完成后再进行一次性绘制
    • 场景:防止视觉闪烁(如测量 DOM 尺寸并立即调整位置)
  • useEffect
    • 时机:在浏览器完成绘制(屏幕上已经看到画面)之后异步执行
    • 特点:不会阻塞渲染,性能更好
    • 场景:绝大多数副作用(数据请求、事件监听、日志记录)

十一、 Hooks 与生命周期的“映射图谱”

函数组件没有生命周期方法,它拥有的是 “同步数据到副作用” 的能力。我们可以通过 useEffect 的依赖数组(Dependency Array)来模拟不同的生命周期

核心映射表

类组件生命周期Hooks 实现方式关键点
constructor函数体 / useState 初始值仅在组件首次调用时运行逻辑
render函数体本身纯函数,不应包含副作用
componentDidMountuseEffect(fn, [])依赖为空数组,表示仅在挂载时运行一次
componentDidUpdateuseEffect(fn, [deps])当依赖项变化时触发
componentWillUnmountuseEffect 返回的清理函数在组件卸载或下一次副作用执行前运行
shouldComponentUpdateReact.memo对 props 进行浅比较优化

十二、 虚拟 DOM (VDOM)

1. 本质是什么?

虚拟 DOM 本质上是一个 轻量级的 JavaScript 对象。它用 tagpropschildren 等属性来描述真实 DOM 的结构,通过事务处理机制,将多次DOM修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改DOM的重绘重排次数,提高渲染性能

2. 核心价值

  • 研发效率:开发者只需关注数据状态(State),无需手动处理复杂的 DOM 增删改查
  • 保证性能下限:框架通过 Diff 算法,确保在大多数情况下都能提供“足够快”的更新,避免了初级开发者写出极其低效的 DOM 操作
  • 跨平台能力:由于 VDOM 是 JS 对象,它可以被渲染到浏览器(ReactDOM)、移动端原生界面(React Native)或服务端(SSR)

十三、虚拟 DOM 到真实渲染的完整流水线

1. 映射阶段:从真实到虚拟(初始化)

当组件第一次渲染(Mount)时,React 会执行 render 函数(或函数组件体),生成一棵 虚拟 DOM 树

  • 本质:这个过程是把声明式的 JSX 转换为嵌套的 JavaScript 对象
  • 跨平台基础:正是因为有了这一层“对象映射”,React 才能通过不同的渲染器(Renderer)将同一个对象渲染到浏览器(ReactDOM)、手机原生应用(ReactNative)或 Canvas 中

2. Diff 阶段:寻找差异(计算 Patch)

当数据(State/Props)发生变化时,React 会生成一棵 新的虚拟 DOM 树。此时,Diff 算法开始介入,对比 旧树(Old Tree)新树(New Tree)

  • 深度优先遍历:React 会从根节点开始,采用深度优先遍历的方式比对两棵树。

  • 生成 Patch 对象:比对过程中,如果发现节点属性变了、节点删除了或位置换了,就会把这些差异记录在一个 Patch(补丁)对象 中。

    Patch 的结构示例:

    JSON

    {
      type: "REPLACE", // 替换节点
      node: newNode,
      index: 2
    }

3. Patch 阶段:应用更新(同步到真实 DOM)

这是性能优化的关键一步。React 不会发现一个差异就改一次 DOM,而是:

  • 批量更新(Batching):收集完所有的 Patch 后,在一次 DOM 操作中完成所有的修改
  • 最小化重绘重排:例如,如果你只是改了颜色,Patch 只会触发重绘(Repaint);如果你删除了节点,才会触发重排(Reflow)

十四、虚拟 DOM 的引入与直接操作原生 DOM 相比,哪一个效率更高,为什么?

虚拟DOM首次渲染时需要构建虚拟DOM树,并进行一次完整的diff和patch,计算量高于直接操作真实DOM,导致虚拟DOM初始渲染时间远大于真实DOM。因此在更新频率低、DOM结构简单的页面下真实DOM性能更好、更快

虚拟DOM的优势:

1. 减少直接DOM操作次数:在内存中进行diff比较,只将最小差异应用到真实DOM,避免频繁的重排和重绘 
2. 批量更新机制:多个状态变更可以合并为一次真实 DOM 更新,减少浏览器渲染次数

十五、React 与 Vue 的 diff 算法有何不同?

维度React (Fiber)Vue (2.x / 3)
遍历顺序从左到右,单向遍历头尾都有指针,双向夹逼
主要算法基于 lastPlacedIndex 的右移算法双端比较算法
Key 的必要性极高。没有 Key 会默认使用索引,导致列表更新出现 Bug 或性能下降较高。没有 Key 会使用就地复用策略(可能引发状态错乱)
编译时/运行时纯运行时。Diff 是在运行时发生的,不知道模板结构编译时 + 运行时。Vue 通过编译模板,可以分析出静态节点,跳过 Diff(静态提升)
列表移动优化对“将节点移动到后面”友好,对“将节点移动到前面”相对吃力对头/尾节点的移动非常友好,优化程度更高

十六、React 与 Vue 之间的异同

维度ReactVue
设计思想函数式编程。强调不可变数据(Immutable)。响应式编程。通过监听数据变化自动更新。
数据流严格的单向数据流。默认单向,支持 v-model 双向绑定。
视图编写JSX。本质是 JS,拥有全量的编程能力。Template。近似 HTML,更符合传统开发习惯。
性能优化手动优化(memo, useMemo)以跳过 Diff。自动优化(依赖收集)。精确追踪组件更新。
逻辑复用Hooks(早期为 HOC)。Composition API(早期为 Mixins)。

十七、React的状态提升是什么?使用场景有哪些?

概括来说就是将多个组件需要共享的状态提升到它们最近的父组件上在父组件上改变这个状态然后通过props分发给子组件。

当多个组件需要反映相同的变化数据时,建议将共享状态提升到它们最近的共同父组件中去。

交互流程:

  1. 子组件 A 触发事件(如 onChange
  2. 调用从 父组件 传下来的回调函数(如 onValueChange
  3. 父组件 执行 setState 修改状态。
  4. 父组件 重新渲染,将新状态通过 props 分发给 子组件 A子组件 B

十八、React 中的集合遍历

在 React 中,我们不使用 v-for,而是回归 JavaScript 原生方法

1. 数组遍历(推荐 map

map 会返回一个新数组,直接在 JSX 中展开:

javascript
const List = ({ items }) => (
  <ul>
    {items.map(item => <li key={item.id}>{item.text}</li>)}
  </ul>
);

2. 对象遍历

通常使用 Object.keys()Object.entries() 将对象转为数组后再遍历:

javascript
const UserInfo = ({ user }) => (
  <ul>
    {Object.entries(user).map(([key, value]) => (
      <li key={key}>{key}: {value}</li>
    ))}
  </ul>
);

注意: 永远不要忘记加 key,且尽量避免使用 index 作为 key,这会严重影响 Diff 算法的性能。

十九、React SSR(服务端渲染)

SSR 的本质是 “空间换时间”:用服务器的 CPU 算力换取用户的首屏加载速度

1. 渲染流程对比

  • CSR (客户端渲染):浏览器下载一个空的 HTML -> 下载巨大的 JS -> 执行 JS 请求数据 -> 渲染内容
  • SSR (服务端渲染):服务器请求数据 -> 将组件渲染为 HTML 字符串 -> 浏览器直接显示 -> 激活(Hydration)JS

2. 优缺点权衡

  • 优势
    • SEO 友好:爬虫能直接读到完整的 HTML 内容
    • 首屏极快:用户不需要等待庞大的 JS 加载完就能看到页面
  • 劣势
    • 服务器压力:高并发下服务器计算成本高
    • 开发复杂度:代码需兼顾 Node.js 和 Browser 环境(例如服务端没有 window 对象)