React Hooks中memo、useMemo、useCallBack的作用
一句话概括
memo
、useMemo
、useCallBack
主要用于避免 React 组件的重复渲染,作为 性能优化 的一种手段,你可以根据场景合理的使用它们。
React组件的更新机制
在使用memo、useCallBack、useMemo前,我们需要先了解React组件的更新机制:React组件在默认情况下,父组件或兄弟组件触发更新后,会按照父组件、子组件的顺序重新渲染,并且即使子组件本身没有发生任何变化,也会重复触发更新。
举一个简单的例子🌰, 目前我们有Parent、Child1、Child2 三个组件。
// parent.jsx
import Child1 from './child1';
import Child2 from './child2';
import { useState } from 'react';
const Parent = () => {
const [count, setCount] = useState(0);
console.log('Parent 组件更新');
return (
<div>
<div>计数: {count} <button onClick={() => setCount(count + 1)}>自增</button></div>
<Child1 />
<Child2/>
</div>
);
};
export default Parent;
// child1.jsx
const Child1 = () => {
console.log('Child1 组件更新');
return (
<div>
我是Child1
</div>
);
};
export default Child1;
// child2.jsx
const Child2 = () => {
console.log('Child2 组件更新');
return (
<div>
我是Child2
</div>
);
};
export default Child2;
我们观察控制台,发现三个组件的 function 全部被重新执行了,即使在这次更新中 Child1、Child2 组件的内容完全没有发生变化!
如果在实际项目中, Child1、Child2 组件包含高开销的计算,Parent 组件的更新会导致它们不断地重复渲染,这样会对性能产生比较大的影响。
那么有没有什么办法可以避免这种情况下的重复渲染,从而达到性能优化的目的?这个就是我们使用memo、useCallBack、useMemo的原因。
memo
如果你的组件不存在 props 或者 props 相同的情况下,使用 React.memo包裹(高阶组件的形式),可以避免组件多余无意义的更新动作。
React.memo 通过记忆组件
渲染结果的方式来提高组件的性能表现,既React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。
// parent.jsx
import Child1 from './child1';
import Child2 from './child2';
import { useState } from 'react';
const Parent = () => {
const [count, setCount] = useState(0);
console.log('Parent 组件更新');
const params = {msg:'哈哈'};
return (
<div>
<div>计数: {count} <button onClick={() => setCount(count + 1)}>自增</button></div>
<Child1 />
<Child2 params={params}/>
</div>
);
};
export default Parent;
// child1.jsx
import { memo } from "react";
const Child1 = memo(() => {
console.log('Child1 组件更新');
return (
<div>
我是Child1
</div>
);
});
export default Child1;
// child2.jsx
import { memo } from "react";
const Child2 = memo((props: any) => {
console.log('Child2 组件更新');
return (
<div>
我是Child2
</div>
);
});
export default Child2;
由于我们点击了按钮,触发了state的更新,Parent 组件的 function 被重新执行。但是因为 Child1 组件因为包裹了 memo,所以此次更新 并未牵连 子组件 Child1 一同更新 既执行 它的 function。那为什么同样包裹的 Child2 会重新执行呢?
这是因为 memo 在判断props是否变化时,是进行的浅比较,比如 空值 或 基本类型 的 props,但是若传递的属性是是引用类型的属性, 则在父组件在更新的时候 属性 params 都会被重新定义一遍,进而导致包裹 Child2 的 memo 内部认为 props 发生了变化,最终重新渲染了 Child2 组件。
useMemo正用于解决这样的问题!
useMemo
useMemo接受两个参数 创建函数
和依赖项数组
,它仅会在依赖项数组
中的元素 发生改变时,才重新计算 memoized 值(通过 创建函数 return 出去)。这种优化有助于避免在每次渲染时都进行高开销的计算。
因此上面的代码我们完全可以这样改写:
// parent.jsx
import Child from './child';
import { useMemo, useState } from 'react';
const Parent = () => {
const [count, setCount] = useState(0);
console.log('Parent 组件更新');
const params = useMemo(()=>({msg:'哈哈'}),[])
return (
<div>
<div>计数: {count} <button onClick={() => setCount(count + 1)}>自增</button></div>
<Child params={params}/>
</div>
);
};
export default Parent;
// child.jsx
import { memo } from "react";
const Child = memo((props: any) => {
console.log('Child 组件更新');
return (
<div>
我是Child
</div>
);
});
export default Child;
可以看到 Child并无重新执行和渲染,这是由于Parent 中的 params 是一个 useMemo ,其方法内部本身没有依赖任何变量,因此它的依赖数组项为空。
这样做可以保证无论 Parent 组件是否更新,params 变量始终都会是同一个。进而也就不会出发 结合了memo的 Child 组件更新了。
至此,我们成功通过使用memo、useMemo的组合达到了我们最终的目标。
useCallBack
本质是 useMemo 的语法糖!用法唯一的区别是:useMemo返回的是传入的回调函数的执行结果,useCallBack返回的是传入的回调函数。
useCallBack 的使用场景是 当传递给子组件的属性是一个函数的时候, 返回该函数的引用。当依赖项变化时,返回新函数的引用;否则返回缓存的旧函数引用:简单来说就是 useMemo 适合 缓存 非函数的属性,而 useCallBack 适合 缓存 函数的属性。
// parent.jsx
import Child from './child';
import { useCallback, useState } from 'react';
const Parent = () => {
const [count, setCount] = useState(0);
console.log('Parent 组件更新');
const sayHello = useCallback(() => {
console.log('你好');
}, []);
return (
<div>
<div>
计数: {count} <button onClick={() => setCount(count + 1)}>自增</button>
</div>
<Child params={sayHello} />
</div>
);
};
export default Parent;
// child.jsx
import { memo } from "react";
const Child = memo((props: any) => {
console.log('Child 组件更新');
return (
<div>
我是Child, <button onClick={()=>props.params()}>Child按钮</button>
</div>
);
});
export default Child;
其它
对比 vue computed
从设计初衷来看,Vue 的 computed
和 React 的 useMemo
有着相似的本意,即通过缓存计算结果来优化性能,避免不必要的重复计算。它们都旨在解决以下问题:
- 避免重复计算:当某些值依赖于其他状态或数据时,如果每次渲染都重新计算,可能会浪费性能。
- 响应式更新:当依赖项变化时,自动重新计算并更新结果。
- 简化代码逻辑:将复杂的计算逻辑封装起来,使代码更清晰、更易维护。
目标 | Vue 的 computed |
React 的 useMemo |
---|---|---|
缓存计算结果 | 通过缓存计算结果,避免重复计算。 | 通过缓存计算结果,避免重复计算。 |
响应式更新 | 当依赖的响应式数据变化时,自动重新计算。 | 当依赖项变化时,重新计算并返回新的值。 |
简化代码逻辑 | 将复杂的计算逻辑封装到 computed 中。 |
将复杂的计算逻辑封装到 useMemo 中。 |
与useEffect对比
useMemo 是同步执行,而 useEffect 是异步的。
import { useMemo, useState, useEffect } from 'react';
function TestComponent() {
const [count, setCount] = useState(0);
console.log('普通代码:在渲染期间同步执行');
useMemo(() => {
console.log('useMemo:在渲染期间同步执行');
}, [count]);
useEffect(() => {
console.log('useEffect:在渲染后异步执行,不阻塞渲染)');
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
export default TestComponent;
对渲染流程的影响
-
useMemo
:- 同步执行,会阻塞渲染流程。如果
useMemo
的回调函数中有复杂的计算,可能导致渲染延迟。 - 适用于轻量级计算或必须立即使用的值(如派生状态)。
- 如果计算结果需要参与当前渲染,必须使用
useMemo
。
- 同步执行,会阻塞渲染流程。如果
-
useEffect
:- 异步执行,不会阻塞渲染流程,适合处理副作用(如网络请求、DOM 操作)。
- 副作用操作不会影响当前渲染的结果,但可能会触发后续的重新渲染。
useMemo
的适用场景
-
派生状态计算
根据props
或state
计算出一个新的值,且需要立即在渲染中使用。const total = useMemo(() => items.reduce((sum, item) => sum + item.price, 0), [items]);
-
避免引用类型重新创建
缓存对象或数组,避免子组件因引用变化而重新渲染。const config = useMemo(() => ({ timeout: 1000 }), []); // 依赖为空,引用不变
-
性能敏感的计算
当计算成本较高时(如大数据过滤、排序)。const filteredList = useMemo(() => { return largeList.filter(item => item.isActive); }, [largeList]); // 仅当 largeList 变化时重新计算
useEffect
的适用场景
-
副作用操作
如网络请求、DOM 操作、订阅事件等。useEffect(() => { fetchData().then(data => setData(data)); }, []);
-
响应状态变化
当某些状态变化后需要执行特定操作(如数据保存)。useEffect(() => { saveToLocalStorage(user); }, [user]); // user 变化时触发保存
-
清理操作
在组件卸载或依赖变化前执行清理(如取消订阅)。useEffect(() => { const subscription = eventEmitter.subscribe(handleEvent); return () => subscription.unsubscribe(); // 清理函数 }, []);
不要滥用
使用useMemo、useCallBack时,本身会产生额外的开销,并且这两个方法必须和memo搭配使用,否则很可能会变成负优化。
因此,在实际项目中,需要结合实际场景,评估重复渲染和创建useCallBack/useCallBack的开销来判断到底用不用useCallBack、useMemo。