文章目录
-
- [一、 深度理解 React Fiber](#一、 深度理解 React Fiber)
-
- [1. 解决的问题](#1. 解决的问题)
- [2. 核心思想:可中断的异步渲染](#2. 核心思想:可中断的异步渲染)
- [二、 高阶组件(HOC)](#二、 高阶组件(HOC))
-
- [1. 核心应用场景](#1. 核心应用场景)
- [2. HOC 的实现方式:属性代理 vs 反向继承](#2. HOC 的实现方式:属性代理 vs 反向继承)
- [三、 受控组件与非受控组件](#三、 受控组件与非受控组件)
- [四、 重新渲染(Re-render)触发机制](#四、 重新渲染(Re-render)触发机制)
- [五、 类组件 vs 函数组件](#五、 类组件 vs 函数组件)
-
- [1. 类组件 (Class)](#1. 类组件 (Class))
- [2. 函数组件 (Function)](#2. 函数组件 (Function))
- [六、`setState` 到底是同步还是异步?](#六、
setState到底是同步还是异步?) -
- [1. 为什么表现为"异步"?(合成事件与生命周期)](#1. 为什么表现为“异步”?(合成事件与生命周期))
- [2. 为什么表现为"同步"?(脱离 React 控制)](#2. 为什么表现为“同步”?(脱离 React 控制))
- [七、深度对比:`state` vs `props`](#七、深度对比:
statevsprops) - [八、为什么 `props` 必须是只读的?](#八、为什么
props必须是只读的?) - 九、组件通信
- 十、useEffect与useLayoutEffect
-
- [1. 执行时机对比](#1. 执行时机对比)
- [十一、 Hooks 与生命周期的"映射图谱"](#十一、 Hooks 与生命周期的“映射图谱”)
- [十二、 虚拟 DOM (VDOM)](#十二、 虚拟 DOM (VDOM))
-
- [1. 本质是什么?](#1. 本质是什么?)
- [2. 核心价值](#2. 核心价值)
- [十三、虚拟 DOM 到真实渲染的完整流水线](#十三、虚拟 DOM 到真实渲染的完整流水线)
-
- [1. 映射阶段:从真实到虚拟(初始化)](#1. 映射阶段:从真实到虚拟(初始化))
- [2. Diff 阶段:寻找差异(计算 Patch)](#2. Diff 阶段:寻找差异(计算 Patch))
- [3. Patch 阶段:应用更新(同步到真实 DOM)](#3. Patch 阶段:应用更新(同步到真实 DOM))
- [十四、虚拟 DOM 的引入与直接操作原生 DOM 相比,哪一个效率更高,为什么?](#十四、虚拟 DOM 的引入与直接操作原生 DOM 相比,哪一个效率更高,为什么?)
- [十五、React 与 Vue 的 diff 算法有何不同?](#十五、React 与 Vue 的 diff 算法有何不同?)
- [十六、React 与 Vue 之间的异同](#十六、React 与 Vue 之间的异同)
- 十七、React的状态提升是什么?使用场景有哪些?
- [十八、React 中的集合遍历](#十八、React 中的集合遍历)
-
- [1. 数组遍历(推荐 `map`)](#1. 数组遍历(推荐
map)) - [2. 对象遍历](#2. 对象遍历)
- [1. 数组遍历(推荐 `map`)](#1. 数组遍历(推荐
- [十九、React SSR(服务端渲染)](#十九、React SSR(服务端渲染))
-
- [1. 渲染流程对比](#1. 渲染流程对比)
- [2. 优缺点权衡](#2. 优缺点权衡)
一、 深度理解 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 的流程如下:
- 触发源 :
setState(非 null)、父组件重新渲染、forceUpdate - 执行 Render:生成新的虚拟 DOM 树
- Diff 算法:深度优先遍历新旧树,找出差异(Patches)
- Commit 阶段:将差异最小代价地应用到真实 DOM
五、 类组件 vs 函数组件
随着 Hooks 的出现,函数组件已经成为了 React 开发的主流选择
1. 类组件 (Class)
- 基于面向对象,拥有显式的生命周期(
componentDidMount等) - 通过
this访问实例,逻辑复用主要靠 HOC 或 Render Props
2. 函数组件 (Function)
- 基于函数式编程,心智模型更简单
- 通过 Hooks 实现状态管理和副作用处理,逻辑复用更加细粒度
- 未来趋势 :并发模式(Concurrent Mode)对函数组件更友好,且性能优化更方便(
React.memo,useMemo)
六、setState 到底是同步还是异步?
结论: 在 React 的设计中,setState 本身的代码执行是同步 的,但它引起的状态更新和组件渲染在不同场景下表现出"异步"或"同步"的特征
1. 为什么表现为"异步"?(合成事件与生命周期)
在 React 可以管理的场景中(如 onClick 合成事件、componentDidMount 生命周期),React 会开启批量更新
- 原理 :React 在进入事件处理函数前,会将
isBatchingUpdates标记为true。此时所有的setState都会被放入一个队列中暂存,等函数执行完毕后,再统一合并 state 并触发一次render - 目的:性能优化,避免频繁的 Diff 和 DOM 操作
2. 为什么表现为"同步"?(脱离 React 控制)
在 React 无法"监控"到的地方,批量更新机制会失效:
- 原生 DOM 事件 :使用
addEventListener绑定的事件 - 异步宏任务 :
setTimeout、setInterval、Promise.then等 - 原因 :当这些异步代码执行时,React 的同步执行上下文已经结束,
isBatchingUpdates已被重置为false,所以每次setState都会立即触发更新
注意: 在 React 18 中,引入了 Automatic Batching(自动批处理) 。无论是在
setTimeout还是原生事件中,React 默认都会进行异步批处理
七、深度对比:state vs props
这两者共同构成了 React 的数据驱动模型,但它们的心智模型完全不同
| 维度 | State (内部状态) | Props (外部属性) |
|---|---|---|
| 来源 | 组件自身内部定义 | 父组件传递 |
| 可变性 | 可变 (通过 setState) |
只读(不可在子组件修改) |
| 所属权 | 属于组件私有 | 属于父组件 |
| 触发更新 | 调用 setState 触发 |
父组件传入新的 props 触发 |
| 类比 | 函数内部声明的变量 | 函数的形参 |
八、为什么 props 必须是只读的?
React 极力推崇函数式编程 和单向数据流 ,props 只读是这一思想的体现
- 保证单向数据流:所有数据变化都能追溯到父组件,形成清晰的数据流向
- 避免副作用:组件不会意外修改外部数据,确保组件行为可预测
- 支持性能优化: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 层级过深?(深度总结)
- 组件组合 (Component Composition) :将子组件作为
children传入,或者将子组件直接在父组件实例化后作为 prop 传入 - Context API:React 原生支持,适合主题、用户信息等轻量级全局数据
- 状态管理库:如 Redux 或 MobX,适合频繁变动、多处共享的业务数据
十、useEffect与useLayoutEffect
两者的根本区别在于 "浏览器重绘(Repaint)" 发生的时机
1. 执行时机对比
useLayoutEffect:- 时机:在 DOM 更新之后,浏览器绘制之前同步执行
- 特点:它会阻塞浏览器的绘制。如果你在其中修改了 DOM 样式,浏览器会等待该任务完成后再进行一次性绘制
- 场景:防止视觉闪烁(如测量 DOM 尺寸并立即调整位置)
useEffect:- 时机:在浏览器完成绘制(屏幕上已经看到画面)之后异步执行
- 特点:不会阻塞渲染,性能更好
- 场景:绝大多数副作用(数据请求、事件监听、日志记录)
十一、 Hooks 与生命周期的"映射图谱"
函数组件没有生命周期方法,它拥有的是 "同步数据到副作用" 的能力。我们可以通过 useEffect 的依赖数组(Dependency Array)来模拟不同的生命周期
核心映射表
| 类组件生命周期 | Hooks 实现方式 | 关键点 |
|---|---|---|
constructor |
函数体 / useState 初始值 |
仅在组件首次调用时运行逻辑 |
render |
函数体本身 | 纯函数,不应包含副作用 |
componentDidMount |
useEffect(fn, []) |
依赖为空数组,表示仅在挂载时运行一次 |
componentDidUpdate |
useEffect(fn, [deps]) |
当依赖项变化时触发 |
componentWillUnmount |
useEffect 返回的清理函数 |
在组件卸载或下一次副作用执行前运行 |
shouldComponentUpdate |
React.memo |
对 props 进行浅比较优化 |
十二、 虚拟 DOM (VDOM)
1. 本质是什么?
虚拟 DOM 本质上是一个 轻量级的 JavaScript 对象 。它用 tag、props 和 children 等属性来描述真实 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 之间的异同
| 维度 | React | Vue |
|---|---|---|
| 设计思想 | 函数式编程。强调不可变数据(Immutable)。 | 响应式编程。通过监听数据变化自动更新。 |
| 数据流 | 严格的单向数据流。 | 默认单向,支持 v-model 双向绑定。 |
| 视图编写 | JSX。本质是 JS,拥有全量的编程能力。 | Template。近似 HTML,更符合传统开发习惯。 |
| 性能优化 | 手动优化(memo, useMemo)以跳过 Diff。 |
自动优化(依赖收集)。精确追踪组件更新。 |
| 逻辑复用 | Hooks(早期为 HOC)。 | Composition API(早期为 Mixins)。 |
十七、React的状态提升是什么?使用场景有哪些?
概括来说就是将多个组件需要共享的状态提升到它们最近的父组件上 ,在父组件上改变这个状态然后通过props分发给子组件。
当多个组件需要反映相同的变化数据时,建议将共享状态提升到它们最近的共同父组件中去。
交互流程:
- 子组件 A 触发事件(如
onChange) - 调用从 父组件 传下来的回调函数(如
onValueChange) - 父组件 执行
setState修改状态 - 父组件 重新渲染,将新状态通过
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对象)