04_React性能优化(Key属性、shouldComponentUpdate、PureComponent、memo)

120 阅读4分钟

React渲染流程

React更新流程

  • React在props或state发生改变时,会调用React的render函数,创建新的虚拟DOM树

  • React基于新旧虚拟DOM树之间的差别来决定如何更新UI

    • 如果一棵树参照另一棵树进行完全比较更新,那么即使是最先进的算法,复杂度也为 O(n3) ,n是树中元素的数量
    • 如果在React中使用了该算法,那么在元素数量非常多的情况下,开销是非常昂贵的,性能会变得非常低效
  • React

    • 同层节点之间相互比较,不会跨节点比较
    • 不同类型的节点,产生不同的树结构
    • 开发过程中,可以通过key属性来指定哪些节点在不同的渲染下保持稳定

Key属性的作用

在遍历列表时,不添加key属性的情况下,会有下面这个警告:

有无key属性的几种情况:

  • 在最后位置插入子元素,这种情况有没有key意义不大

  • 在前面/中间插入子元素

    • 这种做法在没有key的情况下,后面子元素的位置都发生了变化,React默认是按照顺序来比较元素的,所以所有的子元素都会被修改
  • 当子元素拥有key时,React会比较新旧树中相同key的元素,而不是按照顺序进行比较

    • 在这种情况下,相同key在位置发生变化时仅仅进行位移,不需要进行任何修改
    • 将新的元素插入到指定位置即可

注意事项:

  1. key在同级元素中应该是唯一的,开发中一般使用数据的id
  2. 不要使用随机数(每次不一样)
  3. 使用数组的索引index,可以消除警告,但是对性能没有优化(索引可能会发生变化)

shouldComponentUpdate

调用setState函数后render函数会被调用,所有的子组件都需要重新render,进行diff算法,性能必然是很低的

  • 事实上,很多组件重新执行render不是必要的
  • 组件执行render函数应该有一个前提,就是在依赖的数据(state、props)发生改变时,再调用自己的render函数

React提供了一个生命周期方法shouldComponentUpdate(nextProps, nextState, nextContext)(简称SCU),该方法有两个参数,并且需要返回一个boolean类型

参数:

  • 参数一:nextProps-修改之后,最新的props
  • 参数二:nextState-修改之后,最新的state
  • 参数三:nextContext0修改之后,最新的context。仅当你指定了 static contextType(更新的)或 static contextTypes(旧版)时才可用。

返回值:

  • 返回值为true,调用render函数
  • 返回值为false,不调用render函数
  • 默认的返回值是true,也就是只要props或者state中的数据发生了变化,就会调用render函数
import React from 'react'

export class Home {
  shouldComponentUpdate(nextProps) {
    if (this.props.header !== nextProps.header) {
      return true
    }
    return false
  }

  render() {
    console.log("Home render")
    return (
      <div>
        <h2>标题:{this.props.header}</h2>
      </div>
    )
  }
}

export default Home

PureComponent

如果所有的类组件,都需要手动实现 shouldComponentUpdate ,那么会增加很多的工作量

React已经考虑到了这一点,实现方法:

  • 将 class 继承 PureComponent

在源码中,调用 !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState),这个shallowEqual就是进行浅层比较

高阶组件memo

在类组件中可以使用 PureComponent,那么函数组件呢?

  • 在函数组件的props没有发生变化时,同样不希望重新渲染其DOM结构

需要使用一个高阶组件memo对函数进行包裹:

  • 当message发生变化时,才会重新执行函数,否则不会重新执行
import { memo } from "react"

const Profile = memo(function(props) {
  console.log("Home render")
  return <h2>Home: {props.message}</h2>
})

// export default memo(Profile)
export default Profile

不可变数据的力量

不要直接修改状态的值,而是基于当前状态创建新的状态值

import React, { PureComponent } from 'react'

export class App extends PureComponent {
  constructor(props) {
    super(props)

    this.state = {
      books: [
        { name: "你不知道JS", price: 99, count: 1 },
        { name: "JS高级程序设计", price: 88, count: 1 },
        { name: "React高级设计", price: 78, count: 2 },
        { name: "Vue高级设计", price: 95, count: 3 },
      ]
    }
  }

  addNewBook() {
    const newBook = { name: "Angular高级设计", price: 88, count: 1 }

    // 错误的修改方式:直接修改原有的state, 重新设置一遍
    // 在PureComponent是不能重新渲染的
    // this.state.books.push(newBook)
    // this.setState({ books: this.state.books })

    // 正确的修改方式:赋值一份books, 在新的books中修改, 设置新的books
    const books = [...this.state.books]
    books.push(newBook)
    this.setState({ books: books })
  }

  addBookCount(index) {
    // this.state.books[index].count++
    
    const books = [...this.state.books]
    books[index].count++
    this.setState({ books: books })
  }

  render() {
    const { books } = this.state

    return (
      <div>
        <h2>数据列表</h2>
        <ul>
          {
            books.map((item, index) => {
              return (
                <li key={index}>
                  <span>name:{item.name}-price:{item.price}-count:{item.count}</span>
                  <button onClick={e => this.addBookCount(index)}>+1</button>
                </li>
              )
            })
          }
        </ul
        <button onClick={e => this.addNewBook()}>添加</button>
      </div>
    )
  }
}

export default App