现在,让我们看看如何在类和函数组件中实现 Memoization,以避免这种不必要的重新渲染。
函数组件实现 Memoization
为了在函数组件中实现 Memoization,我们将使用 React.memo()。React.memo()
是一个高阶组件(HOC),它执行与 PureComponent
类似的工作,来避免不必要的重新渲染。
以下是函数组件的代码:
//Child.js
export function Child(props) {
console.log("Child render");
return (
<div>
<h2>{props.name}</h2>
</div>
);
}
export default React.memo(Child); // 这里我们给子组件添加 HOC 实现 Memoization
//Child.js
export function Child(props) {
console.log("Child render");
return (
<div>
<h2>{props.name}</h2>
</div>
);
}
export default React.memo(Child); // 这里我们给子组件添加 HOC 实现 Memoization
类组件实现 Memoization
为了在类组件中实现 Memoization,我们将使用 React.PureComponent。React.PureComponent
实现了 shouldComponentUpdate(),它对 state
和 props
进行了浅比较,并且仅在 props 或 state 发生更改时才重新渲染 React 组件。
将子组件更改为如下所示的代码:
//Child.js
class Child extends React.PureComponent { // 这里我们把 React.Component 改成了 React.PureComponent
render() {
console.log("Child render");
return (
<div>
<h2>{this.props.name}</h2>
</div>
);
}
}
export default Child;
复制代码
//Child.js
class Child extends React.PureComponent { // 这里我们把 React.Component 改成了 React.PureComponent
render() {
console.log("Child render");
return (
<div>
<h2>{this.props.name}</h2>
</div>
);
}
}
export default Child;
复制代码
此示例的完整代码显示在这个 sandbox 中。
父组件保持不变。现在,当我们在父组件中增加 count
时,控制台中的输出如下所示:
Parent render
Child render
Parent render
Parent render
复制代码
Parent render
Child render
Parent render
Parent render
复制代码
对于首次渲染,它同时调用父组件和子组件的 render
方法。
对于每次增加 count
后的重新渲染,仅调用父组件的 render
函数。子组件不会重新渲染。
React.memo() 存在的问题(使用useMemo也可以处理)
useCallback是uesMemo的一个语法糖(更好的处理函数_直接写)
在上面的示例中,我们看到,当我们对子组件使用 React.memo()
HOC 时,子组件没有重新渲染,即使父组件重新渲染了。
但是,需要注意的一个小问题是,如果我们将函数作为参数传递给子组件,即使在使用 React.memo()
之后,子组件也会重新渲染。让我们看一个这样的例子。
我们将更改父组件,如下所示。在这里,我们添加了一个处理函数,并作为参数传递给子组件:
//Parent.js
export default function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
const handler = () => {
console.log("handler"); // 这里的 handler 函数将会被传递给子组件
};
console.log("Parent render");
return (
<div className="App">
<button onClick={handleClick}>Increment</button>
<h2>{count}</h2>
<Child name={"joe"} childFunc={handler} />
</div>
);
}
复制代码
//Parent.js
export default function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
const handler = () => {
console.log("handler"); // 这里的 handler 函数将会被传递给子组件
};
console.log("Parent render");
return (
<div className="App">
<button onClick={handleClick}>Increment</button>
<h2>{count}</h2>
<Child name={"joe"} childFunc={handler} />
</div>
);
}
复制代码
子组件代码将保持原样。我们不会在子组件中使用父组件传递来的函数:
//Child.js
export function Child(props) {
console.log("Child render");
return (
<div>
<h2>{props.name}</h2>
</div>
);
}
export default React.memo(Child);
复制代码
//Child.js
export function Child(props) {
console.log("Child render");
return (
<div>
<h2>{props.name}</h2>
</div>
);
}
export default React.memo(Child);
复制代码
现在,当我们递增父组件中的 count
时,它会重新渲染并同时重新渲染子组件,即使传递的参数中没有更改。
那么,是什么原因导致子组件重新渲染的呢?答案是,每次父组件重新渲染时,都会创建一个新的 handler
函数并将其传递给子组件。现在,由于每次重新渲染时都会重新创建 handle
函数,因此子组件在对 props 进行浅比较时会发现 handler
引用已更改,并重新渲染子组件。
通过 useCallback()
来避免更多的重复渲染
导致子组件重新渲染的主要问题是重新创建了 handler
函数,这更改了传递给子组件的引用。因此,我们需要有一种方法来避免这种重复创建。如果未重新创建 handler
函数,则对 handler
函数的引用不会更改,因此子组件不会重新渲染。
为了避免每次渲染父组件时都重新创建函数,我们将使用一个名为 useCallback() 的 React Hook。Hooks 是在 React 16 中引入的。要了解有关 Hooks 的更多信息,你可以查看 React 的官方 hooks 文档,或者查看 `React Hooks: How to Get Started & Build Your Own"。
useCallback()
钩子传入两个参数:回调函数和依赖项列表。
以下是 useCallback()
示例:
const handleClick = useCallback(() => {
}, [x,y]);
复制代码
const handleClick = useCallback(() => {
}, [x,y]);
复制代码
在这里,useCallback()
被添加到 handleClick()
函数中。第二个参数 [x, y]
可以是空数组、单个依赖项或依赖项列表。每当第二个参数中提到的任何依赖项发生更改时,才会重新创建 handleClick()
函数。
如果 useCallback()
中提到的依赖项没有更改,则返回作为第一个参数提及的回调函数的 Memoization 版本。我们将更改父组件,以便对传递给子组件的处理程序使用 useCallback()
钩子:
//Parent.js
export default function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
const handler = useCallback(() => { // 给 handler 函数使用 useCallback()
console.log("handler");
}, []);
console.log("Parent render");
return (
<div className="App">
<button onClick={handleClick}>Increment</button>
<h2>{count}</h2>
<Child name={"joe"} childFunc={handler} />
</div>
);
}
复制代码
//Parent.js
export default function Parent() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
const handler = useCallback(() => { // 给 handler 函数使用 useCallback()
console.log("handler");
}, []);
console.log("Parent render");
return (
<div className="App">
<button onClick={handleClick}>Increment</button>
<h2>{count}</h2>
<Child name={"joe"} childFunc={handler} />
</div>
);
}
复制代码
子组件代码将保持原样。
此示例的完整代码这个 sandbox 中。
当我们在上述代码的父组件中增加 count
时,我们可以看到以下输出:
Parent render
Child render
Parent render
Parent render
Parent render
复制代码
Parent render
Child render
Parent render
Parent render
Parent render
复制代码
由于我们对父组件中的 handler
使用了 useCallback()
钩子,因此每次父组件重新渲染时,都不会重新创建 handler
函数,并且会将 handler
的 Memoization 版本传递到子组件。子组件将进行浅比较,并注意到 handler
函数的引用没有更改,因此它不会调用 render
方法。
值得注意的事
Memoization 是一种很好的手段,可以避免在组件的 state 或 props 没有改变时对组件进行不必要的重新渲染,从而提高 React 应用的性能。你可能会考虑为所有组件添加 Memoization,但这并不一定是构建高性能 React 组件的方法。只有在组件出现以下情况时,才应使用 Memoization:
- 固定的输入有固定的输出时
- 具有较多 UI 元素,虚拟 DOM 检查将影响性能
- 多次传递相同的参数
总结
在本教程中,我们理解了:
- React 是如何渲染 UI 的
- 为什么需要 Memoization
- 如何在 React 中通过函数组件的
React.memo()
和类组件的React.PureComponent
实现 Memoization - 通过一个例子展示,即使在使用
React.memo()
之后,子组件也会重新渲染 - 如何使用
useCallback()
钩子来避免在函数作为 props 传递给子组件时产生重新渲染的问题