在前四篇中,我们走完了 React 核心架构的”主线任务”:Fiber、渲染流程、Hooks、并发模式。从这一篇开始,我们进入”支线副本”——那些你日常开发中高频使用,但底层原理很少有人说清楚的 API。
本篇覆盖两个主题:Memoization 双子星(useMemo / useCallback) 和 Suspense 机制 。它们一个关于”跳过不必要的计算”,一个关于”等待异步数据”,但底层都深深扎根在 Fiber 链表和工作循环中。
第一部分:性能优化的双子星——useMemo 与 useCallback useMemo 和 useCallback 在社区里经常被过度使用,也经常被误解。要搞清楚什么时候该用、什么时候不该用,最好的方式就是看看它们在源码里到底做了什么——你会发现,它们的实现朴素得令人意外 。
1. 底层存储结构 在第三篇中我们讲过,所有 Hooks 的数据都挂在 Fiber 节点的 memoizedState 链表上。useMemo 和 useCallback 也不例外,它们存的只是一个简单的二元组:
1 2 3 4 5 hook.memoizedState = [ value, deps ];
没有缓存淘汰策略,没有 LRU,没有任何复杂的数据结构——就是一个数组,存一个值和一份依赖。
2. Mount 阶段:首次渲染 首次渲染时,React 使用 mountMemo 和 mountCallback:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function mountMemo (nextCreate, deps ) { const hook = mountWorkInProgressHook (); const nextDeps = deps === undefined ? null : deps; const nextValue = nextCreate (); hook.memoizedState = [nextValue, nextDeps]; return nextValue; } function mountCallback (callback, deps ) { const hook = mountWorkInProgressHook (); const nextDeps = deps === undefined ? null : deps; hook.memoizedState = [callback, nextDeps]; return callback; }
注意两者的区别:
mountMemo:执行 nextCreate(),存的是返回值
mountCallback:不执行 callback,存的是函数引用本身
3. Update 阶段:后续渲染的”找不同” 后续渲染时,React 使用 updateMemo 和 updateCallback,核心逻辑就是比较依赖数组 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 function updateMemo (nextCreate, deps ) { const hook = updateWorkInProgressHook (); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState ; if (prevState !== null && nextDeps !== null ) { const prevDeps = prevState[1 ]; if (areHookInputsEqual (nextDeps, prevDeps)) { return prevState[0 ]; } } const nextValue = nextCreate (); hook.memoizedState = [nextValue, nextDeps]; return nextValue; } function updateCallback (callback, deps ) { const hook = updateWorkInProgressHook (); const nextDeps = deps === undefined ? null : deps; const prevState = hook.memoizedState ; if (prevState !== null && nextDeps !== null ) { const prevDeps = prevState[1 ]; if (areHookInputsEqual (nextDeps, prevDeps)) { return prevState[0 ]; } } hook.memoizedState = [callback, nextDeps]; return callback; }
关键的比较函数 areHookInputsEqual 在第三篇已经见过:
1 2 3 4 5 6 7 8 9 function areHookInputsEqual (nextDeps, prevDeps ) { for (let i = 0 ; i < prevDeps.length ; i++) { if (Object .is (nextDeps[i], prevDeps[i])) { continue ; } return false ; } return true ; }
Object.is 做的是引用比较 (对于对象)和值比较 (对于原始类型)。这就解释了一个常见的坑:
1 2 3 4 const result = useMemo (() => expensiveCalc (data), [{ id : 1 }]);
4. useCallback 的本质:useMemo 的语法糖 从源码可以看出,useCallback(fn, deps) 和 useMemo(() => fn, deps) 在行为上完全等价 :
1 2 3 const memoizedFn = useCallback (fn, deps);const memoizedFn = useMemo (() => fn, deps);
区别只是 useCallback 省去了那层包裹的箭头函数——React 源码里甚至有注释说明这一点。
5. 关键误区:useCallback 不能阻止函数创建 这是社区里最常见的误解:
误区 :用了 useCallback 就不会创建新函数了。
真相 :JavaScript 引擎在解析到函数表达式的那一刻,就已经创建了函数对象。useCallback 改变不了这个事实。
1 2 3 4 5 6 7 8 function Parent ( ) { const handleClick = useCallback (() => { console .log ('clicked' ); }, []); return <Child onClick ={handleClick} /> ; }
useCallback 做的事情是:每次渲染都创建了新函数,但如果依赖没变,React 把新函数扔掉,返回旧函数的引用 。这样 handleClick 在两次渲染之间保持了引用稳定。
6. 什么时候该用,什么时候不该用 理解了源码之后,判断标准就很清晰了:
useMemo 该用的场景:
1 2 3 4 5 6 7 8 9 10 11 const sortedList = useMemo ( () => hugeArray.sort ((a, b ) => a.score - b.score ), [hugeArray] ); const fullName = useMemo ( () => `${firstName} ${lastName} ` , [firstName, lastName] );
useCallback 该用的场景:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const MemoChild = React .memo (function Child ({ onClick } ) { return <button onClick ={onClick} > Click</button > ; }); function Parent ( ) { const handleClick = useCallback (() => { }, []); return <MemoChild onClick ={handleClick} /> ; } function Parent ( ) { const handleClick = useCallback (() => { }, []); return <NormalChild onClick ={handleClick} /> ; }
7. React.memo:useMemo/useCallback 的”搭档” 单独说一下 React.memo,因为它和上面两个 Hook 是配套使用的。
React.memo 是一个高阶组件 ,它在组件外层加了一层浅比较:
1 2 3 4 5 6 7 8 9 10 function memo (Component, compare ) { function MemoComponent (props ) { } MemoComponent .$$typeof = REACT_MEMO_TYPE ; MemoComponent .compare = compare || shallowEqual; return MemoComponent ; }
在 beginWork 阶段,React 遇到 Memo 类型的组件时会:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function updateMemoComponent (current, workInProgress, renderLanes ) { const Component = workInProgress.type ; const compare = Component .compare || shallowEqual; const prevProps = current.memoizedProps ; const nextProps = workInProgress.pendingProps ; if (compare (prevProps, nextProps)) { return bailoutOnAlreadyFinishedWork (current, workInProgress); } return updateFunctionComponent (current, workInProgress, renderLanes); }
shallowEqual 的逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function shallowEqual (objA, objB ) { if (Object .is (objA, objB)) return true ; const keysA = Object .keys (objA); const keysB = Object .keys (objB); if (keysA.length !== keysB.length ) return false ; for (let i = 0 ; i < keysA.length ; i++) { if ( !objB.hasOwnProperty (keysA[i]) || !Object .is (objA[keysA[i]], objB[keysA[i]]) ) { return false ; } } return true ; }
所以完整的优化链条是:
graph TD
A["Parent 重新渲染"] --> B["useCallback 保证<br/>handleClick 引用不变"]
B --> C["传 props 给 MemoChild"]
C --> D["React.memo 用 shallowEqual<br/>比较 props"]
D --> E["handleClick 引用没变<br/>→ props 相同"]
E --> F["跳过 MemoChild 的渲染 bailout"]
三者缺一不可:没有 React.memo,useCallback 白费;没有 useCallback,React.memo 每次都通不过 props 比较。
第二部分:Suspense 的悬停魔法 如果说 Memoization 是”跳过不必要的工作”,那 Suspense 就是”等待还没准备好的工作”。React Suspense 的实现方式相当”离经叛道”——它利用了 JavaScript 的错误处理机制 来实现异步流程控制。
1. 核心原理:抛出 Promise 在传统的 React 组件中,如果数据还没加载完,你通常会这样写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function UserProfile ({ userId } ) { const [data, setData] = useState (null ); const [loading, setLoading] = useState (true ); useEffect (() => { fetchUser (userId).then (d => { setData (d); setLoading (false ); }); }, [userId]); if (loading) return <Spinner /> ; return <div > {data.name}</div > ; }
Suspense 的思路完全不同——组件不管理加载状态,而是在数据没准备好时直接”中断”自己的渲染 :
1 2 3 4 5 6 7 8 9 10 11 12 13 function UserProfile ({ userId } ) { const data = resource.read (userId); return <div > {data.name}</div > ; } <Suspense fallback={<Spinner /> }> <UserProfile userId ={1} /> </Suspense >
resource.read() 内部的实现大致是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function createResource (fetcher ) { let status = 'pending' ; let result; const promise = fetcher ().then ( (data ) => { status = 'success' ; result = data; }, (error ) => { status = 'error' ; result = error; } ); return { read ( ) { switch (status) { case 'pending' : throw promise; case 'error' : throw result; case 'success' : return result; } }, }; }
注意这里最关键的一行:throw promise 。这不是 throw new Error(),而是抛出了一个 Promise 对象。这是 Suspense 整个机制的核心。
2. React 工作循环中的 try…catch 在第一篇中我们讲过,React 的工作循环(workLoop)会逐个处理 Fiber 节点。为了支持 Suspense,这个循环被包裹在一个 try...catch 中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function renderRoot (root, lanes ) { prepareFreshStack (root, lanes); do { try { workLoop (); break ; } catch (thrownValue) { handleThrow (root, thrownValue); } } while (true ); }
handleThrow 的逻辑是判断被抛出的东西是什么类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function handleThrow (root, thrownValue ) { if ( thrownValue !== null && typeof thrownValue === 'object' && typeof thrownValue.then === 'function' ) { const wakeable = thrownValue; throwException (root, workInProgress, wakeable); } else { throw thrownValue; } }
3. 挂起(Suspend):Suspense 边界的查找 当 React 确认这是一个 Suspense 场景后,它需要做两件事:
第一步:向上查找最近的 Suspense 边界
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function throwException (root, sourceFiber, wakeable ) { sourceFiber.flags |= Incomplete ; let node = sourceFiber.return ; while (node !== null ) { if (node.tag === SuspenseComponent ) { const wakeables = node.updateQueue ; if (wakeables === null ) { node.updateQueue = new Set ([wakeable]); } else { wakeables.add (wakeable); } node.flags |= ShouldCapture ; return ; } node = node.return ; } }
第二步:渲染 fallback
找到 Suspense 边界后,React 会让这个 Suspense 组件渲染它的 fallback 而不是 children:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function updateSuspenseComponent (current, workInProgress ) { const nextProps = workInProgress.pendingProps ; const showFallback = isSuspended (workInProgress); if (showFallback) { const fallbackChildren = nextProps.fallback ; const primaryChildFragment = createFiberFromOffscreen (nextProps.children ); primaryChildFragment.mode |= OffscreenMode ; const fallbackChildFragment = createFiberFromFragment (fallbackChildren); workInProgress.child = primaryChildFragment; primaryChildFragment.sibling = fallbackChildFragment; primaryChildFragment.return = workInProgress; fallbackChildFragment.return = workInProgress; return fallbackChildFragment; } else { const primaryChildren = nextProps.children ; return reconcileChildren (current, workInProgress, primaryChildren); } }
4. 恢复(Resume):Promise 决议后的重渲染 挂起后,React 需要在 Promise 决议时收到通知。它会给 Promise 附加一个回调:
1 2 3 4 5 6 7 8 9 10 function attachPingListener (root, wakeable, lanes ) { const ping = ( ) => { ensureRootIsScheduled (root); }; wakeable.then (ping, ping); }
当 Promise 决议后,整个流程重新来一遍:
graph TD
subgraph render1["第一次渲染"]
A1["UserProfile 执行"] --> A2["resource.read → throw Promise"]
A2 --> A3["React catch → 找到 Suspense"]
A3 --> A4["渲染 Spinner,监听 Promise"]
end
A4 --> B["Promise 决议<br/>ping 触发 → 安排新一轮渲染"]
subgraph render2["第二次渲染"]
C1["UserProfile 执行"] --> C2["resource.read → 返回 data"]
C2 --> C3["正常渲染内容"]
C3 --> C4["隐藏 fallback,显示 children"]
end
B --> C1
用时间线来表示:
graph LR
A["Render 1<br/>throw Promise<br/>渲染 Spinner"] --> B["网络请求进行中..."]
B --> C["Promise resolved<br/>ping → scheduleWork"]
C --> D["Render 2<br/>正常返回 data<br/>Spinner 消失,内容出现"]
5. Suspense 与并发模式的协作 Suspense 在并发模式下会变得更加强大。当 Suspense 与 useTransition 配合时,React 可以在”等待数据”的同时保持旧 UI ,而不是立即显示 Loading:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function App ( ) { const [tab, setTab] = useState ('home' ); const [isPending, startTransition] = useTransition (); const switchTab = (newTab ) => { startTransition (() => { setTab (newTab); }); }; return ( <div > <TabBar currentTab ={tab} onSwitch ={switchTab} /> <div style ={{ opacity: isPending ? 0.7 : 1 }}> <Suspense fallback ={ <Spinner /> }> <TabContent tab ={tab} /> </Suspense > </div > </div > ); }
在这个场景下:
用户点击新 Tab
startTransition 标记这个更新为低优先级
React 开始在后台渲染新 Tab 的内容
如果新 Tab 的组件 throw Promise(数据还没好),React 不会 立即显示 Spinner
而是继续显示旧 Tab 的内容(加上 isPending 的半透明效果)
直到数据加载完成、新界面完全准备好后,一次性切换过去
这就是”避免 Loading 闪烁”的底层机制——并发模式让 React 可以选择性地延迟 Suspense fallback 的显示 。
6. Suspense 的边界嵌套 多个 Suspense 可以嵌套使用,React 会按照”就近原则”向上查找边界:
1 2 3 4 5 6 7 8 9 10 <Suspense fallback={<PageSkeleton /> }> <Header /> <Suspense fallback ={ <ContentSpinner /> }> <MainContent /> <Suspense fallback ={ <SidebarSkeleton /> }> <Sidebar /> </Suspense > </Suspense > <Footer /> </Suspense >
如果 Sidebar 的数据还没好,只有最内层的 Suspense 会显示 <SidebarSkeleton />,Header、MainContent、Footer 都不受影响。
如果 MainContent 也挂了,中间层的 Suspense 接管,显示 <ContentSpinner />——此时 Sidebar 的 Suspense 也被包含在 fallback 范围内。
这种嵌套设计让你可以细粒度地控制加载体验:
graph TD
subgraph s1["Sidebar 未就绪"]
direction LR
A1["Header ✓"] --- A2["MainContent ✓"] --- A3["SidebarSkeleton"]
end
subgraph s2["MainContent 未就绪"]
direction LR
B1["Header ✓"] --- B2["ContentSpinner"] --- B3["Footer ✓"]
end
subgraph s3["全部未就绪"]
C1["PageSkeleton"]
end
总结
主题
核心机制
一句话概括
useMemo
deps 浅比较 → 复用旧值或重新计算
用比较的开销换取计算的跳过
useCallback
deps 浅比较 → 复用旧函数引用
useMemo 的语法糖,核心是引用稳定性
React.memo
props 浅比较 → bailout 跳过渲染
useCallback 的搭档,缺一不可
Suspense
throw Promise → catch → 渲染 fallback → Promise 决议 → 重新渲染
用”假装报错”实现异步流程控制
Memoization 是关于比较与跳过 ——通过对比依赖数组,决定是复用旧值还是计算新值。
Suspense 是关于中断与恢复 ——通过抛出 Promise 中断渲染,利用 Promise 的状态变化重启渲染。
两者在底层都依赖 Fiber 架构提供的基础能力:链表存储让数据有地方放,可中断的工作循环让异常能被安全处理。
Happy Coding!