React 性能优化

91 阅读4分钟

1. React 更新机制

1.1 渲染流程

image.png

1.2 更新流程

image.png

  • React在props或state发生改变时,会调用React的render方法,会创建一颗不同的树

  • React需要基于这两颗不同的树之间的差别来判断如何有效的更新UI:

    • 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为 O(n³),其中 n 是树中元素的数量
    • 如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围
    • 这个开销太过昂贵了,React的更新性能会变得非常低效
  • 于是,React对这个算法进行了优化,将其优化成了O(n),如何优化的呢?

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

2. keys的优化

  • 遍历列表的时候,添加key属性可以起到优化效果

    • 在最后位置插入数据:这种情况,有无key意义并不大

    • 在前面插入数据:这种做法,在没有key的情况下,所有的li都需要进行修改

    • 当子元素(这里的li)拥有 key 时,React 使用 key 来匹配原有树上的子元素以及最新树上的子元素,可以复用

  • key的注意事项:

    • key应该是唯一的
    • key不要使用随机数(随机数在下一次render时,会重新生成一个数字)
    • 使用index作为key,对性能是没有优化的

3. render函数的优化

React 每次调用 setState 函数时都会重新执行render函数,如果当前组件含有子组件,那么子组件的render函数也会执行;如果调用 setState 时数据并未发生更新,但是render函数会执行,无疑又是性能浪费

3.1 shouldComponentUpdate(SCU)

  • 修改数据调用 setState ,所有的组件都需要重新render,进行diff算法,性能必然是很低的:

    • 事实上,很多的组件没有必须要重新render
    • 它们调用render应该有一个前提,就是依赖的数据(state、props)发生改变时,再调用自己的render方法
  • SCU

    • SCU优化就是 一种巧妙的技术,用来减少DOM操作次数,具体为当React元素没有更新时,不会去调用render()方法

    • 通过 shouldComponentUpdate来判断 props/state中的值是否改变,再决定是否调用render函数

  • shouldComponentUpdate

    • 该方法有两个参数:

      • nextProps:修改之后,最新的props属性
      • nextState:修改之后,最新的state属性
    shouldComponentUpdate(nextProps, nextState) {
      if(this.state.message !== nextState.message || this.state.counter !== nextState.counter) {
        return true
      }
      return false
    }
    
    • 该方法返回值是一个boolean类型:

      • 返回值为true,那么就需要调用render方法
      • 返回值为false,那么就不需要调用render方法
      • 默认返回的是true,也就是只要state发生改变,就会调用render方法

3.2. PureComponent/memo

  • 如果所有的类,都需要手动来实现 shouldComponentUpdate,那么会给我们开发者增加非常多的工作量
    • 事实上React已经考虑到了这一点,所以React已经默认帮我们实现好了,只需要将class继承自PureComponent
  • PureComponent: 类组件

  • memo: 函数组件

    • 函数式组件我们在props没有改变时,也是不希望其重新渲染其DOM树结构,使用 memo 对其进行包裹
    import { memo } from "react";
    
    export default memo(function(props) {
      console.log('Profile render');
      return <h2>Profile: {props.message}</h2>
    })
    

4. state的不可变力量

  • 修改state中的某一个数据(引用类型)
    • 先对数据进行拷贝操作
    • 修改拷贝之后对象, 设置新对象
  • 注意: 值类型,在修改的时候,本身就全部替换掉了,所以不需要其他操作,直接改就可以
import React, { PureComponent } from 'react'

export default class App extends PureComponent {
  constructor() {
    super()
    this.state = {
      books: [
        { name: '你不知道的JavaScript', price: 98, count: 1 },
        { name: 'JavaScript高级程序设计', price: 128, count: 2 },
        { name: 'Vue3指南', price: 88, count: 2 },
        { name: 'React实战教程', price: 88, count: 3 }
      ]
    }
  }

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

  addNewBook() {
    const newBook = { name: "设计模式之美", price: 119, count: 2 }

    // 1.直接修改原有的state, 重新设置一遍
    // 在PureComponent是不能引入重新渲染(re-render)
    // this.state.books.push(newBook)
    // this.setState({ books: this.state.books })

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

  render() {
    const {books} = this.state
    return (
      <div>
        <h2>书籍列表</h2>
        <ul>
          {
            books.map((book, index) => {
              return (
                <li key={index}>
                  <span>name:{book.name}-price:{book.price}-count:{book.count}</span>
                  <button onClick={() => this.addBookCount(index)}>+1</button>
                </li>
              )
            })
          }
        </ul>
        <div>
          <h2>添加新书籍</h2>
          <button onClick={() => this.addNewBook()}>添加书籍</button>
        </div>
      </div>
    )
  }
}

  • 注意PureComponent 仅仅会对新老 this.state.books 的值进行简单的对比。由于代码中 addNewBook 方法(第一种写法)改变了同一个 books 数组,使得新老 this.state.books 比较的其实还是同一个数组。即便实际上数组已经变了,但是比较结果是相同的。

  • PureComponent 内部是 通过 shallowEqual 进行浅层比较

    • 先判断是否是同一个state/props,比较两者的属性的长度是否相等
    • 再取出内部的某一项(引用类型)进行浅层比较,看是否是同一个对象