CC 咖啡猫的工作空间 Coding Space

React 18+ 深度笔记

本文聚焦 React 18,覆盖 Fiber 架构、Hooks 原理、并发特性、状态管理(zustand/RTK)、React 19 新特性等核心内容。


1. 核心概念

1.1 JSX

JSX 是 React.createElement 的语法糖,编译后生成 React Element(一个描述节点的普通对象):

// JSX
<div className="container">
  <span>Hello</span>
</div>

// 编译后
React.createElement('div', { className: 'container' },
  React.createElement('span', null, 'Hello')
)

// 结果(React Element)
{
  $$typeof: Symbol(react.element),
  type: 'div',
  props: { className: 'container', children: { type: 'span', ... } },
  key: null,
  ref: null
}

$$typeof 用于防止 XSS 攻击 — JSON 无法序列化 Symbol,服务器返回的恶意 JSON 无法伪造 React 元素。

1.2 虚拟 DOM

React 的 Virtual DOM 与 Vue 的理念一致,但实现差异大:

  • React 的虚拟 DOM 是一棵不可变的 React Element 树
  • React 的 Diff 是在 Fiber 节点上进行的、可中断的过程
  • React 使用 双缓冲(current tree + workInProgress tree)来支持中断恢复

1.3 Diff 算法(Reconciliation)

React 的 Diff 基于三个假设(策略):

  1. 树层级比较 — 只对同一层级的节点做 diff,跨层级移动视为销毁+重建(O(n^3) 降为 O(n))
  2. key 稳定假设 — 同 key 组件尽量复用
  3. 类型决定 — 节点类型变化(div → span)直接销毁重建子树

Diff 流程(Fiber 架构下):

  1. 比较新旧 fiber 节点的 key + type
  2. 匹配则打上 Update flag,复用 DOM
  3. 不匹配则标记 Placement(新增)或 Deletion(删除)

1.4 key 的作用

  • 帮助 React 识别哪些节点发生了变化、被添加或移除
  • 避免使用 index:增加/删除中间项会导致 key 错位,引发渲染错误和性能问题
  • 组件 key 变化导致卸载后重新挂载(可用于强制重置组件状态)

2. Fiber 架构

2.1 为什么需要 Fiber

React 16 之前的递归 Diff(Stack Reconciler)不可中断,一旦开始必须执行完整个递归树。对于复杂组件树:

  • 帧率问题:一次更新占用主线程过长(>16.6ms),导致掉帧
  • 无法优先级调度:无法区分紧急(用户输入)和非紧急(数据加载)更新

Fiber 的核心目标:将渲染过程拆分为可中断的小单元,实现可中断、可复用、可优先级的异步渲染。

2.2 Fiber 节点结构

每个 React 元素都对应一个 Fiber 节点:

interface Fiber {
  tag: WorkTag              // 节点类型(FunctionComponent/HostComponent/...)
  key: string | null
  elementType: any
  type: any                 // 函数组件本身、DOM 标签名等
  stateNode: any            // 关联的真实 DOM / 组件实例

  // 链表结构
  return: Fiber | null      // 父节点
  child: Fiber | null       // 第一个子节点
  sibling: Fiber | null     // 下一个兄弟节点

  // 双缓冲
  alternate: Fiber | null   // 对应 current 或 workInProgress 树的同级节点

  // 副作用
  flags: Flags              // 操作标记(Placement/Update/Deletion/...)
  subtreeFlags: Flags       // 子树副作用标记(快速跳过无副作用的子树)

  // 优先级
  lanes: Lanes
  childLanes: Lanes

  // 更新队列
  memoizedState: any        // 当前状态(Hooks 链表头)
  memoizedProps: any
  updateQueue: any

  // 副作用列表(已完成 render 阶段后收集)
  firstEffect: Fiber | null
  nextEffect: Fiber | null
  lastEffect: Fiber | null
}

Fiber 用链表取代树:通过 return/child/sibling 三指针构成一棵单向链表树,使遍历可中断(暂停后从指针恢复)。

2.3 双缓冲树

current(当前屏幕显示)     workInProgress(内存中构建)
    FiberRoot                    FiberRoot
    └── RootFiber ←—— alternate ——→ RootFiber
         ├── App (alternate) ←——→ App
         ├── Header (alt)   ←——→ Header
         └── ...
  • current:当前已渲染在屏幕上的 Fiber 树
  • workInProgress:正在构建中的替代树(内存中)
  • alternate 连接两个树的对应节点
  • 构建完成后,current 指针切换为 workInProgresscommitRoot 时),旧树成为下一次的 workInProgress(复用 Fiber 对象

2.4 工作循环(workLoop)

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress)
  }
}

function shouldYield() {
  // 检查是否超时(默认 5ms 时间片)
  return deadlineDidExpire || navigator.scheduling?.isInputPending()
}

performUnitOfWork 分为两个阶段:

  1. beginWork(递):处理当前节点,diff + 生成子 fiber,返回 child
    • 节点无更新且优先级不够 → bailout(跳过子树)
    • 有更新 → 打 flag,继续深入
  2. completeUnitOfWork(归):构建副作用列表(effect list),收集 flags,返回 sibling

2.5 调度器(Scheduler)

React Scheduler 实现了一个基于优先级的任务调度系统

  • 使用 MessageChannel(宏任务)实现时间切片,避免阻塞事件队列
  • 任务队列用小顶堆(min-heap)按到期时间排序
  • 每次工作循环检查时间切片是否到期,到期则暂停,将控制权交还浏览器

调度优先级

ImmediatePriority  → 同步执行(过期后立即执行)
UserBlockingPriority → 用户交互(点击、输入)
NormalPriority   → 常规更新
LowPriority    → 低优先级数据加载
IdlePriority    → 空闲时处理

2.6 时间切片(Time Slicing)

  • 每次 requestAnimationFrame 回调获取 frameLength(默认 5ms)
  • Scheduler 在这个时间片内执行 Fiber 工作单元
  • 超时则挂起当前工作,将任务放回队列,让浏览器处理事件和渲染
  • 下次空闲继续上次中断的 fiber

2.7 Lane 模型(优先级)

React 18 用 32 位二进制的 lanes 替代了之前的 expirationTime

NoLanes = 0b000...000
SyncLane = 0b000...001
InputContinuousLane = 0b000...010
DefaultLane = 0b000...100
TransitionLane = 0b000...1000
IdleLane = 0b111...111 (高位)
  • 位数越低优先级越高
  • 位运算快速判断优先级关系和集合操作
  • 一个更新可能同时属于多个 lane(如 transition 更新)

3. Hooks

3.1 useState

const [state, setState] = useState(initialValue)

批量更新(Batching)

  • React 18 之前在事件处理函数中自动批处理;setTimeout/Promise 回调中不会
  • React 18 所有更新都会自动批处理(包括 setTimeout、Promise、原生事件)
setCount(c => c + 1)   // 函数式更新:基于旧值计算,能拿到最新状态
setCount(count + 1)    // 直接传值:可能受闭包影响拿到过期值

3.2 useEffect

挂载:     → effect 函数执行
更新:     → 清理函数执行(基于旧值) → effect 函数执行(新值)
卸载:     → 清理函数执行
useEffect(() => {
  // effect
  return () => { /* 清理 */ }
}, [dep1, dep2])
  • 在浏览器完成布局和绘制之后异步执行(延迟执行,不阻塞渲染)
  • 依赖数组执行 Object.is 浅比较(引用比较)
  • 空依赖 [] 只在挂载时执行一次
  • 不传依赖时每次 render 后都执行

与生命周期的关系:useEffect 模拟 componentDidMount + componentDidUpdate + componentWillUnmount,但语义不同:它关注同步而非生命周期阶段。

3.3 useLayoutEffect

useEffect 完全相同的签名,但执行时机不同:

useEffect useLayoutEffect
执行时机 浏览器绘制之后(异步) DOM 更新后、绘制之前(同步)
阻塞绘制
适用场景 副作用、数据请求、日志 DOM 测量、同步修改布局(避免闪烁)

原则:能用 useEffect 就不用 useLayoutEffect,因为它会阻塞视觉更新。

3.4 useRef

const ref = useRef(initialValue)
// ref.current 可读写,不引发重新渲染
  • 保存可变值的"盒子",ref.current 在每次渲染间保持不变
  • 用于访问 DOM(<div ref={ref}>)或存储任意可变值(定时器 ID、前一次值等)
  • useRef(null) 的实现:{ current: null },React 保证在组件整个生命周期中返回同一对象引用

3.5 useMemo / useCallback

const memoizedValue = useMemo(() => expensive(a, b), [a, b])
const memoizedFn = useCallback(() => doSomething(a, b), [a, b])
// 等价于 useMemo(() => fn, deps)

缓存原理

  • 首次渲染执行函数,缓存结果
  • 依赖变化时重新计算,覆盖缓存
  • 依赖不变则直接返回缓存引用

不要滥用

  • useMemo 本身有计算成本(依赖比较 + 内存占用)
  • 只有昂贵计算或作为引用稳定性场景(传递给 React.memo 子组件)才有意义
  • 过早优化不如不优化 — 优先分析性能瓶颈

3.6 useReducer

适合复杂状态逻辑(多值、状态间依赖):

const [state, dispatch] = useReducer(reducer, initialState, init?)

内部实现复用 useState:将 updater 替换为 dispatch,在 reducer 中计算 newState。

3.7 useContext / use(React 19)

const theme = useContext(ThemeContext)

use(React 19 实验性):在 render 中直接读取 Promise 或 Context,支持条件性使用:

const data = use(fetchData())       // 读取 Promise(配合 Suspense)
const theme = use(ThemeContext)     // 替代 useContext

useContext 区别:use 可在条件分支和循环中使用,useContext 必须在顶层。

3.8 自定义 Hook 设计

自定义 Hook 是以 use 开头的函数,通过组合内置 Hook 实现逻辑复用:

function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 })
  useEffect(() => {
    const handler = () => setSize({ width: window.innerWidth, height: window.innerHeight })
    window.addEventListener('resize', handler)
    return () => window.removeEventListener('resize', handler)
  }, [])
  return size
}

设计原则

  • Hook 之间是独立的 闭包隔离,不是 Mixin 的合并
  • 多个自定义 Hook 可组合(Hook 调用 Hook)
  • 以状态和副作用为核心抽象,非生命周期

4. 并发特性(React 18)

4.1 并发模式(Concurrent Mode)

并发模式不是某个 API,而是 React 18 的底层渲染架构变化。核心价值:

  • 可中断渲染:高优更新可以打断低优更新
  • 过渡更新不阻塞 UI:UI 更新期间仍可响应输入
  • 状态复用:中断的渲染状态可被丢弃,直接回到旧状态

启用并发模式需要 createRoot(替代 ReactDOM.render):

// React 18
const root = createRoot(container)
root.render(<App />)

4.2 useTransition

标记非紧急更新,让 React 为低优更新降级优先级:

function SearchPage() {
  const [query, setQuery] = useState('')
  const [isPending, startTransition] = useTransition()

  return (
    <>
      <input value={query} onChange={e => {
        setQuery(e.target.value)          // 紧急:更新输入框
        startTransition(() => {
          setSearchResults(filter(e.target.value))  // 非紧急:过滤结果
        })
      }} />
      {isPending && <Spinner />}
    </>
  )
}

应用场景:大量列表过滤、搜索建议、标签切换。

4.3 useDeferredValue

useTransition 类似,但用于父组件无法控制更新的场景:

const deferredQuery = useDeferredValue(query)
// deferredQuery 延迟更新,在紧急渲染完成后才更新
const suggestions = useMemo(() => filter(list, deferredQuery), [list, deferredQuery])

区别

  • useTransition:包装状态更新逻辑(函数)
  • useDeferredValue:包装状态值(值)

4.4 自动批处理(Automatic Batching)

React 18 中所有更新自动批处理(不再区分事件内外):

// React 17:setTimeout 中触发两次渲染
// React 18:只触发一次渲染
setTimeout(() => {
  setCount(c => c + 1)
  setFlag(f => !f)
}, 100)

如需取消批处理可用 flushSync

import { flushSync } from 'react-dom'
flushSync(() => setCount(c => c + 1))  // 立即渲染
flushSync(() => setFlag(f => !f))      // 再次渲染

4.5 Suspense(数据获取 / 代码分割 / 流式 SSR)

// 代码分割
const LazyComp = React.lazy(() => import('./LazyComp'))

<Suspense fallback={<Loading />}>
  <LazyComp />
</Suspense>

// 数据获取(配合 Relay / SWR / React 19 use())
<Suspense fallback={<Skeleton />}>
  <UserProfile userId={id} />
</Suspense>

在 Concurrent 模式下,Suspense 可以「暂停」渲染,不卸载原有内容,只需展示 fallback。


5. 状态管理

5.1 Context 的性能问题与优化

Context 的核心问题:所有消费该 Context 的组件都会在 Context 值变化时重新渲染,即使子组件只读取了其中一部分数据。

const AppContext = createContext({ user, theme })

// ❌ 问题:theme 变化导致只用了 user 的组件也重新渲染

解决方案

  1. 拆分 Context
const UserContext = createContext(user)
const ThemeContext = createContext(theme)
  1. 使用 useMemo:避免 provider value 每次重渲染生成新引用
const value = useMemo(() => ({ user, theme }), [user, theme])
  1. use-context-selector(第三方库):通过订阅选择器避免不必要的重渲染

5.2 zustand 原理

Zustand 的核心实现 = 发布订阅模式 + 不可变更新

// 简化版原理
function createStore(createState) {
  let state = createState(set, get)
  const listeners = new Set()

  const set = (partial) => {
    state = Object.assign({}, state, typeof partial === 'function'
      ? partial(state) : partial)
    listeners.forEach(l => l(state))
  }
  const get = () => state
  const subscribe = (listener) => {
    listeners.add(listener)
    return () => listeners.delete(listener)
  }
  const api = { setState: set, getState: get, subscribe }
  return api
}

选择器 + 浅比较

const bears = useBearStore(s => s.bears)
// 默认使用 Object.is 比较,支持 shallow 中间件

5.3 Redux Toolkit (RTK)

createSlice:自动生成 reducer 和 actions,内置 immer 简化不可变更新:

const slice = createSlice({
  name: 'counter',
  initialState: { value: 0, user: null },
  reducers: {
    increment: (state) => { state.value += 1 },   // immer 代理
    setUser: (state, action) => { state.user = action.payload }
  }
})

createAsyncThunk:管理异步请求的 pending/fulfilled/rejected 三态:

const fetchUser = createAsyncThunk('user/fetch', async (id, { rejectWithValue }) => {
  const res = await api.getUser(id)
  if (!res.ok) return rejectWithValue(res.error)
  return res.data
})
// builder.addCase(fetchUser.fulfilled, ...)

RTK Query:缓存策略、自动失效、乐观更新:

const api = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: '/' }),
  endpoints: (builder) => ({
    getPosts: builder.query({ query: () => 'posts' }),
    addPost: builder.mutation({
      query: (body) => ({ url: 'posts', method: 'POST', body }),
      invalidatesTags: ['Posts']
    })
  })
})

5.4 总结 zustand vs RTK vs Context

方案 适用场景 学习成本 性能
Context 低频全局数据(主题、语言) 差(全部重渲染)
zustand 中小型应用、单页组件数据 优(选择器订阅)
RTK 大型应用、复杂异步逻辑 中高

6. React 19 新特性

6.1 Server Components (RSC)

服务端组件仅在服务端运行,不发送 JavaScript 到客户端:

  • 减少包体积:大型依赖仅在服务端执行
  • 直接访问后端资源:数据库、文件系统、内部 API
  • 自动代码分割:客户端组件边界就是分割点

RSC 与 SSR 的区别

RSC SSR
渲染时机 构建时或请求时 请求时
产物 序列化的 UI 描述(不是 HTML) HTML 字符串
交互性 不可交互(无 useState/useEffect) 可交互(hydrate 后)
重新获取 可重新请求组件树 需刷新整页
前后端思维 组件即前后端统一模型 服务端渲染 + 客户端水合

6.2 Server Actions

客户端可通过表单或事件调用服务端函数:

// server action(RSC 中标记)
async function submitForm(formData: FormData) {
  'use server'
  await saveToDB(formData)
}

// 客户端组件调用
<form action={submitForm}>...</form>

6.3 useOptimistic

乐观更新 UI(先显示预期结果,后端确认后校正):

const [optimisticMessages, addOptimistic] = useOptimistic(messages)

6.4 useFormStatus

获取表单提交状态(常用于子组件感知父表单状态):

const { pending, data } = useFormStatus()

7. 性能优化

7.1 React.memo(浅比较)

const Child = React.memo(({ data }: { data: Item }) => {
  return <ExpensiveComponent data={data} />
})
  • 默认对 props 做浅比较(Object.is 逐个字段比较)
  • 父组件重渲染但 props 未变 → 跳过子组件渲染
  • 配合 useMemo/useCallback 传递引用稳定的 props 才有意义

7.2 useMemo / useCallback 的正确场景

✅ 应该用:

  • 传递给 React.memo 子组件的 props(保持引用稳定)
  • 昂贵的计算(大量数据过滤/格式化)
  • 作为其他 Hook 的依赖

❌ 不应该用:

  • 所有函数/计算都包一层(增加比较成本)
  • 简单计算(不如直接用)
  • 没有 React.memo 子组件的回调函数

7.3 虚拟列表(react-window / react-virtuoso)

import { FixedSizeList } from 'react-window'

<FixedSizeList height={600} itemCount={10000} itemSize={50}>
  {({ index, style }) => <div style={style}>Item {index}</div>}
</FixedSizeList>

原理:计算可见范围 → 只渲染可见行 → 通过 padding-top/padding-bottom 撑开滚动高度。

7.4 代码分割(React.lazy + Suspense)

const Dashboard = React.lazy(() => import('./Dashboard'))

配合路由级分割效果最佳:

const routes = [
  { path: '/dashboard', element: <Suspense><Dashboard /></Suspense> }
]

7.5 避免内联函数/对象

内联函数/对象每次渲染创建新的引用,破坏 React.memo 的优化效果:

// ❌ 每次渲染创建新函数
<ExpensiveChild onClick={() => handleClick(id)} />

// ✅ useCallback 保持引用稳定
const handleClick = useCallback(() => { /* ... */ }, [id])
<ExpensiveChild onClick={handleClick} />

7.6 Profiler 分析

<Profiler id="Navigation" onRender={(id, phase, actualDuration) => {
  console.log({ id, phase, actualDuration })
}}>
  <Navigation />
</Profiler>

用于定位渲染瓶颈,分析 Commit 耗时和触发原因。


8. React Router v6

8.1 createBrowserRouter

const router = createBrowserRouter([
  {
    path: '/',
    element: <Root />,
    errorElement: <ErrorPage />,
    children: [
      { index: true, element: <Home /> },
      { path: 'users/:id', element: <User /> },
    ]
  }
])

8.2 Outlet

function Root() {
  return (
    <>
      <Header />
      <main><Outlet /></main>  {/* 子路由渲染位置 */}
      <Footer />
    </>
  )
}

8.3 loader / action

const route = {
  path: 'users/:id',
  loader: ({ params }) => fetch(`/api/users/${params.id}`),
  element: <User />,
  action: async ({ request }) => {
    const formData = await request.formData()
    return updateUser(formData)
  }
}

// 组件中获取
function User() {
  const data = useLoaderData()    // loader 返回的数据
  const navigation = useNavigation() // 加载状态
  const fetcher = useFetcher()    // 非导航请求
  return <fetcher.Form method="post">...</fetcher.Form>
}

8.4 懒加载

const UserRoute = {
  path: 'users',
  async lazy() {
    const { Users } = await import('./Users')
    return { Component: Users, loader: () => fetch('/api/users') }
  }
}