useMemo 和 useCallback 是React 16.8 提供 的Hook。在函数组件中,函数组件的每一次调用都会执行器内部的所有逻辑,就会带来较大的性能损耗。而 useMemo 和 useCallback 可以用来解决函数组件更新过程中的性能问题。
useMemo 和 useCallback 都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变是再次执行。并且这两个 hooks 都是返回缓存的值。useMemo 返回的是缓存的变量,而 useCallback 返回的是缓存的函数。
useMemo 和 useCallback 函数签名
我们简单的看一下 useMemo 和 useCallback 的函数签名:
useMemo:
export function useMemo<T>(
create: () => T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useMemo(create, deps);
}
useCallback:
export function useCallback<T>(
callback: T,
deps: Array<mixed> | void | null,
): T {
const dispatcher = resolveDispatcher();
return dispatcher.useCallback(callback, deps);
}
接下来,我们来看看两者的使用场景:
useMemo
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
useMemo 传入一个函数和依赖数组,返回一个缓存的变量值
把“创建”函数和依赖项数组作为参数传入 useMemo,它仅会在某个依赖项改变时才重新计算 memoized 值。这种优化有助于避免在每次渲染时都进行高开销的计算。
我们先来看一下在组件中不使用 useMemo 会有怎样的问题:
import React, { useState } from "react";
import ReactDOM from "react-dom";
import * as React from "react";
import { useState, useMemo } from "react";
export default function UseMemoPage(props) {
const [count, setCount] = useState(0);
const [value, setValue] = useState("");
const expensive = () => {
console.log("compute");
let sum = 0;
for (let i = 0; i < count * 1000; i++) {
sum += i;
}
return sum;
};
return (
<div>
<h3>UseMemoPage</h3>
<p>expensive:{expensive()}</p>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>add</button>
<input value={value} onChange={event => setValue(event.target.value)} />
</div>
);
}
ReactDOM.render(<UseMemoPage />, document.getElementById("container"));
这里创建了两个 state,count 和 value,count 在button 点击时用于计数,value 则用于保存 input 的值。然后
通过 expensive 函数执行一次昂贵的计算,拿到 count 对应的某个值。我们在控制台中可以看到,无论是修改 count 还是 value,由于组建的重新渲染,都会触发 expensive 的执行,但是这里的昂贵计算只依赖于 count 的值,在 value 发生改变的时候,是没有必要执行 expensive 计算的,这样就会导致性能损耗的问题。在这种情况下,我们就可以使用 useMemo,只有在 count 的值 修改时,才执行 expensive 计算:
import React, { useState, useMemo } from "react";
import ReactDOM from "react-dom";
export default function UseMemoPage(props) {
const [count, setCount] = useState(0);
// 使用 useMemo 缓存变量值,只有在依赖变化时,才重新执行
const expensive = useMemo(() => {
console.log("compute");
let sum = 0;
for (let i = 0; i < count * 1000; i++) {
sum += i;
}
return sum;
//只有count变化,这里才重新执行
}, [count]);
const [value, setValue] = useState("");
return (
<div>
<h3>UseMemoPage</h3>
<p>expensive:{expensive}</p>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>add</button>
<input value={value} onChange={event => setValue(event.target.value)} />
</div>
);
}
ReactDOM.render(<UseMemoPage />, document.getElementById("container"));
在上面的代码中,使用了 useMemo,把原先的 expensive 函数和依赖项 count 传入 useMemo,仅在 依赖项 count 变化的时候才会触发 expensive 执行,在 value 变化的时候,返回上一次缓存的值,不会再触发 expensive 执行。
useCallback
const memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b],
);
useCallback 传入一个函数和依赖数组,返回一个缓存后的函数。
把内联回调函数及依赖项数组作为参数传入 useCallback ,它将返回该回调函数的 memoized 版本, 该回调函数仅在某个依赖项改变时才会更新。当你把回调函数传递给经过优化的并使用引用相等性去避 免非必要渲染(例如 shouldComponentUpdate )的子组件时,它将非常有用。
useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
同样,我们先来看一下在组件中不使用 useCallback 会有怎样的问题:
import * as React from "react";
import ReactDOM from "react-dom";
import { useState, PureComponent } from "react";
export default function UseCallbackPage(props) {
const [count, setCount] = useState(0);
const addClick = () => {
let sum = 0;
for (let i = 0; i < count; i++) {
sum += i;
}
return sum;
};
const [value, setValue] = useState("");
return (
<div>
<h3>UseCallbackPage</h3>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>add</button>
<input value={value} onChange={event => setValue(event.target.value)} />
// count 或 value 变化时都会导致 Child 组件重新渲染
<Child addClick={addClick} />
</div>
);
}
class Child extends PureComponent {
render() {
console.log("child render");
const { addClick } = this.props;
return (
<div>
<h3>Child</h3>
<button onClick={() => console.log(addClick())}>add</button>
</div>
);
}
}
ReactDOM.render(<UseCallbackPage />, document.getElementById("container"));
我们定义了一个 UseCallbackPage 组件和 Child 子组件。在 UseCallbackPage 组件中定义了两个 state,count 和 value,还有 Child 子组件的 addClick 方法。在 UseCallbackPage 组件中,将 addClick 作为 props 传递给 Child 子组件。当 value 的值变化时,就会触发 UseCallbackPage 组件更新,同时也会触发 Child 组件更新,也就是 父组件更新了,子组件也会执行更新。但是在大多数场景下,子组件的更新是没有必要的,此时,我们可以借助 useCallback 来返回一个缓存后的函数,然后把这个函数作为 props 传递给 子组件,就可以避免子组件不必要的更新。
import * as React from "react";
import ReactDOM from "react-dom";
import React, { useState, useCallback, PureComponent } from "react";
export default function UseCallbackPage(props) {
const [count, setCount] = useState(0);
// 使用 useCallback 缓存函数
const addClick = useCallback(() => {
let sum = 0;
for (let i = 0; i < count; i++) {
sum += i;
}
return sum;
}, [count]);
const [value, setValue] = useState("");
return (
<div>
<h3>UseCallbackPage</h3>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>add</button>
<input value={value} onChange={event => setValue(event.target.value)} />
{/*将 使用 useCallback 缓存后的函数传递给子组件,可避免子组件不必要的更新*/}
<Child addClick={addClick} />
</div>
);
}
class Child extends PureComponent {
render() {
console.log("child render");
const { addClick } = this.props;
return (
<div>
<h3>Child</h3>
<button onClick={() => console.log(addClick())}>add</button>
</div>
);
}
}
ReactDOM.render(<UseCallbackPage />, document.getElementById("container"));
在上面的代码中,我们使用 useCallback 将 addClick 函数做了缓存处理,然后把 addClick 作为 props 传递给 Child 组件,当 UseCallbackPage 组件更新时,就避免了 Child 组件的更新。
不仅是上面的这个例子,所有依赖本地状态或 props 来创建函数,需要使用到缓存函数的地方,都是 useCallback 的应用场景