你可能在项目中使用过useCallback,也可能在纠结要不要使用useMemo;本文试验几种这两个api的场景,让我们明白该如何使用。先从useMemo开始。
useMemo
useMemo接收两个参数,第一个参数是个函数,第二个参数是依赖项,返回值是函数的结果;只有在依赖项变化时函数才会重新执行。下面这个来自官网的例子说明,useMemo通常用于缓存花销大的计算。
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
例子1
下面这个例子expensive用useMemo缓存,expensive2没有;当我们点击按钮,count改变,这时候expensive和expensive2都重新执行。这结果正常,普通函数在每次渲染时都重新执行;useMemo函数在依赖项变化时重新执行。
但是当我们在input中输入时,这时候expensive2重新执行,而expensive就不会,实现了缓存的结果。
import React, { useState, useMemo } from "react";
export default function MemoExpensive() {
const [count, setCount] = useState(1);
const [val, setValue] = useState("");
const expensive = useMemo(() => {
console.log("memo compute");
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
}, [count]);
const expensive2 = () => {
console.log("compute");
let sum = 0;
for (let i = 0; i < count * 100; i++) {
sum += i;
}
return sum;
};
return (
<div>
<h4>
{count}-{expensive}
</h4>
<h4>
{count}-{expensive2()}
</h4>
{val}
<div>
<button onClick={() => setCount(count + 1)}>click me</button>
<input value={val} onChange={e => setValue(e.target.value)} />
</div>
</div>
);
}
既然useMemo能缓存昂贵计算,你肯定会想,那能不能所有数据都缓存,这样性能最好。这篇文章 的作者经过试验,得出结论:缓存也有代价,不是所有场景都适合缓存。适合使用useMemo除了Computationally expensive calculations,还有Referential equality,即在父组件传递给子组件时,参数是引用数据类型,例子说明:
import React, { useCallback, useMemo, useEffect, useState } from "react";
function StringFoo({ bar, baz }) {
useEffect(() => {
const options = { bar, baz };
console.log("string", options);
}, [bar, baz]);
return <div>StringFoo</div>;
}
function ObjectFoo({ bar, baz }) {
useEffect(() => {
const options = { bar, baz };
console.log("obj", options);
}, [bar, baz]);
return <div>ObjectFoo</div>;
}
function MemoFoo({ bar, baz }) {
useEffect(() => {
const options = { bar, baz };
console.log("memo", options);
}, [bar, baz]);
return <div>memoFoo</div>;
}
function Blub() {
const [count, setCount] = useState(0);
// object
const objectBar = () => {};
const objectBaz = [1, 2, 3];
// memo
const memoBar = useCallback(() => {}, []);
const memoBaz = useMemo(() => [1, 2, 3], []);
return (
<>
<button
onClick={() => {
setCount(c => c + 1);
}}
>
{count}
</button>
<StringFoo bar="bar" baz="baz" />
<ObjectFoo bar={objectBar} baz={objectBaz} />
<MemoFoo bar={memoBar} baz={memoBaz} />
</>
);
}
export default Blub;
有三种情况,参数是string;参数是数组和函数;参数是useMemo包裹的数组和函数。当我们点击button时:
- stringFoo不会重新渲染,
- ObjectFoo会重新渲染,bar和baz是引用类型,虽然值没变,但是与原先不一样,故会重新渲染
- MemoFoo不会重新渲染,这就是useMemo起作用的地方
看到在打印台只有第二种情况"obj"变打印出来,你可以点击这里,自己试验
useCallback
useCallback与useMemo作用类似,不同的是useMemo返回的是该函数的结果,而useCallback返回的是该函数。除了函数从父组件传递到子组件可以用useCallback之外,通常在useEffect中也会使用useCallback。好处是这函数可以在其他地方复用;函数只关心和函数本身相关的参数(更加内聚),语义清晰。以下为伪代码。
// 没用useCallback
function Blub() {
useEffect(() => {
AuthAPI.update(params).then(data => {
// ...
})
}, [pathname, params])
}
// 使用了useCallback
function Blub() {
const authUpdateCb = (() => {
AuthAPI.update(params).then(data => {
...
})
}, [params])
useEffect(() => {
authUpdateCb()
}, [pathname, authUpdateCb])
}
总结
useMemo用于昂贵计算和引用类型参数传递的,useCallback与useMemo类似,更多用于将函数包裹,使得该函数只受到它自己依赖项的影响。
寒风卷起,落叶抱冬日。感谢阅读,不对之处请指出。