最近面试里遇到这个问题:
父组件中引用了一个子组件,父组件刷新了,子组件会刷新么?用什么来阻止?useMemo 和 React.memo 的区别是什么?
这个问题看起来很简单,但其实很容易答乱。因为里面同时涉及了三个概念:
- 父组件重新渲染
- 子组件是否重新渲染
- props 的引用是否稳定
先看一段代码:
const Child = React.memo(({ obj }) => {
console.log('child render')
return <div>{obj.a}</div>
})
function Parent() {
const [count, setCount] = useState(0)
const obj = useMemo(() => {
return { a: 1 }
}, [])
return (
<>
<button onClick={() => setCount(count + 1)}>
add
</button>
<Child obj={obj} />
</>
)
}点击按钮后,count 改变,Parent 会重新渲染。
那 Child 会不会重新渲染?
答案是:不会。
因为这里同时做了两件事:
Child被React.memo包裹了- 传给
Child的obj被useMemo缓存了,引用没有变
所以父组件更新时,React 会重新执行 Parent 函数,但检查到 Child 的 props 没变,就会跳过 Child 的重新渲染。
父组件更新,子组件默认会不会更新?
默认情况下:会。
函数组件的渲染,本质上就是重新执行函数。
function Parent() {
const [count, setCount] = useState(0)
return (
<>
<button onClick={() => setCount(count + 1)}>
add
</button>
<Child />
</>
)
}当 setCount 执行后:
Parent的 state 变化Parent重新执行- JSX 重新生成
- 子组件默认也会跟着进入渲染流程
所以可以先记住一句话:
父组件重新渲染时,子组件默认也会重新渲染。
这个“重新渲染”不一定代表真实 DOM 一定会更新。React 后面还会做 diff,如果内容没变化,真实 DOM 可能不会变。
但是组件函数是否执行,是另一个问题。
用什么阻止子组件重新渲染?
通常用 React.memo。
const Child = React.memo(function Child({ name }) {
console.log('child render')
return <div>{name}</div>
})React.memo 的作用是:
当父组件重新渲染时,如果子组件的 props 没有变化,就跳过子组件本身的重新渲染。
它比较的是 props。
<Child name="Elemen" />如果 name 还是 "Elemen",那么 Child 可以不重新执行。
但是这里有一个很重要的点:React.memo 默认是浅比较。
也就是说:
- 基本类型比较值
- 对象、数组、函数比较引用
为什么只用 React.memo 还不够?
看这个例子:
const Child = React.memo(({ obj }) => {
console.log('child render')
return <div>{obj.a}</div>
})
function Parent() {
const [count, setCount] = useState(0)
const obj = { a: 1 }
return (
<>
<button onClick={() => setCount(count + 1)}>
add
</button>
<Child obj={obj} />
</>
)
}这时候点击按钮,Child 还是会重新渲染。
原因是:
const obj = { a: 1 }每次 Parent 重新执行时,都会创建一个新的对象。
虽然内容看起来都是 { a: 1 },但是引用地址变了。
{ a: 1 } === { a: 1 } // false所以在 React.memo 看来:
prevProps.obj !== nextProps.objprops 变了,子组件就会重新渲染。
这也是为什么很多时候你明明用了 React.memo,但是发现没有效果。
useMemo 在这里解决了什么?
useMemo 用来缓存一个值。
const obj = useMemo(() => {
return { a: 1 }
}, [])依赖数组是空数组,表示这个对象只在组件第一次渲染时创建一次。
后面 Parent 因为 count 改变而重新渲染时,obj 还是之前那个对象引用。
所以:
prevProps.obj === nextProps.obj这时候 React.memo 才能判断出 props 没有变化,然后跳过 Child 的重新渲染。
也就是说,这道题里的关键不是只用了 React.memo,而是:
React.memo + useMemo一个负责缓存组件渲染结果,一个负责稳定传入的对象引用。
useMemo 和 React.memo 的区别
这两个名字很像,但它们不是同一层面的东西。
React.memo
React.memo 是包组件的。
const Child = React.memo(ChildComponent)它的目标是:
让组件在 props 没变时不要重新渲染。
它关注的是:组件要不要重新执行。
适合用在:
- 子组件渲染成本比较高
- 子组件 props 不经常变化
- 父组件更新很频繁
useMemo
useMemo 是写在组件内部的 Hook。
const value = useMemo(() => {
return expensiveCalc(data)
}, [data])它的目标是:
缓存一个计算结果或引用类型的值。
它关注的是:这个值要不要重新计算 / 重新创建。
适合用在:
- 复杂计算
- 过滤、排序、统计等比较耗时的逻辑
- 缓存对象、数组,避免引用每次都变
- 配合
React.memo稳定子组件 props
可以简单理解为:
| API | 缓存的是什么 | 用在哪里 | 解决的问题 |
|---|---|---|---|
React.memo | 组件 | 组件外面 | props 没变时,跳过子组件渲染 |
useMemo | 值 | 组件里面 | 依赖没变时,复用上一次的计算结果 |
再补一个函数的情况
如果传给子组件的是函数,也会遇到类似问题。
const Child = React.memo(({ onClick }) => {
console.log('child render')
return <button onClick={onClick}>child button</button>
})
function Parent() {
const [count, setCount] = useState(0)
const handleClick = () => {
console.log('click')
}
return (
<>
<button onClick={() => setCount(count + 1)}>
add
</button>
<Child onClick={handleClick} />
</>
)
}每次 Parent 重新渲染时,handleClick 都是一个新函数。
所以 Child 还是会重新渲染。
这种情况一般用 useCallback:
const handleClick = useCallback(() => {
console.log('click')
}, [])useCallback 可以理解为专门缓存函数引用的 useMemo。
const handleClick = useCallback(() => {}, [])
// 大致等价于
const handleClick = useMemo(() => {
return () => {}
}, [])所以常见组合是:
- 传对象 / 数组:
React.memo + useMemo - 传函数:
React.memo + useCallback
回到面试题
如果面试官问:
父组件刷新了,子组件会刷新么?
可以这样答:
默认情况下会。父组件 state 改变后会重新执行组件函数,子组件也会跟着进入渲染流程。但是这不一定代表真实 DOM 一定更新,React 还会做 diff。
如果想阻止子组件不必要的重新渲染,可以用 React.memo 包裹子组件。React.memo 会对 props 做浅比较,如果 props 没变,就跳过子组件渲染。
但如果传给子组件的是对象、数组、函数,就要注意引用问题。因为父组件每次重新执行都会重新创建这些引用类型,导致 React.memo 判断 props 变化。
所以对象、数组可以用 useMemo 缓存,函数可以用 useCallback 缓存。
结合题目中的代码:
const Child = React.memo(({ obj }) => {
console.log('child render')
return <div>{obj.a}</div>
})
const obj = useMemo(() => {
return { a: 1 }
}, [])Child 被 React.memo 包裹,obj 又被 useMemo 缓存,所以点击 add 只会让 Parent 重新渲染,Child 不会重新渲染。
总结
这道题的核心其实是:React 组件重新渲染和 props 引用变化之间的关系。
几个结论:
- 父组件重新渲染时,子组件默认也会重新渲染
React.memo可以在 props 没变时跳过子组件渲染React.memo默认只做 props 浅比较- 对象、数组、函数每次重新创建都会产生新引用
useMemo用来缓存值,常用于稳定对象和数组引用useCallback用来缓存函数引用React.memo缓存的是组件,useMemo缓存的是值
所以不要把 React.memo 和 useMemo 理解成同一个东西。
一个是在组件外面控制“组件要不要重新渲染”,一个是在组件里面控制“值要不要重新计算 / 重新创建”。