面向 Vue3 开发者的 React 快速上手系列
Vue 里你天天用 computed------依赖变了自动重算、自动缓存,几乎不用操心性能。React 里对应的是 useMemo,但心智完全不同 :因为 React 组件每次渲染都重跑整个函数,React 需要你显式告诉它什么时候可以复用上次结果。
一、先破除一个误区:大多数"派生值"根本不需要 useMemo
在 Vue,几乎所有派生数据你都会写成 computed。但在 React,普通计算直接写就行,因为每次渲染重新算一遍通常很快:
jsx
function Demo({ firstName, lastName, price, qty }) {
// ✅ 直接算,不需要任何 Hook
const fullName = `${firstName} ${lastName}`
const total = price * qty
const isExpensive = total > 1000
return <p>{fullName}:{total} 元</p>
}
对标 Vue 你会写三个 computed,但在 React 这些都是廉价计算,直接写反而更清晰。不要一上来就给所有派生值套 useMemo------那是过度优化。
二、useMemo:只为"昂贵计算"缓存
只有当计算真的很昂贵 (大数组排序/过滤、复杂运算)时,才用 useMemo 缓存结果,避免每次渲染都重算:
jsx
import { useMemo } from 'react'
function ProductList({ products, keyword }) {
// 昂贵的过滤+排序,只在 products 或 keyword 变化时重算
const visible = useMemo(() => {
return products
.filter(p => p.name.includes(keyword))
.sort((a, b) => b.sales - a.sales)
}, [products, keyword]) // ← 依赖数组,等价于 computed 的依赖
return <ul>{visible.map(p => <li key={p.id}>{p.name}</li>)}</ul>
}
对标 Vue:
js
// Vue3:computed 自动追踪 products / keyword,自动缓存
const visible = computed(() =>
products.value.filter(p => p.name.includes(keyword.value))
.sort((a, b) => b.sales - a.sales)
)
核心区别再强调一次:
- Vue
computed自动 收集依赖、自动缓存; - React
useMemo需要你手动写依赖数组,且它只是"性能优化提示",React 不保证一定缓存。
三、useCallback:缓存"函数"本身
这个 Vue 里几乎没有对应物,因为 Vue 的 setup 只运行一次,函数天然只创建一次。但 React 每次渲染都会重新创建函数,导致传给子组件的回调"每次都是新的",可能引发子组件不必要的重渲染。
jsx
import { useCallback } from 'react'
function Parent({ userId }) {
// ❌ 每次渲染都创建新函数,引用都不同
const handleClick = () => doSomething(userId)
// ✅ 只在 userId 变化时创建新函数,引用稳定
const handleClick2 = useCallback(() => doSomething(userId), [userId])
return <ExpensiveChild onClick={handleClick2} />
}
useCallback(fn, deps) 本质上等价于 useMemo(() => fn, deps)------一个缓存值,一个缓存函数。
四、React.memo:让子组件"props 没变就不重渲染"
useCallback 通常和 React.memo 搭配才有意义。默认情况下,父组件重渲染,所有子组件都会跟着重渲染(哪怕 props 没变)。这点和 Vue 不同------Vue 靠响应式做到精准更新,React 默认是"全家桶重渲染"。
React.memo 包裹的组件,会在 props 浅比较相等时跳过重渲染:
jsx
import { memo } from 'react'
// 只有当 props 变化时才重渲染
const ExpensiveChild = memo(function ExpensiveChild({ onClick, data }) {
console.log('子组件渲染')
return <button onClick={onClick}>{data.name}</button>
})
三者的配合关系:
React.memo让子组件在 props 不变时跳过渲染;- 但如果父组件每次传的
onClick都是新函数,浅比较就不相等,memo就失效了; - 所以要用
useCallback稳定回调引用,用useMemo稳定对象/数组引用。
jsx
function Parent() {
const [count, setCount] = useState(0)
const handleClick = useCallback(() => console.log('click'), []) // 稳定引用
const data = useMemo(() => ({ name: 'Tom' }), []) // 稳定引用
return (
<>
<button onClick={() => setCount(count + 1)}>{count}</button>
<ExpensiveChild onClick={handleClick} data={data} /> {/* 不会跟着重渲染 */}
</>
)
}
五、React 19 的好消息:React Compiler
手动到处写 useMemo/useCallback/memo 确实繁琐------这正是 Vue 开发者觉得"React 好累"的主要原因。
好消息:React 19 引入了 React Compiler ,它能在编译期自动帮你做记忆化(memoization),让你几乎不用再手写 useMemo/useCallback。这让 React 的开发体验向 Vue 的"自动优化"靠拢了一大步。
启用 Compiler 后,你可以像写 Vue 一样直接写派生值和函数,编译器自动优化。目前它逐步普及中,但方向已经明确:未来手写 memo 的场景会越来越少。
建议:新项目开启 React Compiler;在它未覆盖的关键性能路径,才手动用
useMemo/useCallback。
六、何时该用、何时别用(决策表)
| 场景 | 该用吗 |
|---|---|
| 简单派生值(拼字符串、算总价) | ❌ 直接写 |
| 大数组过滤/排序/复杂运算 | ✅ useMemo |
传给 React.memo 子组件的回调 |
✅ useCallback |
传给 React.memo 子组件的对象/数组 |
✅ useMemo |
| 普通事件处理函数(不往下传) | ❌ 直接写 |
| 已启用 React 19 Compiler | 多数情况交给编译器 |
黄金法则:先写正确的代码,遇到实测的性能问题再优化。 不要为了 memo 而 memo------过度记忆化本身也有成本,还会让代码变丑。
七、对照速查
| 需求 | Vue3 | React |
|---|---|---|
| 简单派生值 | computed |
直接写变量 |
| 昂贵派生值 | computed(自动缓存) |
useMemo(fn, deps) |
| 稳定函数引用 | 无需(setup 只跑一次) | useCallback(fn, deps) |
| 跳过子组件重渲染 | 自动(响应式精准更新) | React.memo + 稳定引用 |
| 依赖收集 | 自动 | 手动写依赖数组 |
小结
- 普通派生值直接写 ,别滥用
useMemo------这是 Vue 开发者最常犯的过度优化。 useMemo缓存昂贵计算结果;useCallback缓存函数引用。- React 默认"父渲染子全渲染",用
React.memo+ 稳定引用来精准跳过。 - React 19 的 Compiler 正在把这些手动优化自动化,体验向 Vue 靠拢。
- 原则:先正确,再按需优化。
最后一篇,我们从零用 Vite 搭一个 React 项目,写一个完整的 Todo 应用,把全系列知识点串起来实战。