react中性能优化、 memo、 PureComponent、shouldComponentUpdate 的使用

66 阅读6分钟

react中性能优化、 memo、 PureComponent、shouldComponentUpdate 的使用

React 是一个用于构建用户界面的 JavaScript 库,主要负责将数据转换为视图,要保证数据和视图的统一,react 通过 重新 render 来保证数据和视图的统一,但当数据没有变化时,视图重新渲染,就会造成不必要的性能浪费

看下面这个例子,

需求:likeList数据改变 更新子组件

// 点击每个button都会重新渲染子组件  如何解决
class Children extends React.Component {
  render() {
    console.log('Children  render ')
    return (
      <div>
        Children Component
        <div>likes:</div>
        <ul>
          {this.props.classInfo.student.likeList.map((likeList, index) => (
            <li key={index}>{likeList}</li>
          ))}
        </ul>
      </div>
    )
  }
}

export default class Parnet extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0,
      classInfo: {
        classNmae: '电信1班',
        student: {
          name: 'jack',
          likeList: ['girl'],
        },
      },
    }
  }

  render() {
    const { classInfo, count } = this.state
    return (
      <div>
        <button
          onClick={() => {
            this.setState({ count: count + 1 })
          }}
        >
          button1
        </button>
        <button
          onClick={() => {
            this.setState({ count: count + 1 })
            this.setState({ classInfo: { ...classInfo } })
          }}
        >
          button2
        </button>
        <button
          onClick={() => {
            this.setState({ count: count + 1 })
            this.setState({
              classInfo: { ...classInfo, classNmae: '电信1班' + Math.random() * 10 },
            })
          }}
        >
          button3
        </button>
        <button
          onClick={() => {
            classInfo.student.likeList.push('boy' + Math.random() * 10) // 数组加入数据
            this.setState({ count: count + 1 })
            this.setState({
              classInfo: {
                ...classInfo,
                student: { ...classInfo.student, likeList: [...classInfo.student.likeList] },
              },
            })
          }}
        >
          button3
        </button>
        <Children classInfo={classInfo} />
        count: {count}
      </div>
    )
  }
}

这个例子主要是用两个组件,一个 Parent 组件 和 一个 Children 组件,当我们在修改 Parent 组件的 count 时, Parent 组件应该重新渲染,保持数据 和 视图的统一,而 Children 组件 这个时候应不应该重新渲染呢,按道理,不会重新渲染,因为他本身的数据没有变,但当我们点击button1的时候,发现 每次 count 改变, Children 组件都会重新渲染一次,这明显存在问题,而 react 提供了几种方案,供我们解决这种问题:

类组件

PureComponet

React.PureComponent 类似于 React.Component。它们的不同之处在于React.Component 没有实现 shouldComponentUpdate(),但是 React.PureComponent实现了它。对属性和状态采用浅比较的方式。适用于简单数据类型的属性传递

如果传入的值,层次比较复杂,就需要我们深层次对比,使用``shouldComponentUpdate`生命周期函数进行比较。

class Children extends React.PureComponent {
  render() {
    console.log('Children  render ')
    return (
      <div>
        Children Component
        <div>likes:</div>
        <ul>
          {this.props.classInfo.student.likeList.map((likeList, index) => (
            <li key={index}>{likeList}</li>
          ))}
        </ul>
      </div>
    )
  }
}

export default class Parnet extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0,
      classInfo: {
        classNmae: '电信1班',
        student: {
          name: 'jack',
          likeList: ['girl'],
        },
      },
    }
  }

  render() {
    const { classInfo, count } = this.state
    return (
      <div>
        <button
          onClick={() => {
            this.setState({ count: count + 1 })
          }}
        >
          button1
        </button>
        <button
          onClick={() => {
            this.setState({ count: count + 1 })
            this.setState({ classInfo: { ...classInfo } })
          }}
        >
          button2
        </button>
        <button
          onClick={() => {
            this.setState({ count: count + 1 })
            this.setState({
              classInfo: { ...classInfo, classNmae: '电信1班' + Math.random() * 10 },
            })
          }}
        >
          button3
        </button>
        <button
          onClick={() => {
            classInfo.student.likeList.push('boy' + Math.random() * 10) // 数组加入数据
            this.setState({ count: count + 1 })
            this.setState({
              classInfo: {
                ...classInfo,
                student: { ...classInfo.student, likeList: [...classInfo.student.likeList] },
              },
            })
          }}
        >
          button3
        </button>
        <Children classInfo={classInfo} />
        count: {count}
      </div>
    )
  }
}

运行结果

  • button1点击 不会触发子组件重新渲染 因为classInfo地址值指向一样 效果已经达到 ok

发现问题

我们的需求是:父组件中likeList数据改变 更新子组件

  • button2点击 【会触发子组件重新渲染,因为classInfo地址值指向不一样】,但是数据没有发生变化 【不是我们期待的结果】 如何解决? 问题
  • button3点击 【会触发子组件重新渲染,因为classInfo地址值指向不一样】,【虽然里面的数据发生变化,但是子组件期待的数据没有发生变化】 【我们期待的结果是不渲染】 如何解决? 问题
  • button4点击 会触发子组件重新渲染 因为classInfo地址值指向不一样 里面期待的数据有发生变化 子组件重新渲染 【是我们期待的结果】ok

如何解决上面两个问题呢?使用:shouldComponentUpdate

shouldComponentUpdate

类生命周期函数 shouldComponentUpdate 如果返回 false,该组件就不会重新渲染,我们认为,只要 Children 组件当前的 likeList 和 下一次 更新的值地址值相等,那么 Children 组件就没必要重新渲染,shouldComponentUpdate 函数 接受两个参数,第一个是 下一次更新的 porps 值,第二个 是 下一次 更新的 state 值, 只需要当 nextProps.classInfo.student.likeList !== this.props.classInfo.student.likeList 时,return falseChildren 就不会重新渲染

class Children extends React.Component {
  // 通过shouldComponentUpdate生命周期函数,自己重写方法判断是否重新渲染
  shouldComponentUpdate(nextProps, nextState) {
    let isTriggerRender = false // 返回 true 时,触发 re-render

    // 如果likeList数据改变 re-render
    if (nextProps.classInfo.student.likeList !== this.props.classInfo.student.likeList) {
      isTriggerRender = true
    }
    console.log('isTriggerRender', isTriggerRender)
    return isTriggerRender
  }

  render() {
    console.log('Children  render ')
    return (
      <div>
        Children Component
        <div>likes:</div>
        <ul>
          {this.props.classInfo.student.likeList.map((likeList, index) => (
            <li key={index}>{likeList}</li>
          ))}
        </ul>
      </div>
    )
  }
}

export default class Parnet extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      count: 0,
      classInfo: {
        classNmae: '电信1班',
        student: {
          name: 'jack',
          likeList: ['girl'],
        },
      },
    }
  }

  render() {
    const { classInfo, count } = this.state
    return (
      <div>
        <button
          onClick={() => {
            this.setState({ count: count + 1 })
          }}
        >
          button1
        </button>
        <button
          onClick={() => {
            this.setState({ count: count + 1 })
            this.setState({ classInfo: { ...classInfo } })
          }}
        >
          button2
        </button>
        <button
          onClick={() => {
            this.setState({ count: count + 1 })
            this.setState({
              classInfo: { ...classInfo, classNmae: '电信1班' + Math.random() * 10 },
            })
          }}
        >
          button3
        </button>
        <button
          onClick={() => {
            classInfo.student.likeList.push('boy' + Math.random() * 10) // 数组加入数据
            this.setState({ count: count + 1 })
            this.setState({
              classInfo: {
                ...classInfo,
                student: { ...classInfo.student, likeList: [...classInfo.student.likeList] },
              },
            })
          }}
        >
          button3
        </button>
        <Children classInfo={classInfo} />
        count: {count}
      </div>
    )
  }
}

函数组件

memo

说了这么多,我们没有说到 memo, 但其实我们已经说完了,在使用 class 去创建 组件时,我们可以使用 PureComponent,或者重写生命周期shouldComponentUpdate ,但当我们使用 函数组件时,我们就没办法 继承 PureComponent,所以这个时候就用到 memo

①简单类型数据 解决方法:使用 React.memo(ChildrenFunc)包裹一下组件

const Children = React.memo(ChildrenFunc)

①复杂类型数据*

如何解决button2,button3问呢 解决方法 在memo的第二参数中,自己写方法判断是否让子组件渲染

function arePropsEqual(prevProps, nextProps) {
  let isTriggerRender = true // 返回 true 时,不会触发 render

  // 期待的数据变化,触发子组件更新
  if (prevProps.classInfo.student.likeList !== nextProps.classInfo.student.likeList) {
    isTriggerRender = false
  }

  console.log('isTriggerRender', isTriggerRender)
  return isTriggerRender
}

const Children = React.memo(ChildrenFunc, arePropsEqual)

完整代码

import React, { useState } from 'react'

function ChildrenFunc(props) {
  console.log('ChildrenFunc render')
  return (
    <div>
      Children Component classInfo
      <p>
        classNmae--{props.classInfo.classNmae} studentName: {props.classInfo.student.name}
      </p>
      likes:
      <ul>
        {props.classInfo.student.likeList.map((likeList, index) => (
          <li key={index}>{likeList}</li>
        ))}
      </ul>
    </div>
  )
}

function arePropsEqual(prevProps, nextProps) {
  let isTriggerRender = true // 返回 true 时,不会触发 render

  // 期待的数据变化,触发子组件更新
  if (prevProps.classInfo.student.likeList !== nextProps.classInfo.student.likeList) {
    isTriggerRender = false
  }

  console.log('isTriggerRender', isTriggerRender)
  return isTriggerRender
}

// 需求:likeList数据改变 更新子组件

// button每次点击 都会触发子组件重新渲染  【不是我们期待的结果】 如何解决?

// 解决方法:使用 React.memo(ChildrenFunc)包裹一下组件
// const Children = React.memo(ChildrenFunc)
// 发现问题
// button1点击 不会触发子组件重新渲染 因为classInfo地址值指向一样 ok 效果已经达到
// button2点击 会触发子组件重新渲染 因为classInfo地址值指向不一样 但是数据没有发生变化 【不是我们期待的结果】 如何解决? 问题
// button3点击 会触发子组件重新渲染 因为classInfo地址值指向不一样 虽然里面的数据发生变化,但是子组件期待的数据没有发生变化 【我们期待的结果是不渲染】 如何解决? 问题
// button4点击 会触发子组件重新渲染 因为classInfo地址值指向不一样 里面期待的数据有发生变化 会让子组件重新渲染 【是我们期待的结果】

// 如何解决button2,button3问呢
// 解决方法 在memo的第二参数中,自己写方法判断是否让子组件渲染
const Children = React.memo(ChildrenFunc, arePropsEqual)

const Parnet = () => {
  const [count, setCount] = useState(0)

  const [classInfo, setClassInfo] = useState(() => ({
    classNmae: '电信1班',
    student: {
      name: 'jack',
      likeList: ['girl'],
    },
  }))

  return (
    <div>
      <button
        onClick={() => {
          setCount(pre => pre + 1)
        }}
      >
        button1
      </button>
      <button
        onClick={() => {
          setClassInfo({ ...classInfo })
          setCount(pre => pre + 1)
        }}
      >
        button2
      </button>
      <button
        onClick={() => {
          setClassInfo({ ...classInfo, classNmae: '电信1班' + Math.random() * 10 })
          setCount(pre => pre + 1)
        }}
      >
        button3
      </button>
      <button
        onClick={() => {
          classInfo.student.likeList.push('boy' + Math.random() * 10) // 数组加入数据
          setClassInfo({
            ...classInfo,
            student: { ...classInfo.student, likeList: [...classInfo.student.likeList] }, // likeList引用发生改变
          })
          setCount(pre => pre + 1)
        }}
      >
        button3
      </button>
      <Children classInfo={classInfo}></Children>
      count: {count}
    </div>
  )
}

export default Parnet