React学习笔记[10]✨~关于React中的性能优化(key & SCU & PureComponent)👻

60 阅读5分钟

我正在参加「掘金·启航计划」

React在props或state发生改变时,会重新执行render函数,产生新的DOM树;新旧DOM树会进行diff操作,计算出差异后进行更新。React需要基于这两棵不同的DOM树之间的差别来判断如何有效的更新UI

如果一棵树完全去参考另一颗DOM树进行比较,即使采用最先进算法,算法的复杂程度仍然是O(n^3) (n代表树中元素的数量)

所以,如果React中采用了这种算法,如果是1000个子元素,那么计算量将在十亿量级,这个开销太昂贵了,更新效率会非常的低。

React中采用了以下算法进行优化,将复杂度降低为O(n):

同层节点比较,不会跨节点比较;

如果产生了不同类型的节点,则不会去进行对比,直接会生成新的树结构;

开发中通过key来指定节点以保证渲染的稳定性;

一、keys的优化

如果在遍历列表时没有指定key,React会报如下警告:

【如果在最后位置插入数据】

  • 此时有无key意义并不大

【如果在前面插入数据】

  • 在没有key的情况或者key为列表元素的index的话(因为index会发生改变),所有的li都要进行修改
  • 如果设置了唯一的值作为key,那么key相同的元素只需要位移即可,不需要进行任何修改,然后将新插入的内容放到最前面即可

key的注意事项

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

二、render函数的优化(SCU)

如果在一个父组件中嵌套了两个子组件,父组件中state的数据发生了变化触发了render,此时不管向子组件传递的数据有无发生变化,所有的子组件render函数均会重新执行。

先来看以下案例:

在App中使用了子组件Home与Recommend,其中Home使用App中的message数据、Recommend使用App中的counter数据

App.jsx:

import React, { Component } from 'react'
import Home from './Home'
import Recommend from './Recommend'
import Profile from './Profile'


export class App extends Component {
  constructor() {
    super()

    this.state = {
      message: "Hello World",
      counter: 0
    }
  }

  changeText() {
    this.setState({ message: "Hello React" })
  }

  increment() {
    this.setState({ counter: this.state.counter + 1 })
  }

  render() {
    console.log("App render")
    const { message, counter } = this.state

    return (
      <div>
        <h2>App-{message}-{counter}</h2>
        <button onClick={e => this.changeText()}>修改文本</button>
        <button onClick={e => this.increment()}>counter+1</button>
        <Home message={message}/>
        <Recommend counter={counter}/>
      </div>
    )
  }
}

export default App

Home.jsx:

import React, { Component } from 'react'

export class Home extends Component {
  render() {
    console.log("Home render...")
    return (
      <div>
        <h2>Home Page: {this.props.message}</h2>
      </div>
    )
  }
}

export default Home

Recommend.jsx:

import React, { Component } from 'react'

export class Recommend extends Component {
  render() {
    console.log("Recommend render...")
    return (
      <div>
        <h2>Recommend Page: {this.props.counter}</h2>
      </div>
    )
  }
}

export default Recommend

此时无论是修改文本还是修改数字,Home与Recommend组件均会重新执行render函数

在开发中,只是修改了App中的数据,所有的子组件都要重新进行render,进行diff算法,性能必然会很低:

事实上很多子组件没有必要进行重新render(比如以上案例中,如果只修改了message,Recommend组件是没有必要重新render的,因为Recommend中只使用到了父组件中的counter)

那么如何来控制组件中何时进行重新render呢?

shouldComponentUpdate

我们可以通过类组件中的生命周期函数:shouldComponentUpdate来控制是否重新执行render函数。

该方法有两个参数:

  • nextProps 修改之后的最新的props
  • nextState 修改之后的最新的state

返回值为boolean类型:

  • 返回true则重新执行render
  • 返回false则不会重新执行render
  • 默认返回true

此时Home组件与Recommend组件中可以优化为:

Home.jsx:

import React, { Component } from 'react'

export class Home extends Component {
  shouldComponentUpdate(nextProps) {
    return nextProps.message !== this.props.message
  }
  render() {
    console.log("Home render...")
    return (
      <div>
        <h2>Home Page: {this.props.message}</h2>
      </div>
    )
  }
}

export default Home

Recommend.jsx:

import React, { Component } from 'react'

export class Recommend extends Component {
  shouldComponentUpdate(nextProps) {
    return nextProps.counter !== this.props.counter
  }
  render() {
    console.log("Recommend render...")
    return (
      <div>
        <h2>Recommend Page: {this.props.counter}</h2>
      </div>
    )
  }
}

export default Recommend

可见,修改message时Recommend不再会重新渲染;修改counter时Home不再会重新渲染:

PureComponent

如果所有的组件都需要我们手动实现以下shouldComponentUpdate方法会增加非常多的工作量,事实上React已考虑到了这点,默认帮我们实现好了,我们只需要让类组件继承自PureComponent即可

在PureComponent中会将props与state进行浅层的比较:

示例

import React, { PureComponent } from 'react'

export class Home extends PureComponent {
  render() {
    console.log("Home render...")
    return (
      <div>
        <h2>Home Page: {this.props.message}</h2>
      </div>
    )
  }
}

export default Home

不再需要手动实现shouldComponentUpdate,直接继承PureComponent即可

memo

shouldComponentUpdate是类组件的生命周期,那么在函数组件中需要通过高阶组件memo来实现props的对比

import { memo } from 'react'

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

export default Home

三、如何正确在PureComponent触发重新渲染

如果一个类组件继承了PureComponent后,在组件中渲染列表结构并对列表中的数据做操作时需要注意了:

由于PureComponent在对比props与state数据时是浅层比较 所以如果直接对数组进行push操作、或者对数组中的某一项数据进行修改,都是不会触发重新render的 (因为数组的地址值并没有发生改变)

错误示范

我们首先来看以下错误的示范:

import React, { PureComponent } from 'react'

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

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

    this.state.books.push(newBook)
    this.setState({ books: this.state.books })
  }

  addBookCount(index) {
    this.state.books[index].count++
  }

  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

可以发现,当点击添加数据与+1操作按钮时页面均无反应

因为这些操作都是在原数组的基础上进行操作的数组的地址值并没有发生改变,PureComponent中在进行浅层比较的时候会认为state中的数据并未发生改变,所以此时不会重新执行render函数

正确做法

正确的做法是:

  1. 首先通过 const newArr = [ ...oldArr ] 来创建一个与原数组数据相同的新数组(让地址值发生改变)
  2. 然后在此数组的基础上进行数组操作
  3. 最后将操作完的数组通过setState合并进去即可
addNewBook() {
  const books = [...this.state.books]
  const newBook = { name: "React高级设计", price: 88, count: 1 }
  books.push(newBook)
  this.setState({ books: books })
}

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