跳到主要内容

原理✅

react 和 react-dom 是什么关系?

答案

参考 Codebase Overview

  • react 包含了 react 脱离平台的通用能力,例如 hooks, react 核心 API,详见 react reference 一般需要配合具体的运行环境使用比如 DOM、React Native 等。
  • react-dom 是 react 的 DOM 版本,用于在浏览器中渲染 React 组件,包含了特定平台下的组件和 api,详见 react-dom reference

本质上 react 是脱离宿主环境的通用能力,react-dom 是 react 在浏览器中的实现和能力扩充

延伸阅读

React 中 mode 是什么?

答案

mode 是 React 框架内部概念,用来决定渲染策略,React 18 后统一采用 Concurrent Mode。具体模式如下

  1. Legacy Mode: React 17 中使用的当前模式
    • 默认禁用 StrictMode
    • 默认为同步模式
    • 使用传统的 Suspense 语义
  2. Blocking Mode: Legacy 和 Concurrent 之间的混合模式
    • 默认启用 StrictMode
    • 默认为同步模式
    • 支持一些新特性
  3. Concurrent Mode: React 18 中使用的新模式
    • 默认启用 StrictMode
    • 默认为并发模式
    • 支持所有新特性

关联题目

延伸阅读

Fiber 是什么,有哪些作用?

答案

Fiber 是用来表示渲染节点的数据结构,用来解决 React 历史的 Reconcliation 必须递归完成渲染,导致无法及时响应用户事件造成的卡顿问题。 每个 Fiber 对应这一个渲染节点,这样可以将组件的递归渲染拆分为一系列的工作单元,从而在此基础上实现对渲染的调度,包括延迟执行,优先级控制等。

Fiber 结构如下

对 fiber 结构的属性聚类如下

分类字段名类型功能描述
组件身份与引用tagWorkTagFiber 类型,如函数组件、DOM 元素等
keystring | null唯一标识子节点,用于列表 diff
elementTypeanyJSX 中的 type,如 'div'、组件名
typeany实际的函数或类组件定义
stateNodeany对应的 DOM 节点或类实例
refRefObject | Function | null节点引用
refCleanup(() => void) | null卸载时清理 ref 的函数
树结构关系returnFiber | null父节点
childFiber | null第一个子节点
siblingFiber | null下一个兄弟节点
indexnumber在父节点子数组中的索引
状态快照与更新pendingPropsany新的 props
memoizedPropsany上一次渲染用的 props
memoizedStateany上一次渲染后的 state
updateQueuemixed状态更新队列
dependenciesDependencies | nullContext 等依赖信息
调度与副作用modeTypeOfModeFiber 渲染模式,如并发
flagsFlags当前 Fiber 的副作用标记
subtreeFlagsFlags子树副作用标记汇总
deletionsFiber[] | null需要删除的子节点列表
lanesLanes当前 Fiber 的优先级通道
childLanesLanes子树中最高优先级通道
双缓冲机制alternateFiber | null当前 Fiber 的“工作副本”
性能分析(Profiler)actualDuration?number当前更新中此 Fiber 的实际耗时
actualStartTime?number当前渲染任务开始时间
selfBaseDuration?number自身渲染耗时(跳过不更新不变)
treeBaseDuration?number子树总耗时
调试信息(仅 DEV)_debugInfo?ReactDebugInfo | nullFiber 的调试元信息
_debugOwner?Fiber | null拥有当前 Fiber 的组件
_debugStack?string | Error | null调试堆栈
_debugTask?ConsoleTask | null调试任务追踪
_debugNeedsRemount?boolean是否需要重新挂载
_debugHookTypes?HookType[] | null用于验证 Hook 顺序的列表

延伸阅读

react 是如何进行渲染的?

答案

以该代码为例

<div id="root"></div>

<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>

<script type="text/babel" data-type="module">
   import React, { Component } from "https://esm.sh/react@19";
   import ReactDOM from "https://esm.sh/react-dom@19/client";
debugger
   function Counter() {
      const [count, setCount] = React.useState(0);
      return (
         <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      );
   }

   class App extends Component {
      state = { time: new Date().toLocaleTimeString() };

      render() {
         debugger;
         return (
            <div>
               <Counter /> {this.state.time}
            </div>
         );
      }
   }

   const root = ReactDOM.createRoot(document.getElementById("root"));
   root.render(<App />);
</script>

一. 编译阶段

  1. 编写的函数或类组件中 jsx 语法被替换为 React.createElement 函数调用。例如 <div>hello</div> 转换为 React.createElement('div', null, 'hello'),可以采用 @babel/preset-react 进行转换,对于 tsx, tsc 支持 --jsx 控制输出产物为 js 或者是 jsx ,然后交给 babel 进一步处理。
注意

注意此 demo 是在浏览器中运行,实际上编译阶段是在构建代码的时候发生的,此处只是为了说明,不要在浏览器采用这种方式

二.运行时阶段首次加载

  1. 调用 ReactDOM.createRoot(container) 返回 root 节点 ,核心逻辑包括
    1. 事件委托,将事件挂载在 container 节点上 ,详见listenToAllSupportedEvents
    2. 创建 FiberRoot 元素,详见 ReactFiberRoot
    3. 返回 ReactDOMRoot 对象包含
  2. 调用 root.render(<App />) 渲染组件到 container 中,核心逻辑包括
    1. 触发 updateContainerImpl
    2. 触发 scheduleImmediateRootScheduleTask 这里会异步调度渲染任务默认优先级是, 只考虑浏览器端
      1. 优先使用 queueMicrotask
      2. Promise.resolve
      3. setTimeout
      4. new MessageChannel()
    3. 异步触发 processRootScheduleInMicrotask,内部调用 scheduleTaskForRootDuringMicrotask, 注册一个异步回调 performWorkUntilDeadline 通过 postMessage 触发,内部执行 performWorkOnRootViaSchedulerTask核心逻辑包括
      1. flushPendingEffects 清除被挂起的副作用
      2. performWorkOnRoot 执行渲染任务
        1. 基于 shouldTimeSlice 判断是 renderRootConcurrent 还是 renderRootSync 默认执行 renderRootSync
          1. renderRootSync 内部会触发 workLoopSync 来对 fiber 节点执行深度遍历,核心函数包括
            1. performUnitOfWork(unitOfWork) 从根节点开始已 fiber 节点为粒度执行
            2. beginWork 根据 fiber 节点类型创建子节点
              1. reconcileChildren 会基于 FiberNode 的 tag 类型处理子 fiber 的生成,关联 fiber.child 关系
            3. completeUnitOfWork 当 fiberNode 没有子节点的时候会触发该逻辑,完成状态更新
            4. completeWork 在该阶段会完成节点的创建绑定到 stateNode 等
          2. 完成了 workLoop 循环会触发 commitRootWhenReady 内部会调用 commitRoot
            1. 执行 commitBeforeMutationEffects getSnapshotBeforeUpdate 会在此阶段触发
            2. 执行 flushMutationEffects 完成 willxx 的事件, 和 effect 中的清除回调
            3. 执行 flushLayoutEffects 完成 useLayoutEffects 清理回调,和触发事件
            4. 执行 flushSpawnedWork 完成本次 commit 的相关状态清理动作, 同时触发
            5. 调度器触发执行异步调度的 performWorkUntilDeadline 执行 flushPassiveEffects 触发 useEffect 的清理回调和触发事件

三.运行时阶状态变更 当通过类似 setState 等改变状态后,会重新触发调度器执行

  1. performWorkUntilDeadline 然后内部执行 performWorkOnRootViaSchedulerTask,后续流程和首次渲染调用链相同一样会从根节点开始递归执行 render 阶段,但是对于没有变化的 fiberNode 会直接跳过,只收集变化的 fiberNode,而后触发 commit 阶段操作。

延伸阅读

scheduler 调度机制原理

答案

React Scheduler 是 React 在 Concurrent 模式下用来管理更新任务的调度器,核心目的是:

  1. 解决 UI 卡顿:将大型更新拆分成小任务,在多个帧内执行;
  2. 实现任务优先级:高优先级更新(如用户交互)可中断低优先级任务,提升响应性;
  3. 支持可中断 & 恢复:防止主线程长时间被渲染卡住。

核心原理

  1. 采用 SchedulerPriorities 定义任务优先级, SchedulerFeatureFlags 定义时间分片和超时兜底策略
  2. unstable_scheduleCallback 处理调度,优先使用 MessageChannel 机制,没有则回退到 setTimeout
  3. 在 reconciler 中通过 shouldYield 判断是否需要重新调度

diff 算法细节?

答案

基础逻辑参考 The Diffing Algorithm

  1. 节点类型不同直接替换
  2. 节点相同类型增量 patch, 触发对应钩子
  3. 如果是数组比对详见 reconcileChildrenArray、reconcileChildrenIterator
    1. 首先执行“顺序比较”,从左到右遍历新旧列表,如果 key 和 type 都一致,则复用旧 Fiber 并继续;一旦发现不一致,停止顺序遍历,进入下一阶段。
    2. React 构建旧 Fiber 节点的 key → fiber 的 Map,用于新节点根据 key 快速查找是否有可复用节点,避免多次扫描旧列表。
    3. 对于每个新节点,React 根据 key 从旧 Map 中查找是否可复用 fiber,如果没有匹配项则创建新 fiber,并设置 Placement 标志。
    4. 如果找到复用的 fiber,但其在旧列表中的位置小于上一个复用节点的位置,则说明它“左移”了,React 会标记为需要移动(也打上 Placement 标志)。
    5. 遍历完新列表后,旧列表中未被复用的 fiber 节点会统一打上 Deletion 标志,在提交阶段进行删除。
    6. 整个 diff 阶段不会操作 DOM,而是通过 Placement / Update / Deletion 等 effectTag 标记所有变更操作,等待 commit 阶段批量执行。
    7. React diff 的核心优化点在于 key 的使用;如果没有提供 key 或 key 变化频繁,会导致大量非必要的删除与重建。
提示

React 没有采用双端对比策略(如 Vue2 的双指针算法),只支持单向从左到右比对,原因是 fiber 的结构是一个单向链表,性能在中间插入、左移场景下不如 Vue。React 也未使用“最长递增子序列(LIS)”优化最少移动路径,所以 key 顺序发生变更时,依旧会触发多个移动操作。

react hooks 核心原理?

答案

核心原理

  • useState 是 React Hooks 的基础,依赖 Fiber 架构和链表数据结构实现状态管理。
  • 每次组件渲染时,React 会按顺序遍历 hooks 链表,确保每个 useState 都能正确获取和更新自己的状态。

底层机制

  • 首次渲染时,useState 会在 Fiber 节点上创建一个 hook 对象,保存初始值和更新队列。
  • 每次调用 setState,React 会将新的状态更新加入队列,并触发组件重新渲染。
  • 渲染时,React 依次遍历 hooks 链表,取出最新状态,保证顺序一致性。
  • 状态持久化依赖 Fiber 节点,卸载时清理,挂载时恢复。

代码示例

import React, { useState } from 'react'
function Counter () {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
)
}

常见误区

  • 不能在条件、循环或嵌套函数中调用 useState,否则会破坏 hooks 链表顺序,导致状态错乱。
  • setState 并非立即更新,而是异步批量处理,避免多次渲染。

实际开发建议

  • Hooks 顺序必须保持一致,避免在条件语句中调用。
  • 利用批量更新提升性能,合理拆分组件。
提示

Hooks 本质是链表,顺序决定状态归属,务必保证每次渲染调用顺序一致。

延伸阅读

ref 是如何实现的?

答案

核心原理

  • ref 是 React 提供的访问 DOM 或组件实例的机制,底层通过 Fiber 节点和 hooks 链表实现数据持久化和共享。
  • useRef 返回一个稳定的对象 { current },每次渲染都保持引用不变,避免因变化导致组件重渲染。

实现机制

  • 函数组件中,useRef 在 Fiber 节点的 hooks 链表上创建一个 ref 对象,挂载到 memoizedState,每次渲染都复用同一个对象。
  • 类组件中,ref 属性会在组件挂载时自动赋值为实例或 DOM 节点,卸载时清空。
  • forwardRef 用于将 ref 透传到子组件,支持函数组件访问 DOM。

代码示例

import React, { useRef, forwardRef } from 'react'
// useRef 用法
function Demo () {
const inputRef = useRef(null)
return <input ref={inputRef} />
}
// forwardRef 用法
const FancyInput = forwardRef((props, ref) => <input ref={ref} {...props} />)

常见误区

  • useRef 变化不会触发组件重新渲染,适合存储副作用或 DOM 节点。
  • ref 不是响应式数据,不能用于驱动视图更新。

实际开发建议

  • 访问 DOM、保存定时器、缓存值优先用 useRef。
  • 组件间 ref 传递用 forwardRef,避免 props 传递冗余。
提示

useRef 返回的对象始终稳定,适合存储跨渲染周期的可变数据。

延伸阅读

为什么不能在循环、条件或嵌套函数中调用 Hooks?

答案

核心原理

  • React Hooks(如 useState/useEffect)底层通过链表结构存储,每次渲染按顺序遍历 hooks 链表,确保每个 hook 能正确获取自己的状态。
  • 如果在循环、条件或嵌套函数中调用 hooks,会导致每次渲染时 hooks 顺序不一致,链表结构错乱,状态无法正确归属,React 会直接抛错。

底层机制

  • 首次渲染时,React 按代码顺序依次创建 hook 节点,追加到 Fiber 节点的 hooks 链表。
  • 更新时,React 依次遍历链表,取出对应的状态和副作用。
  • 顺序错乱会导致取值错位,状态混乱,甚至出现“状态丢失”或“状态错位”问题。

代码示例

// 错误用法
if (flag) {
const [a, setA] = useState(0)
}
const [b, setB] = useState(1) // 状态归属不确定,可能出错

常见误区

  • Hooks 本质不是数组,而是链表,顺序决定状态归属。
  • 不能在条件、循环、嵌套函数中调用 hooks,必须在组件顶层保持顺序一致。

实际开发建议

  • 所有 hooks 必须在组件顶层调用,保证每次渲染顺序一致。
  • 避免在 if/for/函数内部调用 hooks,必要时拆分组件。
提示

Hooks 顺序决定状态归属,顶层调用是硬性规范,违背会直接报错。

延伸阅读

为什么要自定义合成事件

答案

核心原理

  • React 合成事件系统通过事件委托和统一 API,解决浏览器兼容性、性能和安全问题。
  • 合成事件对象是 React 自己实现的,具备一致的属性和行为,简化事件处理和调试。

优势说明

  • 跨浏览器一致性:所有事件都通过统一接口处理,避免不同浏览器差异。
  • 性能优化:事件委托到根节点,减少 DOM 监听数量,支持事件池复用,降低内存消耗。
  • 简化事件处理:只需关注冒泡阶段,API一致,易于维护。
  • 安全性和可控性:防止 XSS,便于事件管理和清理,生命周期集成。
  • 与 React 特性集成:支持虚拟 DOM、状态管理、自动解绑,提升开发体验。

触发顺序说明

  • 合成事件优先于原生事件执行,只有未阻止冒泡时才会触发原生事件。
  • 早期 React 事件委托机制可能导致原生事件先执行,现代版本已优化为“先合成后原生”。

代码示例

function Demo () {
function handleClick (e) {
e.stopPropagation() // 阻止原生事件
console.log('React 合成事件')
}
return <div onClick={handleClick}>点击我</div>
}

实际开发建议

  • 推荐始终使用 React 合成事件,避免直接 addEventListener,保证兼容性和性能。
  • 复杂交互场景可结合原生事件,但需注意事件顺序和解绑。
提示

合成事件统一管理,便于跨平台和调试,适合绝大多数业务场景。

延伸阅读

lazy import

答案

核心原理

  • React.lazy 利用 JavaScript 的动态 import() 实现组件的异步加载和代码分割。
  • 当组件首次渲染时,lazy 返回的特殊组件会触发 import(),将目标组件代码单独打包并按需加载。

实现机制

  • lazy(fn) 接收一个返回 Promise 的函数,内部会等待 Promise resolve 后获取组件定义。
  • 加载期间,需用 <Suspense> 包裹,fallback 属性用于显示加载占位符。
  • 加载完成后,React 自动渲染异步加载的组件,未加载时显示 fallback。

代码示例

import React, { lazy, Suspense } from 'react'
const MyComponent = lazy(() => import('./MyComponent'))
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<MyComponent />
</Suspense>
)
}

常见误区

  • lazy 只能用于默认导出的组件,命名导出需额外处理。
  • 必须配合 Suspense,否则加载期间会报错。

实际开发建议

  • 适合路由、弹窗等大体积组件的按需加载,提升首屏性能。
  • fallback 可自定义为骨架屏、动画等,优化用户体验。
提示

lazy 结合 Suspense 可实现高效代码分割,建议用于大型项目的性能优化。

延伸阅读

react 优化手段有哪些?

答案
优化手段主要方式/工具优势/典型场景
代码分割Suspense、lazy、react-loadable按需加载,减少首屏体积
组件颗粒化独立请求渲染单元降低父组件冗余渲染
性能调优PureComponent、React.memo浅比较 props/state,减少无效渲染
不可变数据immutable.js、immer.js高效比较,提升 diff 性能
函数缓存useMemo、useCallback、ahooks避免重复声明,提升子组件复用
状态管理优化connect、recoil精细化订阅,减少链式渲染
事件绑定优化事件委托降低 DOM 监听数量

详细说明

  • 代码分割:通过 React.lazy<Suspense> 实现异步加载,react-loadable 支持自定义加载动画,适合路由、弹窗等大体积组件。
  • 组件颗粒化:将依赖数据请求的组件拆分为独立渲染单元,减少父组件渲染影响,提升性能。
  • 性能调优:PureComponentReact.memo 通过浅比较 props/state,避免无关组件渲染;shouldComponentUpdate 可自定义渲染条件。
  • 不可变数据:immutable.jsimmer.js 提供高效不可变数据结构,配合 diff 算法提升性能。
  • 函数缓存:useMemouseCallbackahooks 避免重复声明函数,提升子组件 memo 效果。
  • 状态管理优化:connect 精细化订阅,recoil 细粒度状态管理,减少全局渲染。
  • 事件绑定优化:React 事件委托机制,减少 DOM 事件监听,提升整体性能。

常见误区

  • 过度颗粒化可能增加维护成本,需结合实际场景权衡。
  • memo 只对 props 浅比较,深层数据需配合不可变数据结构。

实际开发建议

  • 优先使用 lazy/Suspense 实现代码分割,结合 memo/immutable 优化渲染。
  • 复杂状态建议拆分为独立渲染单元,避免全局状态变更导致链式渲染。
提示

性能优化需结合业务场景,建议先分析瓶颈再选用合适手段,避免过度优化。

延伸阅读

什么时候 React 会发生 re-render 如何避免?

答案

延伸阅读

在 React 应用中如何排查性能问题?

在 React 应用中,可以通过以下方法来排查性能问题:

一、使用 Chrome 开发者工具

  1. 性能分析(Performance)
  • 打开 Chrome 浏览器,进入开发者工具。选择“Performance”选项卡。
  • 点击“Record”按钮开始录制页面的交互过程。进行一些典型的操作,如加载页面、点击按钮、滚动页面等。
  • 停止录制后,开发者工具会生成一个性能分析报告。这个报告显示了页面在录制期间的各种性能指标,包括 CPU 使用率、内存使用情况、网络请求等。
  • 分析报告中的“Main”线程,可以查看在录制期间哪些操作占用了大量的 CPU 时间。常见的性能瓶颈包括长时间的 JavaScript 执行、频繁的重渲染等。
  • 例如,如果发现某个函数的执行时间很长,可以点击该函数查看详细的调用栈,以确定问题的根源。
  1. React 开发者工具(React Developer Tools)
  • 安装 React 开发者工具插件。在 Chrome 浏览器中,打开需要排查性能问题的 React 应用页面。
  • 打开开发者工具,选择“React”选项卡。
  • 在 React 开发者工具中,可以查看组件的层次结构、Props 和 State。这有助于确定哪些组件的状态变化频繁,或者哪些组件的渲染时间较长。
  • 特别关注那些在不必要的时候触发重新渲染的组件。可以通过检查组件的shouldComponentUpdate方法或使用React.memoPureComponent等优化手段来减少不必要的重新渲染。

二、使用 React Profiler

  1. 开启 Profiler
  • 在 React 应用中,可以使用React.Profiler组件来进行性能分析。在需要分析性能的组件树的根节点处包裹React.Profiler
  • 例如:
import React from "react";
import ReactDOM from "react-dom";
import { Profiler } from "react";

const App = () => (
<Profiler
id="MyApp"
onRender={(id, phase, actualDuration) => {
console.log(`Component ${id} rendered in phase ${phase} with duration ${actualDuration} ms.`);
}}
>
{/* 你的应用组件 */}
</Profiler>
);

ReactDOM.render(<App />, document.getElementById("root"));
  1. 分析结果
  • 在应用运行过程中,React.Profiler会记录组件的渲染时间和其他性能指标。可以在控制台中查看输出的日志,了解每个组件的渲染时间和触发渲染的原因。
  • 根据日志信息,可以确定哪些组件的渲染时间较长,以及哪些操作导致了频繁的重新渲染。这有助于针对性地进行性能优化。

三、检查代码中的潜在问题

  1. 避免不必要的重新渲染
  • 确保组件的shouldComponentUpdate方法正确实现,或者使用React.memoPureComponent来避免不必要的重新渲染。检查组件的依赖项是否正确设置,避免因为不必要的状态变化而触发重新渲染。
  • 例如,如果一个组件的渲染结果只依赖于某个特定的 prop,而不是所有的 props,可以使用React.memo并指定一个自定义的比较函数来进行更精确的比较。
  1. 优化大型列表渲染
  • 对于大型列表的渲染,考虑使用React.memokey属性来优化性能。确保为每个列表项设置一个唯一的key属性,这有助于 React 更高效地识别列表项的变化。
  • 避免在列表渲染中使用索引作为key属性,因为这可能会导致性能问题。如果列表项的顺序可能发生变化,使用一个稳定的唯一标识符作为key
  1. 减少不必要的计算和副作用
  • 检查代码中是否存在不必要的计算或副作用。例如,避免在render方法中进行复杂的计算或发起网络请求。将这些操作移到生命周期方法或使用useEffect钩子中,并确保副作用的依赖项正确设置,以避免不必要的执行。
  • 对于频繁执行的计算,可以考虑使用 memoization(记忆化)技术来缓存结果,避免重复计算。
  1. 优化网络请求
  • 检查应用中的网络请求是否高效。避免频繁的重复请求,使用缓存策略来减少请求次数。确保网络请求的响应时间合理,可以使用工具来监测网络请求的性能,并考虑优化服务器端的响应时间。

通过以上方法,可以系统地排查 React 应用中的性能问题,并采取相应的优化措施来提高应用的性能和响应速度。

如何确定哪个数据变化引起的组件渲染?

帮助开发者排查是哪个属性改变导致了组件的 rerender。

直接接受 ahooks 里面的一个方法: useWhyDidYouUpdate

源码实现:

import { useEffect, useRef } from 'react'

export type IProps = Record<string, any>;

export default function useWhyDidYouUpdate (componentName: string, props: IProps) {
const prevProps = useRef<IProps>({})

useEffect(() => {
if (prevProps.current) {
const allKeys = Object.keys({ ...prevProps.current, ...props })
const changedProps: IProps = {}

allKeys.forEach((key) => {
if (!Object.is(prevProps.current[key], props[key])) {
changedProps[key] = {
from: prevProps.current[key],
to: props[key]
}
}
})

if (Object.keys(changedProps).length) {
console.log('[why-did-you-update]', componentName, changedProps)
}
}

prevProps.current = props
})
}
55%