React中的计算属性useMemo,有哪些让人眼前一亮的特性?useMemo 是一个 React Hook,它在每次重
往期文章:
useMemo简介
useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。
语法:
const cachedValue = useMemo(calculateValue, dependencies)
参数 :
- calculateValue
要缓存计算值的函数:没有任何参数的纯函数,并且可以返回任意类型,React 将会在首次渲染时调用该函数;在之后的渲染中,如果 dependencies 没有发生变化,React 将直接返回相同值。
- dependencies
所有在 calculateValue 函数中使用的响应式变量组成的数组。响应式变量包括 props、state 和所有你直接在组件中定义的变量和函数。
返回值 :
在初次渲染时,useMemo 返回不带参数调用 calculateValue 的结果。
在接下来的渲染中,如果依赖项没有发生改变,它将返回上次缓存的值;否则将再次调用 calculateValue,并返回最新结果。
类比vue中的计算属性
学过vue的同学都知道,vue中的computed计算属性可以根据ref对象或者其他computed计算属性自动更新值,像这样:
// 定义 ref 对象
const count = ref(1);
const factor = ref(2);
// 计算属性:根据 count 自动计算出 doubledCount
const doubledCount = computed(() => {
return count.value * 2;
});
// 计算属性:根据 doubledCount 和 factor 计算最终结果
const finalResult = computed(() => {
return doubledCount.value * factor.value;
});
在 React 中,useMemo
也可以看成是一种实现计算属性的方式:
import React, { useMemo } from 'react';
function MyComponent({ value }) {
const computedValue = useMemo(() => {
return value * 2;
}, [value]);
return <div>{computedValue}</div>;
}
上面代码中的 computedValue
就类似于 Vue 中的计算属性,会根据 value
的变化动态更新。
但我们得注意,它的使用场景和机制与 Vue 中的计算属性存在一些差异。
响应式 vs. 手动依赖:
- 在 Vue 中,计算属性依赖于 Vue 的响应式系统,依赖项会自动追踪。
- 在 React 中,
useMemo
需要手动指定依赖项数组(第二个参数)。
函数式 vs. 声明式:
- Vue 的计算属性是声明式的,通常在组件定义中通过
computed
对象声明,使用起来很直观。 useMemo
是基于函数式编程思想的,需要在函数组件的逻辑中显式调用和定义。
应用场景:
- Vue 的计算属性通常用于根据多个数据源派生出一个新的值。
- React 的
useMemo
更多是用于优化性能,避免在每次渲染时进行不必要的复杂计算。
性能优化
通过上面的介绍,我们可以知道, useMemo
更多是用在性能优化方面。比如
- 避免不必要的重复计算
- 避免不必要的子组件重新渲染
- 避免函数的重新创建
避免不必要的重复计算
假如我们有一个非常耗费性能的计算方法 expensiveResult,父组件每次更新都会触发这个方法,这个非常浪费性能的!
function ExpensiveCalculationComponent({ value }) {
// 使用 useMemo 缓存计算结果,只有当 value 变化时才重新计算
const expensiveResult = () => {
const time = new Date().getTime();
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += value * i;
}
const endTime = new Date().getTime();
console.log(`计算耗时: ${(endTime - time)/ 1000}s`);
return result;
};
return (
<div>
<p>父组件传递的值value值: {value}</p>
<p>useMemo计算的结果: {expensiveResult()}</p>
</div>
);
}
可以看到,每次父组件更新(即使value没更新),子组件都会被这个方法阻塞约1s。
这显然是不合理的,我们只希望value改变时,重新执行这个方法!此时,useMemo就派上用场了!
function ExpensiveCalculationComponent({ value }) {
// 使用 useMemo 缓存计算结果,只有当 value 变化时才重新计算
const expensiveResult = useMemo(
() => {
const time = new Date().getTime();
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += value * i;
}
const endTime = new Date().getTime();
console.log(`计算耗时: ${(endTime - time)/ 1000}s`);
return result;
},[value]
)
return (
<div>
<p>父组件传递的值value值: {value}</p>
<p>useMemo计算的结果: {expensiveResult}</p>
</div>
);
}
可以看到,使用useMemo后,父组件更新不会导致expensiveResult重新计算,除非useMemo的依赖项value发生改变!
示例完成代码
import React, { useMemo, useState } from 'react';
function ExpensiveCalculationComponent({ value }) {
// 使用 useMemo 缓存计算结果,只有当 value 变化时才重新计算
const expensiveResult = useMemo(
() => {
const time = new Date().getTime();
let result = 0;
for (let i = 0; i < 1000000000; i++) {
result += value * i;
}
const endTime = new Date().getTime();
console.log(`计算耗时: ${(endTime - time)/ 1000}s`);
return result;
},[value]
)
return (
<div>
<p>父组件传递的值value值: {value}</p>
<p>useMemo计算的结果: {expensiveResult}</p>
</div>
);
}
function App() {
const [inputValue, setInputValue] = useState(1);
const [otherValue, setOtherValue] = useState(10);
return (
<div>
<h3>useMemo性能演示</h3>
<div style={{marginBottom:'15px'}}>父组件--------------------------------------------------------</div>
{/* 改变这个值不会触发 ExpensiveCalculationComponent 的重新计算 */}
<button onClick={() => setOtherValue(otherValue + 1)}>
点击重新加载父组件
</button>
{/* 改变 inputValue 会触发 ExpensiveCalculationComponent 的重新计算 */}
<button onClick={() => setInputValue(inputValue + 1)}>
点击更新value的值 ({inputValue})
</button>
<div style={{marginTop:'15px'}}>子组件--------------------------------------------------------</div>
<ExpensiveCalculationComponent value={inputValue} />
</div>
);
}
export default App;
避免不必要的子组件重新渲染
在某些情况下,我们希望某个子组件仅在其属性变化时才重新渲染。如果该属性是通过复杂计算得来的,可以使用 useMemo
缓存该计算结果,从而避免子组件不必要的重新渲染。
import React, { useMemo, useState } from 'react';
function ChildComponent({ data }) {
console.log('子组件渲染');
return <div>{data.join(', ')}</div>;
}
function ParentComponent() {
const [count, setCount] = useState(0);
const data = useMemo(() => {
console.log('计算中...');
return [count, count + 1, count + 2];
}, [count]);
return (
<div>
<ChildComponent data={data} />
<button onClick={() => setCount(count + 1)}>Increment count ({count})</button>
</div>
);
}
export default ParentComponent;
解释:
- 这里的
data
是通过useMemo
计算得来的数组,只有当count
变化时才会重新计算。 - 如果没有
useMemo
,每次ParentComponent
重新渲染时,即使count
没有变化,ChildComponent
也会因为data
的重新创建而重新渲染。 - 使用
useMemo
后,ChildComponent
只在data
变化时才重新渲染。
避免函数的重新创建
在 React 中,每次组件渲染时,组件中的函数都会重新创建。
import React, { useMemo, useState, useCallback } from 'react';
function ChildComponent({ onClick }) {
console.log('子组件渲染');
return <button onClick={onClick}>点击子组件</button>;
}
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = () => {
alert('按钮被点击!哈哈');
};
return (
<div>
<ChildComponent onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>增加count ({count})</button>
</div>
);
}
export default ParentComponent;
上述代码中,每次 ParentComponent
父组件重新渲染时,handleClick
函数都会重新创建,这可能导致 ChildComponent
子组件做不必要的重新渲染,浪费性能。
如果我们把handleClick使用useMemo缓存,就会避免这个问题
import React, { useMemo, useState, useCallback } from 'react';
function ChildComponent({ onClick }) {
console.log('子组件渲染');
return <button onClick={onClick}>点击子组件</button>;
}
function ParentComponent() {
const [count, setCount] = useState(0);
const handleClick = useMemo(() => {
alert('按钮被点击!哈哈');
},[])
return (
<div>
<ChildComponent onClick={handleClick} />
<button onClick={() => setCount(count + 1)}>增加count ({count})</button>
</div>
);
}
export default ParentComponent;
使用useMemo后,handleClick
只会在第一次渲染时创建一次,并在依赖项变化时(这里为空数组,不会变化)重新创建。
实际上,缓存函数我们会使用useMemo的变体:useCallback
总结
useMemo
可以显著优化组件的性能,特别是在有复杂计算或频繁渲染的场景中。- 与 Vue 的计算属性类似,
useMemo
也是为了缓存计算结果,但它需要手动传递依赖项,并且是基于函数式编程的设计。 useCallback
是useMemo
的变体,用于缓存函数,避免不必要的函数重新创建。
转载自:https://juejin.cn/post/7402517513933488178