React学习笔记

58 阅读24分钟
  1. React 没有像 Vue 3 或 Solid.js 那样采用 细粒度更新自动依赖追踪 的响应式系统,而是选择了基于 虚拟 DOM显式状态管理useState, useEffect)的 协调 机制。使其在复杂应用开发中保持了极高的清晰度和可控性。

    • 确定性与可预测性:React 坚持函数式编程理念。组件是一个纯函数:UI = f(state, props)。输入(stateprops)变了,输出(UI)就重新计算。这个过程是确定的、可预测的
    • 更好的调试与工具支持:强大的工具链(React DevTools)可以清晰地追踪状态变化和组件渲染。useEffect 的依赖数组是显式的。ESLint 插件 (eslint-plugin-react-hooks) 可以静态分析代码,自动检查依赖是否完整,避免遗漏依赖导致的 bug。
    • 避免细粒度追踪的复杂性与开销:实现一个高效、可靠的细粒度响应式系统非常复杂。需要处理嵌套对象、数组的深层响应式,这可能导致不必要的追踪或性能问题。在大型、复杂的应用中,细粒度依赖图可能变得庞大且难以管理。React 的协调算法(Fiber 架构)专注于高效地比较两棵 VDOM 树。虽然它会重新执行组件函数,但通过 memo 和合理的组件拆分,可以将“重新执行”的开销控制在可接受范围内。
    • 灵活性与控制力:开发者拥有完全的控制权。你可以决定:哪些状态需要 useState;哪些计算需要 useMemo 缓存;哪些副作用需要 useEffect 以及它们的依赖是什么。这种显式控制提供了极大的灵活性,可以针对特定场景进行精确优化。
  2. 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.memouseMemo 实现高效渲染;
      • 状态变迁路径清晰,易于调试、测试和时间旅行
      • 与并发渲染(Concurrent Rendering)天然契合,支持中断与恢复。
      依赖机制:React 的更新不自动追踪依赖,需手动通过 setState 或 useReducer 触发,组件是否重渲染由父组件传递的 props 或自身状态决定。
  3. Scheduler(调度器)

    协调 React 更新任务的执行时机,利用浏览器空闲时间分片执行工作,保持主线程响应性。长任务被拆成小块,主线程始终能响应用户交互。
    它与 Reconciler(协调器)  紧密协作:

    • Reconciler 负责“做什么”(构建 Fiber 树、Diff);
    • Scheduler 负责“何时做”(安排任务执行时机)。

    核心机制

    • 任务分类(语义化优先级)
      React 不直接暴露“优先级数字”,而是通过更新来源隐式分配调度语义:

      更新类型示例调度行为
      紧急更新(Urgent)onClickonChangeuseLayoutEffect立即同步执行,保证即时反馈
      过渡更新(Transition)startTransition 包裹的更新可中断、可延迟,在空闲时执行
      延迟值(Deferred)useDeferredValue类似 Transition,用于派生状态
      空闲任务(Idle)useEffect 的清理函数(部分场景)在浏览器完全空闲时执行
    • 时间切片(Time Slicing)
      Scheduler 利用浏览器 API 探测“空闲时间”:

      环境使用的 API
      现代浏览器scheduler.postTask(推荐)或 requestIdleCallback
      旧浏览器基于 MessageChannel 的 polyfill(微任务 + 宏任务调度)

      工作流程

      1. React 将 Reconciler 的工作单元(如处理一个 Fiber 节点)封装为 任务(task)
      2. Scheduler 将任务放入优先级队列
      3. 在每一帧(frame)开始时:
        • 先处理高优先级任务(如用户输入);
        • 若有剩余时间(通常 < 5ms),则执行低优先级任务的一个“切片”;
        • 若时间用完,暂停任务,交还控制权给浏览器。
      4. 下一帧继续执行剩余任务(可恢复)。
  4. 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 更新:是同步、不可中断的,如果组件树很大(如渲染 1000 行列表),JS 线程会长时间占用,导致页面卡顿、无响应(“掉帧”);
      • 时间切片的解决方案:将渲染工作拆成 多个小“切片”(chunks),每个切片执行后,交还控制权给浏览器,浏览器可处理用户输入、动画等高优先级任务,然后再继续下一个切片。
    • 时间切片如何工作?
      • 利用 requestIdleCallback(或 polyfill)探测浏览器空闲时间;
      • React 调度器(Scheduler)决定何时执行哪个任务
      • 任务可被中断、恢复、重新排序
  5. Reconciler(协调器)

    React 核心机制之一,负责比较新旧 React 元素树(Virtual DOM)的差异,并高效地更新真实 DOM。从 React 16 开始,Reconciler 从 Stack Reconciler 升级为 Fiber Reconciler,支持可中断、可恢复、带优先级的更新,为并发模式(Concurrent Mode)奠定基础。
    将“声明式 UI 描述”(React 元素树)转化为“真实 DOM 更新操作” ,过程分为两个阶段:

    1. Render 阶段(协调/对比) :可中断、可重入;
    2. Commit 阶段(提交) :不可中断,同步执行 DOM 操作。

    详细工作流程

    • 阶段一:Render 阶段(协调 & Diff)

      1. 触发更新
        • 来源:setState()useState()ReactDOM.render()useEffect 等;
        • React 创建一个 更新对象(Update) ,加入更新队列。
      2. 构建/遍历 Fiber 树
        Fiber 节点:每个 React 元素对应一个 Fiber 对象,包含:
        • type(组件类型)、propsstate
        • childsiblingreturn(构成链表式树结构);
        • alternate(指向上一次渲染的 Fiber,用于 Diff)。
        React 从根节点开始,深度优先遍历(DFS),逐个处理 Fiber 节点。
      3. 执行工作单元(Work Loop)
        • 每个 Fiber 节点是一个“工作单元”;
        • React 执行:
          • 函数组件 → 调用函数,获取子元素;
          • 类组件 → 调用 render()
          • Host 组件(div/span)  → 直接使用子元素。
        • 关键:此过程可被中断(利用浏览器空闲时间分片执行)。
      4. Diff 算法(协调核心)
        React 采用 启发式 O(n) 算法,基于两个假设:
        • 不同 type 的元素,生成不同的树
        • 通过 key 属性,稳定识别同层级子元素
      5. 标记副作用(Side Effects)
        为需要 DOM 操作的 Fiber 节点打上 effectTag
        • Placement:插入新节点
        • Update:更新属性/文本
        • Deletion:删除节点
        • Passive:对应 useEffect
        • Layout:对应 useLayoutEffect
    • 阶段二:Commit 阶段(提交更新)
      此阶段不可中断,必须一气呵成,否则会导致 UI 不一致。

      1. 执行 DOM 操作
        遍历所有带 effectTag 的 Fiber 节点,按顺序执行:
        1. Before Mutation:快照(如 getSnapshotBeforeUpdate);
        2. Mutation:实际操作 DOM(插入/更新/删除);
        3. Layout:同步执行 useLayoutEffect / componentDidMount 等。
      2. 触发副作用
        • Layout Effects:在 DOM 更新后、浏览器绘制前同步执行(可读取布局);
        • Passive Effects:在浏览器绘制后异步执行(即 useEffect)。
      3. 完成提交
        • 将 workInProgress 树切换为 current 树;
        • 清理旧 Fiber 引用,准备下一次更新。
  6. 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 过程:

    1. 树层级 Diff(Tree Diff)
      • 只对比同一层级的节点,不跨层级移动;
      • 若节点类型(type)不同,直接销毁整棵子树,重新创建。
    2. 组件 Diff(Component Diff)
      • 若节点是自定义组件(函数/类),且 type 相同:
        • 复用组件实例
        • 仅更新 props,触发 render() 或函数调用。
      • 若 type 不同(如 Button → Link):
        • 销毁旧组件,创建新组件
    3. 列表 Diff(List Diff)
      1. 构建 key → Fiber 节点 的映射表(existingChildren);
      2. 遍历新子节点列表:
        • 若 key 存在且 type 相同 → 复用节点,标记为 Update
        • 若 key 不存在 → 创建新节点,标记为 Placement
        • 若旧节点未被复用 → 标记为 Deletion
      3. 执行 DOM 操作:插入、移动、删除。

    Diff 的输出:副作用标记(Effect Tags)
    Diff 过程不会直接操作 DOM,而是为 Fiber 节点打上 副作用标记(effectTag) ,供 Commit 阶段使用:

    标记含义
    Placement插入新 DOM 节点
    Update更新属性、文本内容
    Deletion删除 DOM 节点
    Passive对应 useEffect
    Layout对应 useLayoutEffect

    最佳实践

    场景建议
    列表渲染永远使用稳定、唯一的 key(如数据库 ID)
    避免 key 变化不要使用 Math.random() 或 Date.now() 作 key
    条件渲染尽量保持根节点类型一致(如都用 <div> 包裹)
    性能优化对复杂子树使用 React.memo,避免无谓 Diff
    调试 Diff使用 React DevTools 的 “Highlight Updates” 功能
  7. React 的核心机制:协调 (Reconciliation) 与 Diffing

    React 的更新流程

    • 状态变更:当 useState 或 useReducer 的状态更新时,组件函数重新执行
    • 生成新 VDOM:组件函数执行会生成一个新的虚拟 DOM 树。
    • Diffing:React 将新的 VDOM 树与上一次的 VDOM 树进行对比
    • 更新真实 DOM:找出差异后,只更新真实 DOM 中真正变化的部分。

    这个过程的关键在于

    • 组件级重新渲染:状态更新会触发组件及其子组件的函数重新执行(生成新的 VDOM)。
    • 不是直接追踪变量依赖:React 不知道 useState 返回的 count 变量在 render 中被用到了。它只知道 useState 被调用了,状态变了,所以整个组件需要重新渲染。
    • 优化靠 memo:为了避免不必要的子组件重新渲染,React 提供了 React.memouseMemouseCallback 等 手动优化工具,开发者需要显式地指定依赖项。
  8. React 16+ 生命周期

    一、挂载阶段(Mounting)
    组件首次被创建并插入 DOM 时调用,按顺序执行

    • constructor(props)
      • 初始化 state,绑定事件处理器。
      • 避免在这里执行副作用(如请求、订阅)。
    • static getDerivedStateFromProps(props, state)
      • 静态方法,无 this
      • 用于根据 props 派生 state(极少数场景,如受控表单重置)。
      • 返回 null 表示无需更新 state,否则返回新 state 对象。
      • 在 mount 和 update 阶段都会调用
    • render()
      • 唯一必须实现的方法。
      • 返回 JSX(或 nullfalse、字符串等合法 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() (无意义且会警告)。
  9. React Hooks 必须遵循两条核心规则:

    • 只能在函数组件或自定义 Hook 的顶层调用(不能在条件、循环、嵌套函数中调用);
    • 不能在普通的 JavaScript 函数中调用(只能在 React 组件或自定义 Hook 中使用)。

    原因:React 通过内部链表按调用顺序记录每个 Hook 的状态。在组件初次渲染时,Hook 被依次注册到链表;在后续更新时,React 依赖相同的调用顺序来复用对应的状态。若在条件或循环中调用 Hook,会导致调用顺序不一致,从而错位读取状态(例如:useState A 的值被当作 B 的值),引发难以调试的渲染错误。

    解决方案

    • 若需在条件逻辑中使用状态,将条件逻辑移到 Hook 之后(用 state/props 控制逻辑,而非控制 Hook 调用);
    • 提取子组件,在子组件内部安全使用 Hook。
  10. React 与 Vue 3 在状态管理上的根本区别在于“状态的身份标识方式”:

    • React 的 Hooks(如 useState
      • 通过调用顺序(Hook 链表)来识别状态。
      • 因此必须在组件顶层无条件调用——若在条件或循环中使用,会导致渲染间 Hook 顺序不一致,状态错位,引发 bug。
      • 这种设计是为了支持并发渲染:状态与组件函数解耦,存于 Fiber 节点,确保渲染可中断、可恢复。
    • Vue 3 的 ref/reactive
      • 每个响应式对象都有唯一的 JavaScript 引用身份
      • 状态本身独立于组件函数存在,渲染函数通过闭包引用这些对象。
      • 因此即使在条件或循环中创建 ref,只要引用被保留,响应式系统仍能正确追踪和更新。
      • Vue 的响应式系统天然将状态与视图分离,因此无需依赖调用顺序,也无需为并发做类似妥协。

    一句话概括
    React 用“位置”找状态,Vue 用“引用”找状态。
    这也是为什么 Vue 3 允许在条件/循环中使用 ref,而 React 严格禁止在非顶层使用 useState

  11. React HooksuseStateuseEffectuseContextuseReduceruseMemouseCallbackuseRefuseImperativeHandleuseLayoutEffectuseDebugValue

    1. useState

      • 接收一个初始值或初始化函数(() => initialValue)。
      • 返回 [state, setState]
        • state:当前状态;
        • setState(value | updaterFn):支持函数式更新(如 setState(prev => prev + 1))。
      • 状态更新是批处理的(React 18+ 默认全局批处理)。
    2. useEffect

      用于处理异步副作用(如数据请求、订阅、手动 DOM 操作)。 useEffect(effectFn, deps?)

      • effectFn:副作用逻辑,可返回清理函数
      • deps:依赖数组,控制执行时机。

      副作用执行

      • deps:每次渲染后执行;
      • deps = []:仅在挂载后执行一次;
      • 有依赖项:挂载后 + 依赖变化后执行。

      清理函数执行

      • 下一次副作用执行前,先调用上一次返回的清理函数;
      • 组件卸载前,调用最后一次副作用返回的清理函数。

      注意

      • useEffect 异步执行(浏览器绘制之后),不阻塞渲染;
      • 清理函数用于释放资源(如清除定时器、取消订阅),防止内存泄漏。
    3. useContext

      • 用法:const value = useContext(MyContext)
      • 用于直接读取由 <MyContext.Provider value={...} /> 提供的上下文值。
      • 当 Provider 的 value 引用发生变化时,所有使用该 Context 的组件都会重新渲染(即使被 React.memo 包裹)。
      • 适用于跨多层组件共享状态(如主题、用户信息、语言包)。
      • 最佳实践:避免在渲染中创建新对象作为 value(例如 value={{ theme: 'dark' }}),应将其提升到组件外部或用 useMemo 缓存,防止不必要的重渲染。
    4. useReducer

      • 适用于复杂状态逻辑(如多字段表单、状态机、动作驱动的状态更新)。
      • 签名:const [state, dispatch] = useReducer(reducer, initialState, initFn?)
        • reducer 是一个 (state, action) => newState 的纯函数;
        • 可选第三个参数 initFn 用于惰性初始化。
      • dispatch(action) 触发状态更新,自动批处理。
      • 可与 useContext 结合,实现轻量级全局状态管理,替代部分 Redux 场景。
    5. useMemouseCallback

      • useMemo:缓存计算结果,避免重复昂贵计算。
      • useCallback:缓存函数引用,防止因函数引用变化导致子组件不必要的重渲染。

      useMemo 适用场景

      • 对大数据进行过滤、排序、格式化(如 list.filter(...).map(...)
      • 创建复杂对象或数组(避免每次渲染都新建引用)
      • 作为 useEffect 或其他 Hook 的依赖(避免无限循环)

      useCallback 适用场景

      • 将回调函数作为 prop 传递给 React.memo 优化过的子组件
      • 回调函数被用作其他 Hook(如 useEffectuseMemo)的依赖项
    6. useRef

      • 用法:const ref = useRef(initialValue)
      • initialValue 是 ref.current 的初始值
      • 返回一个普通 JavaScript 对象{ current: initialValue }
      • 该值仅在组件首次渲染时被设置一次,后续重新渲染时不会重置
    7. 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 传入的父组件的 ref
      create() => Object返回一个对象,定义父组件可访问的属性和方法
      deps(可选)Array依赖数组,控制何时重新创建暴露的对象(类似 useMemo
    8. useLayoutEffect

      • 用法与 useEffect 完全相同,但执行时机不同
      • 在 DOM 更新完成后、浏览器绘制(paint)之前 同步执行,适用于需要读取或同步修改 DOM 布局的场景。
      • 默认优先使用 useEffect,仅当遇到布局同步问题时才考虑 useLayoutEffect,会阻塞浏览器绘制。
    9. useDebugValue

      在 React DevTools 中为自定义 Hook 显示可读的调试标签,便于开发时快速了解 Hook 的内部状态。

      • 仅在 React 开发者工具(DevTools)  中生效;
      • 不影响生产环境(生产构建中会被自动移除);
      • 主要用于自定义 Hook 的调试增强
  12. React Class 生命周期 ↔ Hooks 对照表

    Class 生命周期 / 能力Hooks 替代方案说明
    constructor()useState(() => {...}) useRef()初始化状态(惰性初始化)或存储可变值(不触发渲染)
    componentDidMount()useEffect(() => { ... }, [])挂载后执行副作用(请求、订阅、DOM 操作)
    componentDidUpdate(prevProps, prevState)useEffect(() => { ... }, [deps])响应依赖变化;自动对比,无需手动判断
    componentWillUnmount()useEffect 返回的清理函数清理定时器、取消请求、移除监听等
    shouldComponentUpdate()React.memo(...)包裹组件,浅比较 props;配合 useMemo/useCallback 优化子 props
    getSnapshotBeforeUpdate()useLayoutEffect(() => { ... })同步执行,在浏览器 paint 前读写 DOM,实现快照逻辑
    static getDerivedStateFromProps()避免使用 → 改用: - 直接计算(const x = compute(props)) - 重置 key - 完全受控组件官方视为反模式;Hooks 中无直接等价 API
    实例方法 / 可变引用useRef()替代 this.xxx 存储不触发渲染的值(如 timer ID、DOM 节点)
    强制更新(极少用)useReducer(x => x + 1, 0)不推荐;通常说明状态设计有问题
    • 关键补充说明

      • useLayoutEffect 不完全等价于 getSnapshotBeforeUpdate

        • getSnapshotBeforeUpdate 是  “render 后、commit 前”  读取旧 DOM。
        • useLayoutEffect 是  “commit 后、paint 前”  读取新 DOM。
        • 但在实现滚动位置同步等场景时,两者可达成相同效果(通过读取新旧高度差)。

        所以实践中 useLayoutEffect功能等价替代,尽管执行时机略有不同。

      • React.memo vs shouldComponentUpdate

        • shouldComponentUpdate 可访问 nextProps 和 nextState,做任意逻辑判断
        • React.memo 默认只浅比较 props;如需自定义比较,可传第二个参数:
        jsx
        编辑
        const MyComponent = React.memo(({ a, b }) => { ... }, (prevProps, nextProps) => {
          return prevProps.a === nextProps.a; // 返回 true 表示不更新
        });
        
  13. 在类组件中,必须使用 setState() 更新状态,禁止直接修改 this.state,原因如下:

    • React 依赖 setState 触发更新流程
      直接修改 this.state 不会触发重新渲染,也不会调用任何生命周期(如 shouldComponentUpdatecomponentDidUpdate),导致 UI 与状态不一致。
    • 状态更新会被自动批处理
      在 React 18+ 中,即使在异步回调(如 setTimeoutPromise)中调用 setState多个状态更新也会被合并为一次渲染,避免不必要的多次重绘,提升性能。
    • 确保生命周期和派生逻辑正确执行
      调用 setState 会触发完整的更新流程:
      getDerivedStateFromPropsshouldComponentUpdaterendergetSnapshotBeforeUpdatecomponentDidUpdate
      从而保证子组件更新、DOM 快照、副作用清理等逻辑正常运行。
    • 支持更新完成后的回调
      setState(updater, callback) 的第二个参数可在 DOM 更新后执行,适用于依赖最新 DOM 的操作(如滚动、焦点)。
    • 保障状态更新的可预测性与可调试性
      所有状态变更都通过统一入口,便于 DevTools 追踪、时间旅行调试(如 React Developer Tools),也避免“幽灵 bug”。
  14. React 懒加载:React.lazy + Suspense

    实现代码分割(Code Splitting)与组件按需加载,优化首屏加载性能。

    • React.lazy():动态导入组件(返回一个 lazy component);
    • Suspense:在组件加载完成前,显示 fallback 内容(如 loading 状态);
    • 必须用 Suspense 包裹 React.lazy 组件,否则会报错。
  15. React.memo

    对函数组件进行浅层 props 比较,避免不必要的重新渲染,提升性能。

    const MyComponent = React.memo(
      ({ user, onEdit }) => <div>{user.name}</div>,
      (prevProps, nextProps) => {
        // 返回 true:props “相等”,**跳过渲染**
        // 返回 false:props “不等”,**执行渲染**
      }
    );
    
    • 是一个高阶组件(HOC) ,用于包裹函数组件;
    • 默认行为:仅当 props 引用发生变化时才重新渲染
    • 适用于渲染开销大、props 变化不频繁的组件;
    • 第二个参数可传入自定义比较函数true 表示“不用更新”
  16. React.forwardRef

    让函数组件能够接收 ref,并将其转发(forward)到内部的 DOM 节点或其他子组件,从而实现父组件对子组件内部 DOM 的直接访问。

    const MyComponent = React.forwardRef((props, ref) => {
      return <input ref={ref} {...props} />;
    });
    
    • 函数组件默认不接收 refref 会被忽略),forwardRef 打破这一限制,使函数组件像原生 DOM 元素一样可被 ref 引用;
    • 若不想暴露原始 DOM,而是提供封装后的命令式方法,应结合 useImperativeHandle,暴露自定义 API。
  17. 新状态依赖于前一状态时,应使用 setState 的函数式更新形式:this.setState(prevState => newState)。因为 setState异步批量更新的,直接使用 this.state 可能获取到过期值;函数式更新能确保基于最新的状态进行计算。
    setState不会立即更新组件状态,而是将状态变更加入更新队列,并在后续批量处理以提升性能。 当新状态的计算依赖于当前状态(如计数器递增),应使用函数式更新: this.setState(prevState => ({ count: prevState.count + 1 })) 这样可避免因闭包异步更新导致的“状态过期”问题。该原则同样适用于函数组件中的 useState 更新函数。

  18. 模板语法(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。
  19. React 的 Fragment 和 Vue 的 <template> 都可用于作为“无渲染”的逻辑容器,避免引入不必要的 DOM 包裹元素。但它们的实现机制和使用场景有所不同:

    • Fragment 是 React 的内置特殊组件,简写语法 <>,它没有生命周期,仅用于 JSX 中返回多个同级元素。
    • <template>  是 Vue 模板语法的一部分,在配合 v-forv-if 或插槽时作为不可见包装器,本身不是组件。
  20. JSX 标签体的内容就是 props.children;如果同时通过 JSX 内容显式 prop 传入 children显式传入的值会覆盖 JSX 内容,本质上它们是同一个 prop

  21. craco

    在不执行 eject 的前提下,安全地自定义 Create React App 的 Webpack、Babel、PostCSS 等配置。

    • CRA 默认隐藏所有构建配置,且不支持直接修改。
    • craco 通过“配置覆盖”(override)机制,在保留 CRA 默认行为的基础上进行扩展或修改。
    • 无需 eject,避免失去 CRA 的自动更新和维护能力。

    基本使用步骤

    • 安装 craconpm 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 插件
  22. eject

    eject 是 CRA 提供的命令,用于将隐藏的 Webpack、Babel、ESLint 等配置文件和构建脚本全部释放到项目中,从此由开发者完全接管构建流程。

    • 执行方式:在 CRA 项目根目录运行npm run eject
      执行后,项目会发生以下变化:

      • 新增 config/ 目录,包含 webpack.config.jswebpackDevServer.config.jsjest.config.js 等;
      • 新增 scripts/ 目录,包含 build.jsstart.jstest.js 等构建脚本;
      • package.json 被修改,移除 react-scripts显式列出所有底层依赖(如 webpack、babel-loader、eslint 等)。
    • 为什么要 eject:CRA 的设计哲学是 “约定优于配置” ,默认隐藏所有构建细节,让开发者专注业务。
      但当你需要以下能力时,可能考虑 eject:

      • 自定义 Webpack 配置(如添加 loader/plugin、修改输出结构);
      • 深度定制 Babel 或 ESLint 规则;
      • 集成不被 CRA 支持的工具(如某些 Webpack 插件);
      • 完全控制构建流程(如自定义环境变量处理、代码分割策略)。
    • 为什么通常不建议 eject

      风险说明
      失去 CRA 的自动更新未来 React 或工具链升级需手动同步配置
      维护成本高你需要自己维护几十个依赖和复杂配置
      容易出错Webpack/Babel 配置复杂,稍有不慎导致构建失败
      团队协作负担新成员需理解整套构建系统