首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

狗教我React——组件渲染性能优化

  • 25-04-24 21:01
  • 3187
  • 12083
juejin.cn

关键词: shouldComponentUpdate、PureComnent、React.memo、useMemo、useCallback

对于React,不管是初学者还是老手应该都知道这些,毕竟面试题经常问到嘛,所以在这个专栏还是有必要总结一下。

image.png

shouldComponentUpdate 与 PureComnent

shouldComponentUpdate 与 PureComnent 用于类组件。虽然官方推荐使用函数组件,但我们依然需要对类组件的渲染优化策略有所了解,不仅是维护旧的类组件代码需要,很多优化的概念是通用的。

所以,我们先简单了解一下 shouldComponentUpdate 与 PureComnent。

先看一个类组件示例

jsx
代码解读
复制代码
import React from 'react'; class Child extends React.Component { render() { console.log('Child rendered'); return ( <div> <h1>Child Count: {this.props.count}h1> div> ); } } class App extends React.Component { state = { count: 0, otherValue: 'Hello', }; increment = () => { this.setState((prevState) => ({ count: prevState.count + 1 })); }; changeOtherValue = () => { this.setState({ otherValue: this.state.otherValue === 'Hello' ? 'World' : 'Hello' }); }; render() { console.log('Parent rendered'); return ( <div> <h1>otherValue: {this.state.otherValue}h1> <Child count={this.state.count} /> <button onClick={this.increment}>Increment Countbutton> <button onClick={this.changeOtherValue}>Change Other Valuebutton> div> ); } } export default App;

在上面的代码中,Child 组件的 count 属性是 App 的 state 的一部分。点击 APP 组件的 Increment Count,count 会增加,此时 App 组件重新渲染了,Child 组件也重新渲染了:

image.png

点击 APP 组件的 Change Other Value,otherValue 会改变,此时 App 组件重新渲染了,但 Child 组件虽然没有用到 otherValue,但依然重新渲染了:

image.png

这是因为当 Parent 组件(在这个案例中是 App)的 state 或 props 发生变化时,React 会默认重新渲染该组件及其所有 Child 组件。

此时就可以用到shouldComponentUpdate 来优化性能,避免不必要的渲染。

shouldComponentUpdate

文档:zh-hans.react.dev/reference/r…

shouldComponentUpdate 是一个生命周期方法,可以用来决定组件是否需要更新。返回 true 会让组件继续更新,而返回 false 则会阻止更新。

使用 shouldComponentUpdate 优化后的 Child 代码如下:

jsx
代码解读
复制代码
class Child extends React.Component { shouldComponentUpdate(nextProps) { // 仅在 count 属性变化时重新渲染 return this.props.count !== nextProps.count; } render() { console.log('Child rendered'); return ( <div> <h1>Child Count: {this.props.count}h1> div> ); } }

此时,点击 APP 组件的 Change Other Value,otherValue 会改变,但 Child 组件不会重新渲染:

image.png

PureComponent

除了手动实现 shouldComponentUpdate,我们还可以使用 React.PureComponent来自动处理这一逻辑。PureComponent 会对其 props 进行浅比较,如果 props 没有变化,则不会重新渲染。

下面是使用 PureComponent 重写 Counter 组件的示例:

jsx
代码解读
复制代码
class Child extends React.PureComponent { render() { console.log('Child rendered'); return ( <div> <h1>Child Count: {this.props.count}h1> div> ); } }

使用 PureComponent 后,Child 组件在 props.count 没有变化时将也不会重新渲染。

需要注意的是,PureComponent 并未实现 shouldComponentUpdate()。

React.PureComponent 只进行浅比较,如果 props 或 state 中包含复杂的数据结构(如对象或数组),浅比较可能无法正确判断数据是否发生变化。在这种情况下,可以使用深比较或手动实现 shouldComponentUpdate 来确保组件正确地更新。(但其实我们一般在更新数组时都是返回一个新的数组从而改变引用地址)。

React.memo

文档:zh-hans.react.dev/reference/r…

其实在官方文档中,shouldComponentUpdate 和 PureComponent 都被列为了过时的 API,官方推荐使用 React.memo 来代替。

React.memo 是一个高阶组件,类似于 PureComponent,但其使用于函数组件。它接受一个函数组件作为参数,并返回一个新的函数组件。新的函数组件会对传入的 props 进行浅比较来决定是否重新渲染组件。

把上面的组件改成函数组件,并在 Child 组件使用 React.memo:

jsx
代码解读
复制代码
import React, { useState } from 'react'; // 将 Child 组件定义为函数组件并使用 React.memo const Child = React.memo(({ count }) => { console.log('Child rendered'); return ( <div> <h1>Child Count: {count}h1> div> ); }); const App = () => { const [count, setCount] = useState(0); const [otherValue, setOtherValue] = useState('Hello'); const increment = () => { setCount((prevCount) => prevCount + 1); }; const changeOtherValue = () => { setOtherValue((prevValue) => (prevValue === 'Hello' ? 'World' : 'Hello')); }; console.log('Parent rendered'); return ( <div> <Child count={count} /> <button onClick={increment}>Increment Countbutton> <button onClick={changeOtherValue}>Change Other Valuebutton> div> ); }; export default App;

image.png

可以看到,使用 React.memo可以和 PureComponent 一样,当 props.count 没有变化时,Child 组件不会重新渲染。

前面说到 React.memo 是一个高阶组件。实际上, React.memo 的源码就是返回一个具有类似于 PureComponent 的行为的组件。

需要注意的是,React.memo 也是只对 props 进行浅比较。

那么,如果 Child 组件的 props 中包含复杂的数据结构,我们在更新时习惯性地返回一个新的对象或数组,就能避免浅比较的问题。

React.memo 语法

除此之外,React.memo 还可以接受第二个参数,用于自定义比较逻辑。第二个参数是一个函数,接受两个参数:oldProps 和 newProps,返回一个布尔值,表示是否需要重新渲染组件。

jsx
代码解读
复制代码
function MyComponent(props) { /* 使用 props 渲染 */ } export default React.memo(MyComponent, areEqual); // 自定义比较逻辑 function areEqual(oldProps, newProps) { // 在这里自定义规则 // 如果返回true,表示新旧props相等,不渲染 与shouldComponentUpdate相反 // 如果返回false,表示新旧props不等,重新渲染 }

useCallback

useCallback 是一个 React Hook,用于优化函数组件的性能。具体的作用简单来说就是缓存函数。

文档:zh-hans.react.dev/reference/r…

仅使用 React.memo 时遇到的问题

在实际开发时,在一个组件中会出现很多 Child 组件。我们还是以之前的例子为例,把 count 和 increment 放到 Child 组件中:

jsx
代码解读
复制代码
import React, { useState } from 'react'; // 将 Child 组件定义为函数组件并使用 React.memo const Child = React.memo(() => { console.log('Child rendered'); const [count, setCount] = useState(0); const increment = () => { setCount((prevCount) => prevCount + 1); }; return ( <div style={{ border: '1px solid black', width: '300px', padding: '10px' }}> <h1>Child Count: {count}h1> <button onClick={increment}>Increment Countbutton> div> ); }); const App = () => { const [otherValue, setOtherValue] = useState('Hello'); const changeOtherValue = () => { setOtherValue((prevValue) => (prevValue === 'Hello' ? 'World' : 'Hello')); }; console.log('Parent rendered'); return ( <div> <h1>otherValue: {otherValue}h1> <button onClick={changeOtherValue}>Change Other Valuebutton> <Child /> div> ); }; export default App;

分别点击 Increment Count 按钮和 Change Other Value 按钮,可以看到,各自的更新没有互相影响。

image.png

(因为在 Child 使用了 React.memo, 所以 otherValue 的改变不会导致 Child 组件重新渲染。如果不使用 React.memo,点击 Change Other Value 按钮时,Child 组件会重新渲染)

但是,如果 count 和 increment 在 Parent 组件中定义,那么每次 Parent 组件重新渲染时,都会创建新的 count 和 increment 函数,导致 Child 组件也重新渲染。

jsx
代码解读
复制代码
import React, { useState } from 'react'; // 将 Child 组件定义为函数组件并使用 React.memo const Child = React.memo(({ count, increment }) => { console.log('Child rendered'); return ( <div style={{ border: '1px solid black', width: '300px', padding: '10px' }}> <h1>Child Count: {count}h1> <button onClick={increment}>Increment Countbutton> div> ); }); // Parent 组件: App const App = () => { const [count, setCount] = useState(0); const [otherValue, setOtherValue] = useState('Hello'); const increment = () => { setCount((prevCount) => prevCount + 1); }; const changeOtherValue = () => { setOtherValue((prevValue) => (prevValue === 'Hello' ? 'World' : 'Hello')); }; console.log('Parent rendered'); return ( <div> <h1>otherValue: {otherValue}h1> <button onClick={changeOtherValue}>Change Other Valuebutton> <Child count={count} increment={increment} /> div> ); }; export default App;

点击查看输出

image.png

可以看到,otherValue 变化时,这个输出不太合理, Child 组件没有使用 otherValue 但也重新渲染了。

这是因为每次 Parent 组件重新渲染时,都会创建新的 increment 函数。对于 Child 组件来说传入的 increment 导致 props 不同,所以也会重新渲染。

此时,就可以使用 useCallback 来缓存 increment 函数,避免每次都重新创建。

useCallback 的语法:

jsx
代码解读
复制代码
const memoizedCallback = useCallback(fn, dependencies); // fn:回调函数 // dependencies:依赖数组。当依赖数组中的值发生变化时,才会重新生成回调函数

使用 useCallback 把 Parent 组件传入的 increment 函数缓存起来:

jsx
代码解读
复制代码
const increment = useCallback(() => { setCount((prevCount) => prevCount + 1); }, []); // 示例的函数比较简单,并不需要响应任何状态或属性的变化,只需要在组件首次渲染时创建就可以了,所以依赖数组为空数组。

看一下效果:

image.png

可以看到,otherValue 变化时,Child 组件没有重新渲染,达到了我们想要的效果。

在实际应用中,React.memo 和 useCallback 经常结合使用,以减少不必要的组件渲染和函数创建,从而提高性能。

useMemo

说到这里,不得不提 React 提供的另一个 Hook: useMemo。 其用于缓存计算结果,避免在每次渲染时都重新计算。

文档:zh-hans.react.dev/reference/r…

useMemo 的语法:

jsx
代码解读
复制代码
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); // computeExpensiveValue:计算函数 // [a, b]:依赖数组。当依赖数组中的值发生变化时,才会重新计算

使用场景

某些时候,组件中某些值需要根据状态进行一个二次计算(类似于 Vue 中的计算属性),由于组件一旦重新渲染,就会重新执行整个函数,这就导致之前的二次计算也会重新执行一次,从而浪费性能。

例如,我们实现一个购物车时,总价需要根据当前购物车里面的商品内容进行计算,如果每次组件重新渲染时都重新计算总价,就会浪费性能。这时,我们就可以使用 useMemo 来缓存计算结果,避免每次都重新计算。

示例

还是是上面的例子,我们现在要根据 count 的值来计算一个num。

jsx
代码解读
复制代码
import React, { useState } from 'react'; function App() { const [count, setCount] = useState(0); const [otherValue, setOtherValue] = useState('Hello'); console.log('App 渲染了'); function getNum() { console.log('getNum调用了'); return count + 100; } const increment = useCallback(() => { setCount((prevCount) => prevCount + 1); }, []); const changeOtherValue = () => { setOtherValue((prevValue) => (prevValue === 'Hello' ? 'World' : 'Hello')); }; return ( <div> <h1>getNum:{getNum()}h1> <h1>otherValue: {otherValue}h1> <div> <button onClick={increment}>Increment Countbutton> <button onClick={changeOtherValue}>Change Other Valuebutton> div> div> ); } export default App;

运行一下,点击按钮,可以看到控制台输出:

image.png

可以看到,不管是更新 count 还是 otherValue,getNum 都会重新调用。但是,当 otherValue 变化时,其实没必要重新执行 getNum。

此时就可以使用 useMemo 来缓存 getNum 的计算结果:

jsx
代码解读
复制代码
import React, { useState, useMemo } from 'react'; function App() { const [count, setCount] = useState(0); const [otherValue, setOtherValue] = useState('Hello'); console.log('App 渲染了'); const getNum = useMemo(() => { console.log('getNum调用了'); return count + 100; }, [count]); // 依赖数组为[count],只有当 count 变化时,才会重新计算 getNum const increment = useCallback(() => { setCount((prevCount) => prevCount + 1); }, []); const changeOtherValue = () => { setOtherValue((prevValue) => (prevValue === 'Hello' ? 'World' : 'Hello')); }; return ( <div> <h1>getNum:{getNum}h1> <h1>otherValue: {otherValue}h1> <div> <button onClick={increment}>Increment Countbutton> <button onClick={changeOtherValue}>Change Other Valuebutton> div> div> ); } export default App;

运行,点击按钮,可以看到控制台输出:

image.png

可以看到,当 otherValue 变化时,getNum 没有重新调用,达到了我们想要的效果。

总结

image.png

下面对 React.memo、useCallback 和 useMemo 进行一个简单的对比总结:

特性React.memouseCallbackuseMemo
主要功能缓存组件,防止不必要的渲染缓存回调函数缓存计算结果
使用场景当传入的 props 没有变化时,避免组件重新渲染传递函数到子组件时,避免重新渲染时重新创建该函数避免在每次渲染时,进行不必要的昂贵计算
依赖项根据 props 变化根据依赖数组变化根据依赖数组变化
返回值类型返回新的组件返回记忆化的函数返回记忆化的值
注:本文转载自juejin.cn的本狗超级忙的的文章"https://juejin.cn/post/7425940291691954230"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top