-
React 没有像 Vue 3 或 Solid.js 那样采用 细粒度更新 和 自动依赖追踪 的响应式系统,而是选择了基于 虚拟 DOM 和 显式状态管理(
useState,useEffect)的 协调 机制。使其在复杂应用开发中保持了极高的清晰度和可控性。- 确定性与可预测性:React 坚持函数式编程理念。组件是一个纯函数:
UI = f(state, props)。输入(state,props)变了,输出(UI)就重新计算。这个过程是确定的、可预测的。 - 更好的调试与工具支持:强大的工具链(React DevTools)可以清晰地追踪状态变化和组件渲染。
useEffect的依赖数组是显式的。ESLint 插件 (eslint-plugin-react-hooks) 可以静态分析代码,自动检查依赖是否完整,避免遗漏依赖导致的 bug。 - 避免细粒度追踪的复杂性与开销:实现一个高效、可靠的细粒度响应式系统非常复杂。需要处理嵌套对象、数组的深层响应式,这可能导致不必要的追踪或性能问题。在大型、复杂的应用中,细粒度依赖图可能变得庞大且难以管理。React 的协调算法(Fiber 架构)专注于高效地比较两棵 VDOM 树。虽然它会重新执行组件函数,但通过
memo和合理的组件拆分,可以将“重新执行”的开销控制在可接受范围内。 - 灵活性与控制力:开发者拥有完全的控制权。你可以决定:哪些状态需要
useState;哪些计算需要useMemo缓存;哪些副作用需要useEffect以及它们的依赖是什么。这种显式控制提供了极大的灵活性,可以针对特定场景进行精确优化。
- 确定性与可预测性:React 坚持函数式编程理念。组件是一个纯函数:
-
Vue 与 React 在状态更新哲学上的核心差异:
Vue(响应式系统)
- 基于 Proxy(Vue 3)或 Object.defineProperty(Vue 2)实现深度响应式。
- Vue 3 使用
Proxy,天然支持嵌套对象/数组的自动响应,无需Vue.set(该 API 在 Vue 3 中已废弃)。 - 开发者可直接修改状态(如
obj.a.b = 1),响应式系统会自动追踪依赖并触发更新。 - 需注意:避免解构响应式对象(如
const { count } = state),否则会丢失响应性。
优势:语法直观,状态更新“所见即所得”,适合中小型项目快速开发。
代价:- 初始化时需递归遍历对象创建
Proxy,大对象可能带来性能开销; - 响应式对象始终持有引用,内存占用略高。
- 初始化时需递归遍历对象创建
React(不可变数据 + 单向数据流)
- 强调不可变性(Immutability):状态更新必须返回新对象/数组,而非修改原值。
- 例如:
setUser({ ...user, name: 'Alice' })而非user.name = 'Alice'。
优势:- 通过 引用比较(===) 快速判断是否变化,配合
React.memo、useMemo实现高效渲染; - 状态变迁路径清晰,易于调试、测试和时间旅行;
- 与并发渲染(Concurrent Rendering)天然契合,支持中断与恢复。
setState或useReducer触发,组件是否重渲染由父组件传递的 props 或自身状态决定。 - 通过 引用比较(===) 快速判断是否变化,配合
-
Scheduler(调度器)
协调 React 更新任务的执行时机,利用浏览器空闲时间分片执行工作,保持主线程响应性。长任务被拆成小块,主线程始终能响应用户交互。
它与 Reconciler(协调器) 紧密协作:- Reconciler 负责“做什么”(构建 Fiber 树、Diff);
- Scheduler 负责“何时做”(安排任务执行时机)。
核心机制
-
任务分类(语义化优先级)
React 不直接暴露“优先级数字”,而是通过更新来源隐式分配调度语义:更新类型 示例 调度行为 紧急更新(Urgent) onClick、onChange、useLayoutEffect立即同步执行,保证即时反馈 过渡更新(Transition) startTransition包裹的更新可中断、可延迟,在空闲时执行 延迟值(Deferred) useDeferredValue类似 Transition,用于派生状态 空闲任务(Idle) useEffect的清理函数(部分场景)在浏览器完全空闲时执行 -
时间切片(Time Slicing)
Scheduler 利用浏览器 API 探测“空闲时间”:环境 使用的 API 现代浏览器 scheduler.postTask(推荐)或requestIdleCallback旧浏览器 基于 MessageChannel的 polyfill(微任务 + 宏任务调度)工作流程:
- React 将 Reconciler 的工作单元(如处理一个 Fiber 节点)封装为 任务(task);
- Scheduler 将任务放入优先级队列;
- 在每一帧(frame)开始时:
- 先处理高优先级任务(如用户输入);
- 若有剩余时间(通常 < 5ms),则执行低优先级任务的一个“切片”;
- 若时间用完,暂停任务,交还控制权给浏览器。
- 下一帧继续执行剩余任务(可恢复)。
-
Time Slicing(时间切片)
Time Slicing(时间切片)是 React 将大型更新任务拆分成多个小任务,并在浏览器空闲时段(idle periods)分批执行的调度机制,目的是让高优先级任务(如用户输入)能打断低优先级任务(如后台数据渲染),避免阻塞主线程,保持 UI 的响应性(如滚动、输入不卡顿)。
- 用法:
- 使用 React 18+ 的
createRoot,就自动启用了并发特性(包括时间切片); - 使用并发 API 声明更新语义。虽然开发者无法直接控制时间切片或调度策略,但可以通过以下并发 API 声明更新语义意图,帮助 React 以更合理的方式安排工作:
startTransition:将状态更新标记为 UI 过渡(transition) ,例如搜索过滤或页面切换。这类更新是可中断的,不会阻塞用户输入、点击等高响应性交互;useDeferredValue:延迟派生值的更新(如筛选后的列表),在保持主 UI 响应的同时,异步渲染非紧急内容。适用于避免因频繁输入导致的过度重渲染;useInsertionEffect:专为 CSS-in-JS 库设计,在 DOM 变更之前同步执行,用于注入样式,防止首次渲染时出现无样式内容闪烁(FOUC)。它在并发渲染中具有最严格的执行时机。
- 使用 React 18+ 的
- 为什么需要时间切片?
- 传统 React 更新:是同步、不可中断的,如果组件树很大(如渲染 1000 行列表),JS 线程会长时间占用,导致页面卡顿、无响应(“掉帧”);
- 时间切片的解决方案:将渲染工作拆成 多个小“切片”(chunks),每个切片执行后,交还控制权给浏览器,浏览器可处理用户输入、动画等高优先级任务,然后再继续下一个切片。
- 时间切片如何工作?
- 利用
requestIdleCallback(或 polyfill)探测浏览器空闲时间; - React 调度器(Scheduler)决定何时执行哪个任务;
- 任务可被中断、恢复、重新排序。
- 利用
- 用法:
-
Reconciler(协调器)
React 核心机制之一,负责比较新旧 React 元素树(Virtual DOM)的差异,并高效地更新真实 DOM。从 React 16 开始,Reconciler 从 Stack Reconciler 升级为 Fiber Reconciler,支持可中断、可恢复、带优先级的更新,为并发模式(Concurrent Mode)奠定基础。
将“声明式 UI 描述”(React 元素树)转化为“真实 DOM 更新操作” ,过程分为两个阶段:- Render 阶段(协调/对比) :可中断、可重入;
- Commit 阶段(提交) :不可中断,同步执行 DOM 操作。
详细工作流程
-
阶段一:Render 阶段(协调 & Diff)
- 触发更新
- 来源:
setState()、useState()、ReactDOM.render()、useEffect等; - React 创建一个 更新对象(Update) ,加入更新队列。
- 来源:
- 构建/遍历 Fiber 树
Fiber 节点:每个 React 元素对应一个 Fiber 对象,包含:type(组件类型)、props、state;child、sibling、return(构成链表式树结构);alternate(指向上一次渲染的 Fiber,用于 Diff)。
- 执行工作单元(Work Loop)
- 每个 Fiber 节点是一个“工作单元”;
- React 执行:
- 函数组件 → 调用函数,获取子元素;
- 类组件 → 调用
render(); - Host 组件(div/span) → 直接使用子元素。
- 关键:此过程可被中断(利用浏览器空闲时间分片执行)。
- Diff 算法(协调核心)
React 采用 启发式 O(n) 算法,基于两个假设:- 不同 type 的元素,生成不同的树;
- 通过 key 属性,稳定识别同层级子元素。
- 标记副作用(Side Effects)
为需要 DOM 操作的 Fiber 节点打上 effectTag:Placement:插入新节点Update:更新属性/文本Deletion:删除节点Passive:对应useEffectLayout:对应useLayoutEffect
- 触发更新
-
阶段二:Commit 阶段(提交更新)
此阶段不可中断,必须一气呵成,否则会导致 UI 不一致。- 执行 DOM 操作
遍历所有带 effectTag 的 Fiber 节点,按顺序执行:- Before Mutation:快照(如
getSnapshotBeforeUpdate); - Mutation:实际操作 DOM(插入/更新/删除);
- Layout:同步执行
useLayoutEffect/componentDidMount等。
- Before Mutation:快照(如
- 触发副作用
- Layout Effects:在 DOM 更新后、浏览器绘制前同步执行(可读取布局);
- Passive Effects:在浏览器绘制后异步执行(即
useEffect)。
- 完成提交
- 将
workInProgress树切换为current树; - 清理旧 Fiber 引用,准备下一次更新。
- 将
- 执行 DOM 操作
-
Diff 算法
是 React 高性能渲染的核心机制之一。它的目标是:在状态变化时,以最小代价将旧的 UI 更新为新的 UI。在React 18+中的Fiber 架构 + Scheduler 下,使 Diff 过程可中断、可恢复。在并发模式下,一次更新可能经历多次 Diff 尝试(直到完成或被更高优先级任务打断)。
采用启发式策略,而不是精确算法,不追求 100% 最优 DOM 操作,只要在绝大多数真实场景下足够快、足够准就行。在 O(n) 时间复杂度内,找出两棵 React 元素树之间的最小差异,并生成高效的 DOM 更新操作。
基于两个经验性假设,大幅简化问题:假设 说明 开发者启示 1. 不同类型的元素,生成不同的树 <div>→<span>或ComponentA→ComponentB会被完全销毁重建避免频繁切换根节点类型 2. 通过 key稳定识别同层级子元素列表渲染必须用 稳定、唯一 的 key(如item.id),而非index防止状态错乱、无效重渲染 Diff 过程:
- 树层级 Diff(Tree Diff)
- 只对比同一层级的节点,不跨层级移动;
- 若节点类型(
type)不同,直接销毁整棵子树,重新创建。
- 组件 Diff(Component Diff)
- 若节点是自定义组件(函数/类),且
type相同:- 复用组件实例;
- 仅更新
props,触发render()或函数调用。
- 若
type不同(如Button→Link):- 销毁旧组件,创建新组件。
- 若节点是自定义组件(函数/类),且
- 列表 Diff(List Diff)
- 构建 key → Fiber 节点 的映射表(
existingChildren); - 遍历新子节点列表:
- 若
key存在且type相同 → 复用节点,标记为Update; - 若
key不存在 → 创建新节点,标记为Placement; - 若旧节点未被复用 → 标记为
Deletion。
- 若
- 执行 DOM 操作:插入、移动、删除。
- 构建 key → Fiber 节点 的映射表(
Diff 的输出:副作用标记(Effect Tags)
Diff 过程不会直接操作 DOM,而是为 Fiber 节点打上 副作用标记(effectTag) ,供 Commit 阶段使用:标记 含义 Placement插入新 DOM 节点 Update更新属性、文本内容 Deletion删除 DOM 节点 Passive对应 useEffectLayout对应 useLayoutEffect最佳实践
场景 建议 列表渲染 永远使用稳定、唯一的 key(如数据库 ID)避免 key 变化 不要使用 Math.random()或Date.now()作 key条件渲染 尽量保持根节点类型一致(如都用 <div>包裹)性能优化 对复杂子树使用 React.memo,避免无谓 Diff调试 Diff 使用 React DevTools 的 “Highlight Updates” 功能 - 树层级 Diff(Tree Diff)
-
React 的核心机制:协调 (Reconciliation) 与 Diffing
React 的更新流程:
- 状态变更:当
useState或useReducer的状态更新时,组件函数重新执行。 - 生成新 VDOM:组件函数执行会生成一个新的虚拟 DOM 树。
- Diffing:React 将新的 VDOM 树与上一次的 VDOM 树进行对比 。
- 更新真实 DOM:找出差异后,只更新真实 DOM 中真正变化的部分。
这个过程的关键在于:
- 组件级重新渲染:状态更新会触发组件及其子组件的函数重新执行(生成新的 VDOM)。
- 不是直接追踪变量依赖:React 不知道
useState返回的count变量在render中被用到了。它只知道useState被调用了,状态变了,所以整个组件需要重新渲染。 - 优化靠
memo:为了避免不必要的子组件重新渲染,React 提供了React.memo、useMemo、useCallback等 手动优化工具,开发者需要显式地指定依赖项。
- 状态变更:当
-
React 16+ 生命周期
一、挂载阶段(Mounting)
组件首次被创建并插入 DOM 时调用,按顺序执行:constructor(props)- 初始化
state,绑定事件处理器。 - 避免在这里执行副作用(如请求、订阅)。
- 初始化
static getDerivedStateFromProps(props, state)- 静态方法,无
this。 - 用于根据 props 派生 state(极少数场景,如受控表单重置)。
- 返回
null表示无需更新 state,否则返回新 state 对象。 - 在 mount 和 update 阶段都会调用。
- 静态方法,无
render()- 唯一必须实现的方法。
- 返回 JSX(或
null、false、字符串等合法 React 节点)。 - 应为纯函数,不能包含副作用。
componentDidMount()- 组件已挂载到 DOM。
- 执行副作用的推荐位置:数据请求、订阅、手动 DOM 操作、启动定时器等。
- 此时可安全调用
setState()(会触发一次额外渲染,但浏览器通常合并绘制,用户无感知)。
二、更新阶段(Updating)
当 props 或 state 发生变化时触发,按顺序执行:static getDerivedStateFromProps(props, state)- 同上,每次更新前都会调用(包括 setState 和 props 变化)。
shouldComponentUpdate(nextProps, nextState)- 控制组件是否重新渲染。
- 返回
false可跳过render()及后续生命周期。 - 使用
React.PureComponent可自动实现浅比较,避免手动实现。 - 不适用于函数组件(函数组件用
React.memo)。
render()- 重新生成虚拟 DOM。
getSnapshotBeforeUpdate(prevProps, prevState)- 在 DOM 更新前调用,可捕获更新前的 DOM 状态(如滚动位置)。
- 返回值将作为第三个参数传给
componentDidUpdate。 - 必须配合
componentDidUpdate使用,且需显式返回null或快照值。
componentDidUpdate(prevProps, prevState, snapshot)- DOM 已更新完毕。
- 适合执行依赖 DOM 的操作,或根据 props/state 变化发起请求。
- 若在此调用
setState(),必须包裹在条件判断中,否则会导致无限循环。
三、卸载阶段(Unmounting)
componentWillUnmount()- 组件即将从 DOM 移除。
- 清理副作用:取消网络请求、清除定时器、移除订阅等。
- 此时组件仍处于挂载状态,不可调用
setState()(无意义且会警告)。
-
React Hooks 必须遵循两条核心规则:
- 只能在函数组件或自定义 Hook 的顶层调用(不能在条件、循环、嵌套函数中调用);
- 不能在普通的 JavaScript 函数中调用(只能在 React 组件或自定义 Hook 中使用)。
原因:React 通过内部链表按调用顺序记录每个 Hook 的状态。在组件初次渲染时,Hook 被依次注册到链表;在后续更新时,React 依赖相同的调用顺序来复用对应的状态。若在条件或循环中调用 Hook,会导致调用顺序不一致,从而错位读取状态(例如:
useStateA 的值被当作 B 的值),引发难以调试的渲染错误。解决方案:
- 若需在条件逻辑中使用状态,将条件逻辑移到 Hook 之后(用 state/props 控制逻辑,而非控制 Hook 调用);
- 或提取子组件,在子组件内部安全使用 Hook。
-
React 与 Vue 3 在状态管理上的根本区别在于“状态的身份标识方式”:
- React 的 Hooks(如
useState)- 通过调用顺序(Hook 链表)来识别状态。
- 因此必须在组件顶层无条件调用——若在条件或循环中使用,会导致渲染间 Hook 顺序不一致,状态错位,引发 bug。
- 这种设计是为了支持并发渲染:状态与组件函数解耦,存于 Fiber 节点,确保渲染可中断、可恢复。
- Vue 3 的
ref/reactive- 每个响应式对象都有唯一的 JavaScript 引用身份。
- 状态本身独立于组件函数存在,渲染函数通过闭包引用这些对象。
- 因此即使在条件或循环中创建
ref,只要引用被保留,响应式系统仍能正确追踪和更新。 - Vue 的响应式系统天然将状态与视图分离,因此无需依赖调用顺序,也无需为并发做类似妥协。
一句话概括:
React 用“位置”找状态,Vue 用“引用”找状态。
这也是为什么 Vue 3 允许在条件/循环中使用ref,而 React 严格禁止在非顶层使用useState。 - React 的 Hooks(如
-
React Hooks:
useState、useEffect、useContext、useReducer、useMemo、useCallback、useRef、useImperativeHandle、useLayoutEffect、useDebugValue。-
useState- 接收一个初始值或初始化函数(
() => initialValue)。 - 返回
[state, setState]:state:当前状态;setState(value | updaterFn):支持函数式更新(如setState(prev => prev + 1))。
- 状态更新是批处理的(React 18+ 默认全局批处理)。
- 接收一个初始值或初始化函数(
-
useEffect用于处理异步副作用(如数据请求、订阅、手动 DOM 操作)。
useEffect(effectFn, deps?)effectFn:副作用逻辑,可返回清理函数;deps:依赖数组,控制执行时机。
副作用执行:
- 无
deps:每次渲染后执行; deps = []:仅在挂载后执行一次;- 有依赖项:挂载后 + 依赖变化后执行。
清理函数执行:
- 在下一次副作用执行前,先调用上一次返回的清理函数;
- 在组件卸载前,调用最后一次副作用返回的清理函数。
注意:
useEffect异步执行(浏览器绘制之后),不阻塞渲染;- 清理函数用于释放资源(如清除定时器、取消订阅),防止内存泄漏。
-
useContext- 用法:
const value = useContext(MyContext) - 用于直接读取由
<MyContext.Provider value={...} />提供的上下文值。 - 当
Provider的value引用发生变化时,所有使用该 Context 的组件都会重新渲染(即使被React.memo包裹)。 - 适用于跨多层组件共享状态(如主题、用户信息、语言包)。
- 最佳实践:避免在渲染中创建新对象作为
value(例如value={{ theme: 'dark' }}),应将其提升到组件外部或用useMemo缓存,防止不必要的重渲染。
- 用法:
-
useReducer- 适用于复杂状态逻辑(如多字段表单、状态机、动作驱动的状态更新)。
- 签名:
const [state, dispatch] = useReducer(reducer, initialState, initFn?)reducer是一个(state, action) => newState的纯函数;- 可选第三个参数
initFn用于惰性初始化。
dispatch(action)触发状态更新,自动批处理。- 可与
useContext结合,实现轻量级全局状态管理,替代部分 Redux 场景。
-
useMemo与useCallbackuseMemo:缓存计算结果,避免重复昂贵计算。useCallback:缓存函数引用,防止因函数引用变化导致子组件不必要的重渲染。
useMemo适用场景- 对大数据进行过滤、排序、格式化(如
list.filter(...).map(...)) - 创建复杂对象或数组(避免每次渲染都新建引用)
- 作为
useEffect或其他 Hook 的依赖(避免无限循环)
useCallback适用场景- 将回调函数作为 prop 传递给
React.memo优化过的子组件 - 回调函数被用作其他 Hook(如
useEffect、useMemo)的依赖项
-
useRef- 用法:
const ref = useRef(initialValue); initialValue是ref.current的初始值;- 返回一个普通 JavaScript 对象:
{ current: initialValue }; - 该值仅在组件首次渲染时被设置一次,后续重新渲染时不会重置。
- 用法:
-
useImperativeHandle- 默认情况下,父组件通过
ref只能拿到子组件的 DOM 节点(如果是原生标签)或 null(如果是普通函数组件)。 - 使用
useImperativeHandle+forwardRef,可自定义暴露给父组件的 API。必须与React.forwardRef配合使用。 - 用法:
const FancyInput = forwardRef((props, ref) => { const inputRef = useRef(null); const [value, setValue] = useState(''); // 暴露特定方法给父组件 useImperativeHandle(ref, () => ({ focus: () => inputRef.current?.focus(), getValue: () => value, clear: () => setValue('') }), [value]); return <input ref={inputRef} value={value} onChange={e => setValue(e.target.value)} />; });参数说明:
参数 类型 说明 refRefObject由 forwardRef传入的父组件的refcreate() => Object返回一个对象,定义父组件可访问的属性和方法 deps(可选)Array依赖数组,控制何时重新创建暴露的对象(类似 useMemo) - 默认情况下,父组件通过
-
useLayoutEffect- 用法与
useEffect完全相同,但执行时机不同。 - 在 DOM 更新完成后、浏览器绘制(paint)之前 同步执行,适用于需要读取或同步修改 DOM 布局的场景。
- 默认优先使用
useEffect,仅当遇到布局同步问题时才考虑useLayoutEffect,会阻塞浏览器绘制。
- 用法与
-
useDebugValue在 React DevTools 中为自定义 Hook 显示可读的调试标签,便于开发时快速了解 Hook 的内部状态。
- 仅在 React 开发者工具(DevTools) 中生效;
- 不影响生产环境(生产构建中会被自动移除);
- 主要用于自定义 Hook 的调试增强。
-
-
React Class 生命周期 ↔ Hooks 对照表
Class 生命周期 / 能力 Hooks 替代方案 说明 constructor()useState(() => {...})useRef()初始化状态(惰性初始化)或存储可变值(不触发渲染) componentDidMount()useEffect(() => { ... }, [])挂载后执行副作用(请求、订阅、DOM 操作) componentDidUpdate(prevProps, prevState)useEffect(() => { ... }, [deps])响应依赖变化;自动对比,无需手动判断 componentWillUnmount()useEffect返回的清理函数清理定时器、取消请求、移除监听等 shouldComponentUpdate()React.memo(...)包裹组件,浅比较 props;配合 useMemo/useCallback优化子 propsgetSnapshotBeforeUpdate()useLayoutEffect(() => { ... })同步执行,在浏览器 paint 前读写 DOM,实现快照逻辑 static getDerivedStateFromProps()避免使用 → 改用: - 直接计算( const x = compute(props)) - 重置key- 完全受控组件官方视为反模式;Hooks 中无直接等价 API 实例方法 / 可变引用 useRef()替代 this.xxx存储不触发渲染的值(如 timer ID、DOM 节点)强制更新(极少用) useReducer(x => x + 1, 0)不推荐;通常说明状态设计有问题 -
关键补充说明
-
useLayoutEffect不完全等价于getSnapshotBeforeUpdategetSnapshotBeforeUpdate是 “render 后、commit 前” 读取旧 DOM。useLayoutEffect是 “commit 后、paint 前” 读取新 DOM。- 但在实现滚动位置同步等场景时,两者可达成相同效果(通过读取新旧高度差)。
所以实践中
useLayoutEffect是 功能等价替代,尽管执行时机略有不同。 -
React.memovsshouldComponentUpdateshouldComponentUpdate可访问nextProps和nextState,做任意逻辑判断。React.memo默认只浅比较 props;如需自定义比较,可传第二个参数:
jsx 编辑 const MyComponent = React.memo(({ a, b }) => { ... }, (prevProps, nextProps) => { return prevProps.a === nextProps.a; // 返回 true 表示不更新 });
-
-
-
在类组件中,必须使用
setState()更新状态,禁止直接修改this.state,原因如下:- React 依赖
setState触发更新流程
直接修改this.state不会触发重新渲染,也不会调用任何生命周期(如shouldComponentUpdate、componentDidUpdate),导致 UI 与状态不一致。 - 状态更新会被自动批处理
在 React 18+ 中,即使在异步回调(如setTimeout、Promise)中调用setState,多个状态更新也会被合并为一次渲染,避免不必要的多次重绘,提升性能。 - 确保生命周期和派生逻辑正确执行
调用setState会触发完整的更新流程:
getDerivedStateFromProps→shouldComponentUpdate→render→getSnapshotBeforeUpdate→componentDidUpdate
从而保证子组件更新、DOM 快照、副作用清理等逻辑正常运行。 - 支持更新完成后的回调
setState(updater, callback)的第二个参数可在 DOM 更新后执行,适用于依赖最新 DOM 的操作(如滚动、焦点)。 - 保障状态更新的可预测性与可调试性
所有状态变更都通过统一入口,便于 DevTools 追踪、时间旅行调试(如 React Developer Tools),也避免“幽灵 bug”。
- React 依赖
-
React 懒加载:
React.lazy+Suspense实现代码分割(Code Splitting)与组件按需加载,优化首屏加载性能。
React.lazy():动态导入组件(返回一个 lazy component);Suspense:在组件加载完成前,显示 fallback 内容(如 loading 状态);- 必须用
Suspense包裹React.lazy组件,否则会报错。
-
React.memo对函数组件进行浅层 props 比较,避免不必要的重新渲染,提升性能。
const MyComponent = React.memo( ({ user, onEdit }) => <div>{user.name}</div>, (prevProps, nextProps) => { // 返回 true:props “相等”,**跳过渲染** // 返回 false:props “不等”,**执行渲染** } );- 是一个高阶组件(HOC) ,用于包裹函数组件;
- 默认行为:仅当 props 引用发生变化时才重新渲染;
- 适用于渲染开销大、props 变化不频繁的组件;
- 第二个参数可传入自定义比较函数,
true表示“不用更新”。
-
React.forwardRef让函数组件能够接收
ref,并将其转发(forward)到内部的 DOM 节点或其他子组件,从而实现父组件对子组件内部 DOM 的直接访问。const MyComponent = React.forwardRef((props, ref) => { return <input ref={ref} {...props} />; });- 函数组件默认不接收
ref(ref会被忽略),forwardRef打破这一限制,使函数组件像原生 DOM 元素一样可被ref引用; - 若不想暴露原始 DOM,而是提供封装后的命令式方法,应结合
useImperativeHandle,暴露自定义 API。
- 函数组件默认不接收
-
当新状态依赖于前一状态时,应使用
setState的函数式更新形式:this.setState(prevState => newState)。因为setState是异步批量更新的,直接使用this.state可能获取到过期值;函数式更新能确保基于最新的状态进行计算。
setState并不会立即更新组件状态,而是将状态变更加入更新队列,并在后续批量处理以提升性能。 当新状态的计算依赖于当前状态(如计数器递增),应使用函数式更新:this.setState(prevState => ({ count: prevState.count + 1 }))这样可避免因闭包或异步更新导致的“状态过期”问题。该原则同样适用于函数组件中的useState更新函数。 -
模板语法(Template Syntax)和 JSX(JavaScript XML)是两种用于在前端框架中定义用户界面(UI) 的不同方式。它们最著名的代表分别是 Vue.js 的模板语法 和 React 的 JSX。
模板语法:扩展的 HTML
- 分离关注点:将结构(HTML) 、样式(CSS) 和逻辑(JavaScript) 尽可能地分离。
- 声明式扩展 HTML:在标准 HTML 的基础上,通过指令(Directives)和插值(Interpolation)来添加动态行为。
JSX:JavaScript 的语法扩展
- 一切皆 JavaScript:UI 是 JavaScript 的一部分。组件的结构、逻辑、样式(通过 CSS-in-JS 或模块化 CSS)都可以写在同一个
.js或.jsx文件中。 - 可编程性:利用 JavaScript 的全部能力(循环、条件、函数、变量)来构建 UI。
-
React 的
Fragment和 Vue 的<template>都可用于作为“无渲染”的逻辑容器,避免引入不必要的 DOM 包裹元素。但它们的实现机制和使用场景有所不同:Fragment是 React 的内置特殊组件,简写语法<>,它没有生命周期,仅用于 JSX 中返回多个同级元素。<template>是 Vue 模板语法的一部分,在配合v-for、v-if或插槽时作为不可见包装器,本身不是组件。
-
JSX标签体的内容就是props.children;如果同时通过JSX内容和显式prop传入children,显式传入的值会覆盖JSX内容,本质上它们是同一个prop。 -
craco在不执行
eject的前提下,安全地自定义 Create React App 的 Webpack、Babel、PostCSS 等配置。- CRA 默认隐藏所有构建配置,且不支持直接修改。
craco通过“配置覆盖”(override)机制,在保留 CRA 默认行为的基础上进行扩展或修改。- 无需
eject,避免失去 CRA 的自动更新和维护能力。
基本使用步骤
- 安装 craco:
npm install @craco/craco --save-dev; - 创建配置文件:在项目根目录创建
craco.config.js; - 替换启动命令:修改
package.json中的脚本。
常见配置场景
需求 配置示例 添加 Sass/SCSS 支持 CRA 已内置,但可通过 craco自定义 loader 选项(如sassOptions)添加路径别名(@/components) 修改 webpack.resolve.alias自定义环境变量前缀 修改 webpack.DefinePlugin添加 PostCSS 插件(如 tailwindcss) 配置 style.postcss修改输出目录或 publicPath 修改 webpack.output集成 Ant Design 按需加载 使用 craco-antd插件 -
ejecteject是 CRA 提供的命令,用于将隐藏的 Webpack、Babel、ESLint 等配置文件和构建脚本全部释放到项目中,从此由开发者完全接管构建流程。-
执行方式:在 CRA 项目根目录运行
npm run eject;
执行后,项目会发生以下变化:- 新增
config/目录,包含webpack.config.js、webpackDevServer.config.js、jest.config.js等; - 新增
scripts/目录,包含build.js、start.js、test.js等构建脚本; package.json被修改,移除react-scripts,显式列出所有底层依赖(如 webpack、babel-loader、eslint 等)。
- 新增
-
为什么要 eject:CRA 的设计哲学是 “约定优于配置” ,默认隐藏所有构建细节,让开发者专注业务。
但当你需要以下能力时,可能考虑 eject:- 自定义 Webpack 配置(如添加 loader/plugin、修改输出结构);
- 深度定制 Babel 或 ESLint 规则;
- 集成不被 CRA 支持的工具(如某些 Webpack 插件);
- 完全控制构建流程(如自定义环境变量处理、代码分割策略)。
-
为什么通常不建议 eject?
风险 说明 失去 CRA 的自动更新 未来 React 或工具链升级需手动同步配置 维护成本高 你需要自己维护几十个依赖和复杂配置 容易出错 Webpack/Babel 配置复杂,稍有不慎导致构建失败 团队协作负担 新成员需理解整套构建系统
-