useMemo 和 useCallback 是React 16.8 提供 的Hook。在函数组件中,函数组件的每一次调用都会重新执行内部的所有逻辑,就会带来较大的性能损耗。而 useMemo 和 useCallback 可以用来解决函数组件更新过程中的性能问题。
useMemo 和 useCallback 都会在组件第一次渲染的时候执行,之后会在其依赖的变量发生改变时再次执行。并且这两个 hooks 都是返回缓存的值。useMemo 返回的是缓存的变量,而 useCallback 返回的是缓存的函数。
useMemo 的使用
我们来看一下 useMemo 的使用:
// 使用 useMemo 缓存变量值,只有在依赖变化时,才重新执行
const expensive = useMemo(() => {
console.log("compute");
let sum = 0;
for (let i = 0; i < count * 1000; i++) {
sum += i;
}
return sum;
//只有count变化,这里才重新执行
}, [count]);
在上面的代码中,我们在 useMemo 的 "创建"函数中返回了累加后的 sum ,经过 useMemo 的包装后,返回了一个被缓存的变量值,只有在依赖项发现变化时,才会重新执行 "创建"函数,重新计算缓存的变量值。
可以通过下面的例子验证效果:
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"));
useCallback 的使用
我们再来看 useCallback的使用:
// 使用 useCallback 缓存函数
const addClick = useCallback(() => {
let sum = 0;
for (let i = 0; i < count; i++) {
sum += i;
}
return sum;
}, [count]);
在上面的代码中,将一个回调函数传入 useCallback,它会返回一个缓存后的回调函数,只有在依赖项发生改变时该回调函数才会重新执行。
可以通过下面的示例验证效果:
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 返回的是一个缓存后的回调函数,而 useMemo 返回的是一个缓存后的变量。它们两者都是返回一个缓存后的值,仅仅只是返回的值类型不一样而已。那么可不可以将它们的功能互换呢?即useCallback 返回缓存后的变量,useMemo 返回缓存后的回调函数。
我们来看看useMemo 和 useCallback 的源码实现。
useMemo 源码实现
组件首次渲染时 useMemo 的源码实现:
// React 版本:16.13.1
// react-reconciler/src/ReactFiberHooks.new.js
function mountMemo<T>(
nextCreate: () => T, // “创建”函数
deps: Array<mixed> | void | null, // 依赖项
): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const nextValue = nextCreate(); // 执行 "创建"函数
hook.memoizedState = [nextValue, nextDeps]; // 将 "创建"函数 执行后的返回值缓存起来
return nextValue; // 返回缓存后的变量值
}
useMemo 的依赖项发生变化时useMemo的源码实现:
// React 版本:16.13.1
// react-reconciler/src/ReactFiberHooks.new.js
function updateMemo<T>(
nextCreate: () => T,
deps: Array<mixed> | void | null,
): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
// Assume these are defined. If they're not, areHookInputsEqual will warn.
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
}
从源码中可以看到,无论是组件首次渲染还是在依赖项发生变化后,useMemo 都是返回一个缓存后的变量值。
我们注意看这行代码:
const nextValue = nextCreate();
nextCreate 是我们在调用 useMemo 时传递给它的 "创建"函数,React 将 nextCreate 执行后的返回值经过缓存处理后,再将缓存后的值返回。既然 useMemo 返回的是 nextCreate 执行后的返回值,那么我们是不是可以在 nextCreate 中返回一个回调函数,再由useMemo返回这个回调函数,由此来模拟useCallback的功能呢?
下面我们再来看看 useCallback 的源码实现。
useCallback 源码实现
组件首次渲染时 useCallback 的源码实现:
// React 版本:16.13.1
// react-reconciler/src/ReactFiberHooks.new.js
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
useCallback 的依赖项发生变化时useMemo的源码实现:
// React 版本:16.13.1
// react-reconciler/src/ReactFiberHooks.new.js
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
hook.memoizedState = [callback, nextDeps];
return callback;
}
我们同样可以从源码中看到,无论是组件首次渲染还是在依赖项发生变化后,useCallback 都是返回一个缓存后的回调函数。
由此可见,useCallback 只能返回一个缓存后的回调函数,它不能模拟useMemo的功能,返回一个缓存后的变量值。
useMemo 模拟 useCallback 的功能
在上文中,我们提到在 useMemo 的 "创建"函数 中返回一个回调函数,来模拟 useCallback的功能,下面我们来验证一下其结果:
在 useMemo 中返回一个回调函数:
const addClick = useMemo(() =>
return () => {
let sum = 0;
for (let i = 0; i < count; i++) {
sum += i;
}
return sum;
}
}, [count]);
完整示例:
import ReactDOM from "react-dom";
import React, { useState, useMemo, PureComponent } from "react";
export default function UseCallbackPage(props) {
const [count, setCount] = useState(0);
// 使用 useMemo 缓存函数
const addClick = useMemo(() => () => {
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"));
通过上面的示例验证,通过 useMemo 返回一个回调函数来模拟useCallback的功能的效果和 useCallback 是完全一致的。
小结
useMemo 和 useCallback 都是返回一个缓存后的值,useMemo 返回的是一个缓存后的变量,而useCallback 返回的是一个缓存后的回调函数。可以在 useMemo 的 "创建函数" 中返回一个回调函数来模拟 useCallback 的功能。useMemo 和 useCallback 可以用来解决函数组件更新过程中的性能问题。