React中的useMemo 钩子实际上是我最喜欢的钩子之一--它在编写React组件时实现了很多最佳实践并解决了一些潜在的棘手的错误。
它是如何工作的
useMemo 钩子是一个函数,它接受两个参数:一个回调函数和一个依赖数组。
function MyComponent() {
const myValue = useMemo(
() => {
/* this is the callback function */
},
[
/* this is the dependency array */
]
);
return <>Here's the value: {myValue}!</>;
}
一个更现实的接口版本可能看起来像这样。
function MyComponent({ a, b, c }) {
const myValue = useMemo(() => {
return computeSomething(a, b, c);
}, [a, b, c]);
return <>Here's the value: {myValue}!</>;
}
但是等等,为什么不直接将computeSomething(a, b, c) 赋值给myValue 呢?难道就不能这样吗?
function MyComponent({ a, b, c }) {
const myValue = computeSomething(a, b, c);
return <>Here's the value: {myValue}!</>;
}
就像编程中的许多事情一样,答案是也许。
useMemo能解决什么问题?
当你移除useMemo 钩子时,每次组件渲染时都会运行computeSomething 。这对于简单的函数来说可能是可以的,但是如果computeSomething 是昂贵的,它可能会影响性能。此外,这对引用平等也有影响,当我们需要保证myValue 和上次渲染组件时是一样的,这就变得很重要了。
让我们先谈谈如何防止昂贵的重新计算,然后再讨论参照平等。
防止昂贵的重新计算
假设我们的computeSomething 函数是特别昂贵的。也许它在一堆元素中循环了很多次。无论如何,computeSomething 是a,b, 和c 的一个函数。假设它是一个纯函数(即,它没有副作用,并且对于相同的输入将返回相同的输出),那么绝对没有理由重新计算myValue ,除非 a 、b 、或c 发生变化。这正是useMemo 钩子所做的事情。它将从提供的函数中返回一个记忆化的值,除非依赖数组中的一个值发生变化。
一个避免重新计算的快速例子
在下面的代码片段中,我让我们的组件变得更加复杂。现在,它有一个count 状态,我们能够使用一个button 来增量。
function MyComponent({ a, b, c }) {
const [count, setCount] = useState(0);
const myValue = useMemo(() => {
return computeSomething(a, b, c);
}, [a, b, c]);
return (
<>
Here's the value: {myValue}!
<button
onClick={() => {
setCount(count + 1);
}}
>
Increment
</button>
</>
);
}
请注意,computeSomething 并不依赖于count 这个变量,所以当count 发生变化时,重新计算它是没有意义的。但是我们很好--使用useMemo ,我们不会重新计算值,除非依赖数组中的东西发生变化。
参照性平等
当涉及到重新计算的值时,缺乏指代平等可能是更经常遇到的问题。假设我们有一个叫做makeObject 的函数,它接受a,b, 和c 作为参数并返回一个对象。
function makeObject(a, b, c) {
return { a, b, c };
}
function MyComponent({ a, b, c }) {
const myObject = makeObject(a, b, c);
return <>Here's the object: {JSON.stringify(myObject)}</>;
}
重要的是要记住,在组件的每一次渲染中,makeObject 将返回一个新的对象,在内存中有一个新的引用。React的很多渲染逻辑都依赖于引用的平等性。作为复习,具有相同属性的对象由于缺乏引用平等性而不平等:它们在内存中引用两个不同的对象。例如,下面的情况是错误的。
{a : 1} === {a: 1};
// false
回到我们的useMemo 讨论-我们要把我们的count 按钮以及一个新的组件扔回这里。
function makeObject(a, b, c) {
return { a, b, c };
}
function MyComponent({ a, b, c }) {
const [count, setCount] = useState(0);
const myObject = makeObject(a, b, c);
return (
<>
<SomeComponent obj={myObject} />
<button
onClick={() => {
setCount(count + 1);
}}
>
Increment
</button>
</>
);
}
如果我们点击Increment 按钮,我们就会增加我们的计数,我们重新运行makeObject (不必要的!),然后我们重新渲染 SomeComponent ,因为myObject 现在在内存中指向的对象与之前渲染时不同!这在很多情况下可能不是什么大问题,但如果你遇到了性能问题,你可能要考虑对makeObject 的调用进行备忘。
function makeObject(a, b, c) {
return { a, b, c };
}
function MyComponent({ a, b, c }) {
const [count, setCount] = useState(0);
const myObject = useMemo(() => makeObject(a, b, c), [a, b, c]);
return (
<>
<SomeComponent obj={myObject} />
<button
onClick={() => {
setCount(count + 1);
}}
>
Increment
</button>
</>
);
}
这成为一个大问题的地方:依赖数组
在我们之前的方案中,我们看到你可能因为渲染一个组件的次数过多而遇到性能问题。虽然这是真的,但在你真正遇到性能问题之前,你往往不需要担心这个问题。
然而,会影响你的是依赖性数组中的引用平等问题。我在useEffect 依赖关系数组中经常看到这种情况。考虑一下下面的代码。
function makeObject(a, b, c) {
return { a, b, c };
}
function MyComponent({ a, b, c }) {
const [count, setCount] = useState(0);
const myObject = makeObject(a, b, c);
useEffect(() => {
doSomething(myObject);
}, [myObject]);
return (
<>
Here's the object: {JSON.stringify(myObject)}
<button
onClick={() => {
setCount(count + 1);
}}
>
Increment
</button>
</>
);
}
我们现在的情况是,增加count 将会运行useEffect 钩子,因为myObject 将不必要地作为一个新对象在内存中重新创建。这可能会导致各种不希望看到的结果!根据doSomething ,它也可能导致一个无限的效果循环,哎呀!
同样,解决这个问题的方法是使用useMemo 钩子来记忆我们函数的返回值。
function makeObject(a, b, c) {
return { a, b, c };
}
function MyComponent({ a, b, c }) {
const [count, setCount] = useState(0);
const myObject = useMemo(() => makeObject(a, b, c), [a, b, c]);
useEffect(() => {
doSomething(myObject);
}, [myObject]);
return (
<>
Here's the object: {JSON.stringify(myObject)}
<button
onClick={() => {
setCount(count + 1);
}}
>
Increment
</button>
</>
);
}
结论
我希望这篇文章能帮助你理解投资于useMemo 钩子的许多原因!