面试官:React怎么做性能优化

news2024/11/16 19:42:31

前言

最近一直在学习关于React方面的知识,并有幸正好得到一个机会将其用在了实际的项目中。所以我打算以博客的形式,将我在学习和开发(React)过程中遇到的问题记录下来。

这两天遇到了关于组件不必要的重复渲染问题,看了很多遍官方文档以及网上各位大大们的介绍,下面我会通过一些demo结合自己的理解进行汇总,并以此作为学习React的第一篇笔记(自己学习,什么都好,就是费头发…)。

本文主要介绍以下三种优化方式(三种方式有着相似的实现原理):

  • shouldComponentUpdate
  • React.PureComponent
  • React.memo

其中shouldComponentUpdateReact.PureComponent是类组件中的优化方式,而React.memo是函数组件中的优化方式。

引出问题

  1. 新建Parent类组件。
import React, { Component } from 'react'
import Child from './Child'

class Parent extends Component {
  constructor(props) {
    super(props)
    this.state = {
      parentInfo: 'parent',
      sonInfo: 'son'
    }
    this.changeParentInfo = this.changeParentInfo.bind(this)
  }

  changeParentInfo() {
    this.setState({
      parentInfo: `改变了父组件state:${Date.now()}`
    })
  }

  render() {
    console.log('Parent Component render')
    return (
      <div>
        <p>{this.state.parentInfo}</p>
        <button onClick={this.changeParentInfo}>改变父组件state</button>
        <br/>
        <Child son={this.state.sonInfo}></Child>
      </div>
    )
  }
}

export default Parent
  1. 新建Child类组件。
import React, {Component} from 'react'

class Child extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  render() {
    console.log('Child Component render')
    return (
      <div>
        这里是child子组件:        <p>{this.props.son}</p>
      </div>
    )
  }
}

export default Child
  1. 打开控制台,我们可以看到控制台中先后输出了Parent Component renderChild Component render 点击按钮,我们会发现又输出了一遍Parent Component renderChild Component render 点击按钮时我们只改变了父组件Parentstate中的parentInfo的值,Parent更新的同时子组件Child也进行了重新渲染,这肯定是我们不愿意看到的。所以下面我们就围绕这个问题介绍本文的主要内容。

shouldComponentUpdate

React提供了生命周期函数shouldComponentUpdate(),根据它的返回值(true | false),判断 React 组件的输出是否受当前 state 或 props 更改的影响。默认行为是 state 每次发生变化组件都会重新渲染(这也就说明了上面👆Child组件重新渲染的原因)。

引用一段来自官网的描述:参考 前端进阶面试题详细解答

当 props 或 state 发生变化时,shouldComponentUpdate() 会在渲染执行之前被调用。返回值默认为 true。目前,如果shouldComponentUpdate返回 false,则不会调用UNSAFE_componentWillUpdate()render()componentDidUpdate()方法。后续版本,React 可能会将shouldComponentUpdate()视为提示而不是严格的指令,并且,当返回 false 时,仍可能导致组件重新渲染。

shouldComponentUpdate方法接收两个参数nextPropsnextState,可以将this.propsnextProps以及this.statenextState进行比较,并返回 false 以告知 React 可以跳过更新。

shouldComponentUpdate (nextProps, nextState) {
  return true
}

此时我们已经知道了shouldComponentUpdate函数的作用,下面我们在Child组件中添加以下代码:

shouldComponentUpdate(nextProps, nextState) {
    return this.props.son !== nextProps.son
}

这个时候再点击按钮修改父组件 state 中的parentInfo的值时,Child组件就不会再重新渲染了。

这里有个注意点就是,我们从父组件Parent向子组件Child传递的是基本类型的数据,若传递的是引用类型的数据,我们就需要在shouldComponentUpdate函数中进行深层比较。但这种方式是非常影响效率,且会损害性能的。所以我们在传递的数据是基本类型是可以考虑使用这种方式(即:this.props.son !== nextProps.son)进行性能优化。

(关于基本类型数据和引用类型数据的介绍,可以参考一下这篇文章:传送门)

React.PureComponent

React.PureComponentReact.Component很相似。两者的区别在于React.Component并未实现 shouldComponentUpdate,而React.PureComponent中以浅层对比 prop 和 state 的方式来实现了该函数。

Child组件的内容修改为以下内容即可,这是不是很方便呢。

import React, { PureComponent } from 'react'

class Child extends PureComponent {
  constructor(props) {
    super(props)
    this.state = {
    }
  }

  render() {
    console.log('Child Component render')
    return (
      <div>
        这里是child子组件:        <p>{this.props.son}</p>
      </div>
    )
  }
}

export default Child

所以,当组件的 props 和 state 均为基本类型时,使用React.PureComponent可以起到优化性能的作用。

如果对象中包含复杂的数据结构,则有可能因为无法检查深层的差别,产生错误的比对结果。

为了更好的感受引用类型数据传递的问题,我们先改写一下上面的例子:

  • 修改Child组件。
import React, {Component} from 'react'

class Child extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  shouldComponentUpdate(nextProps, nextState) {
    return this.props.parentInfo !== nextProps.parentInfo
  }

  updateChild () {
    this.forceUpdate()
  }

  render() {
    console.log('Child Component render')
    return (
      <div>
        这里是child子组件:        <p>{this.props.parentInfo[0].name}</p>
      </div>
    )
  }
}

export default Child
  • 修改Parent组件。
import React, { Component } from 'react'
import Child from './Child'

class Parent extends Component {
  constructor(props) {
    super(props)
    this.state = {
      parentInfo: [
        { name: '哈哈哈' }
      ]
    }
    this.changeParentInfo = this.changeParentInfo.bind(this)
  }

  changeParentInfo() {
    let temp = this.state.parentInfo
    temp[0].name = '呵呵呵:' + new Date().getTime()
    this.setState({
      parentInfo: temp
    })
  }

  render() {
    console.log('Parent Component render')
    return (
      <div>
        <p>{this.state.parentInfo[0].name}</p>
        <button onClick={this.changeParentInfo}>改变父组件state</button>
        <br/>
        <Child parentInfo={this.state.parentInfo}></Child>
      </div>
    )
  }
}

export default Parent

此时在控制台可以看到,ParentChild都进行了一次渲染,显示的内容是一致的。

点击按钮,那么问题来了,如图所示,父组件Parent进行了重新渲染,从页面上我们可以看到,Parent组件中的parentInfo确实已经发生了改变,而子组件却没有发生变化。

所以当我们在传递引用类型数据的时候,shouldComponentUpdate()React.PureComponent存在一定的局限性。

针对这个问题,官方给出的两个解决方案:

  • 在深层数据结构发生变化时调用forceUpdate()来确保组件被正确地更新(不推荐使用);
  • 使用immutable对象加速嵌套数据的比较(不同于深拷贝);

forceUpdate

当我们明确知道父组件Parent修改了引用类型的数据(子组件的渲染依赖于这个数据),此时调用forceUpdate()方法强制更新子组件,注意,forceUpdate()会跳过子组件的shouldComponentUpdate()

修改Parent组件(将子组件通过ref暴露给父组件,在点击按钮后调用子组件的方法,强制更新子组件,此时我们可以看到在父组件更新后,子组件也进行了重新渲染)。

{
  ...
  changeParentInfo() {
    let temp = this.state.parentInfo
    temp[0].name = '呵呵呵:' + new Date().getTime()
    this.setState({
      parentInfo: temp
    })
    this.childRef.updateChild()
  }

  render() {
    console.log('Parent Component render')
    return (
      <div>
        <p>{this.state.parentInfo[0].name}</p>
        <button onClick={this.changeParentInfo}>改变父组件state</button>
        <br/>
        <Child ref={(child)=>{this.childRef = child}} parentInfo={this.state.parentInfo}></Child>
      </div>
    )
  }
}

immutable

Immutable.js是 Facebook 在 2014 年出的持久性数据结构的库,持久性指的是数据一旦创建,就不能再被更改,任何修改或添加删除操作都会返回一个新的 Immutable 对象。可以让我们更容易的去处理缓存、回退、数据变化检测等问题,简化开发。并且提供了大量的类似原生 JS 的方法,还有 Lazy Operation 的特性,完全的函数式编程。

Immutable 则提供了简洁高效的判断数据是否变化的方法,只需 === 和 is 比较就能知道是否需要执行 render(),而这个操作几乎 0 成本,所以可以极大提高性能。首先将Parent组件中调用子组件强制更新的代码this.childRef.updateChild()进行注释,再修改Child组件的shouldComponentUpdate()方法:

import { is } from 'immutable'

shouldComponentUpdate (nextProps = {}, nextState = {}) => {
  return !(this.props === nextProps || is(this.props, nextProps)) ||
      !(this.state === nextState || is(this.state, nextState))
}

此时我们再查看控制台和页面的结果可以发现,子组件进行了重新渲染。

关于shouldComponentUpdate()函数的优化,上面👆的方法还有待验证,仅作为demo使用,实际的开发过程中可能需要进一步的探究选用什么样的插件,什么样的判断方式才是最全面、最合适的。如果大家有好的建议和相关的文章欢迎砸过来~

React.memo

关于React.memo的介绍,官网描述的已经很清晰了,这里我就直接照搬了~

React.memo 为高阶组件。它与 React.PureComponent 非常相似,但只适用于函数组件,而不适用 class 组件。

如果你的函数组件在给定相同 props 的情况下渲染相同的结果,那么你可以通过将其包装在 React.memo 中调用,以此通过记忆组件渲染结果的方式来提高组件的性能表现。这意味着在这种情况下,React 将跳过渲染组件的操作并直接复用最近一次渲染的结果。

React.memo 仅检查 props 变更。如果函数组件被 React.memo 包裹,且其实现中拥有 useState 或 useContext 的 Hook,当 context 发生变化时,它仍会重新渲染。

默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。

function MyComponent(props) {
  /* 使用 props 渲染 */
}
function areEqual(prevProps, nextProps) {
  /*  如果把 nextProps 传入 render 方法的返回结果与  将 prevProps 传入 render 方法的返回结果一致则返回 true,  否则返回 false  */
}
export default React.memo(MyComponent, areEqual)

使用函数组件改写一下上面的例子:

Child组件:

import React, {useEffect} from 'react'
// import { is } from 'immutable'

function Child(props) {

  useEffect(() => {
    console.log('Child Component')
  })

  return (
    <div>
      这里是child子组件:      <p>{props.parentInfo[0].name}</p>
    </div>
  )
}

export default Child

Parent组件:

import React, {useEffect, useState} from 'react'
import Child from './Child'

function Parent() {

  useEffect(() => {
    console.log('Parent Component')
  })

  const [parentInfo, setParentInfo] = useState([{name: '哈哈哈'}])
  const [count, setCount] = useState(0)

  const changeCount = () => {
    let temp_count = count + 1
    setCount(temp_count)
  }
  return (
    <div>
      <p>{count}</p>
      <button onClick={changeCount}>改变父组件state</button>
      <br/>
      <Child parentInfo={parentInfo}></Child>
    </div>
  )
}

export default Parent

运行程序后,和上面的例子进行一样的操作,我们会发现随着父组件count的值的修改,子组件也在进行重复渲染,由于是函数组件,所以我们只能通过React.memo高阶组件来跳过不必要的渲染。

修改Child组件的导出方式:export default React.memo(Child)

再运行程序,我们可以看到父组件虽然修改了count的值,但子组件跳过了渲染。

这里我用的是React hooks的写法,在hooks中useState修改引用类型数据的时候,每一次修改都是生成一个新的对象,也就避免了引用类型数据传递的时候,子组件不更新的情况。

刚接触react,最大的感触就是它的自由度是真的高,所有的内容都可以根据自己的喜好设置,但这也增加了初学者的学习成本。(不过付出和收获是成正比的,继续我的救赎之路!)

总结

  1. 类组件中:shouldComponentUpdate()React.PureComponent 在基本类型数据传递时都可以起到优化作用,当包含引用类型数据传递的时候,shouldComponentUpdate()更合适一些。
  2. 函数组件:使用 React.memo

另外吐槽一下现在的网上的部分“博客”,一堆重复(一模一样)的文章。复制别人的文章也请自己验证一下吧,API变更、时代发展等因素引起的问题可以理解,但是连错别字,错误的使用方法都全篇照搬,然后文末贴一下别人的地址这就结束了???怕别人的地址失效,想保存下来?但这种方式不说误导别人,就说自己回顾的时候也会有问题吧,这是什么样的心态?

再说下上个月身边的真实例子。有个同事写了篇关于vue模板方面的博客,过了两天竟然在今日头条的推荐栏里面看到了一模一样的一篇文章,连文中使用的图片都是完全一样(这个侵权的博主是谁这里就不透露了,他发的文章、关注者还挺多,只能表示呵呵了)。和这位“光明磊落”的博主进行沟通,得到的却是:“什么你的我的,我看到了就是我的”这样的回复。真是天下之大,无奇不有,果断向平台提交了侵权投诉。然后该博主又舔着脸求放过,不然号要被封了,可真是好笑呢…(负能量结束明天又是美好的一天~)

这篇文章就先到这里啦,毕竟还处于自学阶段,很多理解还不是很全面,文中若有不足之处,欢迎各位看官大大们的指正

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/134157.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2022 年,我身上发生的几件大事

一晃2022互联网寒冬年就要结束了&#xff0c;在今年我的身上发生了好几件人生大事。因为这些事情对我的心态、思绪等产生了不同层次、不同方面的影响&#xff0c;所以很有必要做一次年终复盘。那么&#xff0c;接下来让我用拙略的写作手法&#xff0c;带大家走进我那特别的2022…

别等iPhone14了,苹果iPhone15变化很大

在去年的手机市场当中&#xff0c;苹果可以说是最大的赢家。因为去年iPhone13发布的时间段&#xff0c;恰好是高端旗舰的空档期&#xff0c;小米、OV在高端市场的销量表现一般&#xff0c;华为又没有能力发布新机&#xff0c;三星的Note系列在去年也暂停发布。所以不夸张的说&a…

SpringBoot整合ShardingJdbc实现数据库水平分表实战

(1)添加Maven依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.org/P…

因果推断4--Causal ML(个人笔记)

目录 1 安装教程及官方文档 1.1 pip安装 1.2 API文档 1.3 代码仓库 2 Uplift模型与主要方法介绍 2.1 发放代金券 2.2 多treatment 2.3 实验方法 3 causalml.inference.tree module 3.1 UpliftTreeClassifier 3.2 UpliftRandomForestClassifier 3.3 CausalRandomFor…

sec7-可派生和非抽象类型

创建非抽象可派生类型比创建抽象类型更常见。本节介绍如何创建非抽象可派生类型对象。派生类型的例子是string的对象。它是TStr。它的子对象是一个数字字符串对象。数字字符串是表示数字的字符串。例如“0”、“-100”、“123.45”。子对象(数字字符串)将在下一节中解释。 我想…

前端框架搭建(九)搭建页面布局框架【vite】

## 1.创建目录BasicLayout——全局布局 components——布局组件 GlobalContent&#xff1a;全局内容GlobalHeader&#xff1a;全局头部页面 2.处理GlobalHeader 创建HeaderMenu——头部菜单 声明相关类型 在typings目录下创建system.d.ts declare namespace App {/** 全局…

Canadian Coding Competition(CCC) 2021 Junior 题解

目录 Problem J1: Boiling Water Problem J2: Silent Auction Problem J3: Secret Instructions Problem J4: Arranging Books Problem J5/S2: Modern Art Problem J1: Boiling Water Problem Description At sea level, atmospheric pressure is 100 kPa and water begi…

自动挂载USB和TF卡

转自链接https://zhuanlan.zhihu.com/p/443745437 ①创建用于挂载U盘的目录 mkdir /mnt/usb –p②在/etc/udev/rules.d目录下添加用于检测U盘插入规则&#xff08;add&#xff09;&#xff0c;终端下执行以下命令创建第一个U盘插入规则。 vim /etc/udev/rules.d/11-add-usb.r…

【ROS】—— ROS通信机制——服务通信(三)

文章目录前言1. 服务通信理论模型2. 服务通信自定义srv2.1 定义srv文件2.2 编辑配置文件2.3 编译3. 服务通信自定义srv调用(C)3.1 vscode配置3.2 服务端3.3 客户端3.4 配置 CMakeLists.txt4. 服务通信自定义srv调用(python)4.1 vscode配置4.2 服务端4.3 客户端4.4 配置 CMakeLi…

将Android进行到底之内容提供者(ContentProvider)

文章目录前言一、ContentProvider是什么&#xff1f;二、使用示例1.为应用创建内容提供者2.使用内容提供者2.1 内容URI2.2 Uri参数解析2.2 使用内容URI操作数据3.ContentProvider妙用4 内容URI对应的MIME类型5.ContentProvider重点注意6 演示demo源码总结前言 随着现在的应用越…

java通过JDBC连接mysql8.0数据库,并对数据库进行操作

目录 一、JDBC简介 二、添加依赖 三、JDBC操作数据库的步骤 四、JDBC操作数据库——增删改查 (一)新增数据 (二)删除数据 (三)修改数据 (四)查询数据 (五)多表连接查询 一、JDBC简介 Java数据库连接&#xff0c;&#xff08;Java Database Connectivity&#xff0c;简…

C进阶:字符串相关函数及其模拟实现

目录 &#x1f431;&#x1f638;一.strlen &#x1f54a;️1.功能 &#x1f43f;️2.模拟实现 &#x1f42c;&#x1f40b;二.strcpy &#x1f432;1.功能 &#x1f916;2.注意事项 &#x1f47b;3.模拟实现 &#x1f431;&#x1f42f;三.strcat &#x1f984;1.功能…

i.MX8MP平台开发分享(IOMUX篇)- Linux注册PAD

专栏目录&#xff1a;专栏目录传送门 平台内核i.MX8MP5.15.71文章目录1. pinfunc.h2.pinctrl-imx8mp.c3.PAD信息注册这一篇开始我们深入Linux中的pinctl框架。1. pinfunc.h pinfunc.h中定义了所有的引脚&#xff0c;命名方式是MX8MP_IOMUXC___&#xff0c;例如下面的MX8MP_IO…

【JDBC】----------ServletContext和过滤器

分享第二十四篇励志语句 幸福是什么&#xff1f;幸福不一定是腰缠万贯、高官显禄、呼风唤雨。平凡人自有平凡人的幸福&#xff0c;只要你懂得怎样生活&#xff0c;只要你不放弃对美好生活的追求&#xff0c;你就不会被幸福抛弃。 一&#xff1a;ServletContext&#xff08;重要…

js对象篇

面向对象 对象 如果对象的属性键名不符合JS标识符命名规范&#xff0c;则这个键名必须用引号包裹 访问属性有2种方法&#xff0c;有点语法和方括号填写法&#xff0c;特别地&#xff0c;如果属性名不符合JS命名规范或者使用变量存储属性名&#xff0c;则必须使用方括号访问 属…

【王道操作系统】2.3.6 进程同步与互斥经典问题(生产者消费者问题、吸烟者问题、读者写者问题、哲学家进餐问题)

进程同步与互斥经典问题(生产者消费者问题、吸烟者问题、读者写者问题、哲学家进餐问题) 文章目录进程同步与互斥经典问题(生产者消费者问题、吸烟者问题、读者写者问题、哲学家进餐问题)1.生产者-消费者问题1.1 问题描述1.2 问题分析1.3 如何实现1.4 实现互斥的P操作一定在实现…

深化全面To C战略魏牌发布与用户共创大六座SUV蓝山

对魏牌而言&#xff0c;与用户共创不是吸引眼球的营销噱头&#xff0c;而是“直面用户需求&#xff0c;真实倾听用户意见”的有效途径。 2022年12月30日&#xff0c;第二十届广州国际汽车展览会&#xff08;以下简称“广州车展”&#xff09;正式启幕。魏牌以“品位蓝山 有咖有…

神经网络必备基础知识:卷积、池化、全连接(通道数问题、kernel与filter的概念)

文章目录卷积操作实际操作filter与kernel1x1的卷积层可视化的例子池化全连接卷积操作 这个不难理解。我们知道图像在计算机中是由一个个的像素组成的&#xff0c;可以用矩阵表示。 假设一个5x5的输入图像&#xff0c;我们定义一个3x3的矩阵&#xff08;其中的数值是随机生成的…

excel图表美化:设置标记样式让拆线图精巧有趣

折线图作为我们平时数据视图化非常常规的表现方式&#xff0c;想必大家已经司空见惯了。折线图很简单&#xff0c;每个人都会做&#xff0c;但是不同的人做出来的折线图却千差万别。大多数人的折线图都是直接插入默认折线图样式生成的&#xff0c;这样的折线图先不说有没有用心…

五、IDEA中创建Web项目

文章目录5.1 创建Web项目5.1.1 创建项目5.1.2 编写Servlet类5.2 手动部署项目5.3 自动部署项目5.3.1 IDEA集成Tomcat5.3.2 IDEA部署JavaWeb项目5.4 war包部署5.4.1 导出war包5.1 创建Web项目 5.1.1 创建项目 1、打开IDEA&#xff0c;单击“New Project”或者通过File–>ne…