如何理解react中的setState一定要用不可变值?

2,558 阅读8分钟

this.setState是class组件一个重要的概念,

他并不在函数组件中,因为函数组件根本没有this

我们直接进入正题

为什么说setState一定要用不可变值?

可能很小伙伴不理解,什么是不可变值?

一、什么是不可变值?

其实,不可变值可以理解为函数式编程的纯函数

也就是:在修改数值时,生成的数值不影响原来的数值

举个例子:

const a = 1

const b = a + 1

我们就可以理解,使用 a 生成 b 时, a 是不变的,

那可以理解为 a 就是不可变值,

毕竟,我们也用了 const 去定义一个常量 a

那反例就是

let a = 1

a++ // 等价于 a = a + 1

很明显,a已经改变了,那a就不是不可变值

经过,这简单的例子,我想你应该明白了什么是不可变值

那么接下来,就可以了解为什么setState一定要用不可变值了

二、为什么setState一定要用不可变值?

这里不卖关子,直接说结论:为了性能优化

用过react的小伙伴都知道

reacat在更新过程中,有shouldComponentUpdate这么一个生命周期函数

shouldComponentUpdate具有"拦截更新渲染"的作用

shouldComponentUpdate如果返回值是true那就是去更新渲染,反之,不会更新渲染

如果你觉得对上面的话很懵,那可以继续往下看

举个例子

import React from 'react'

export default class Page extends React.Component {
  state = {
    num0
  }

  handleClick = () => {
    this.setState({
      num0
    })
  }

  componentDidUpdate () {
    console.log('组件更新了~')
  }

  render () {
    return (
      <>
       <button onClick={this.handleClick}> 点击更新num </button>
      </>
    )
  }
}

这是一段非常简单的代码,num原来值是0,点击按钮时 num 赋值也为0

效果图:

setState时候,生命周期函数componentDidUpdate触发了更新

但是,我们在下面加一段代码

import React from 'react'

export default class Page extends React.Component {
  state = {
    num0
  }

  handleClick = () => {
    this.setState({
      num0
    })
  }
  
  /*新增代码开始*/
  /**
   * React生命周期函数: 是否要更新组件
   * @param nextProps 更新后的属性值
   * @param nextState 更新后的状态值
   * @returns {boolean} 返回 true 代表更新,返回 false 代表不更新
   */
  shouldComponentUpdate (nextProps, nextState) {
    if (nextState.num === this.state.num) {
      // nextState
      console.log('nextState', nextState.num)
      console.log('this.state'this.state.num)
      return false // 组件不更新
    }
    return true // 组件更新
  }
  /*新增代码结束*/
  
  componentDidUpdate () {
    console.log('组件更新了~')
  }

  render () {
    return (
      <>
       <button onClick={this.handleClick}>点击更新num</button>
      </>
    )
  }
}

效果图:

现在,你就明白 shouldComponentUpdate具有"拦截更新渲染"的作用 的意思了

但,你也应该明白:react的父组件更新,子组件也会随着更新

所以,如果父组件更新,而子组件不想更新

我们就可以用shouldComponentUpdate去做"拦截更新渲染"

举个例子:

目录结构:

父组件:

import React from 'react'
import AComp from '../components/AComp'

export default class Page extends React.Component {
  state = {
    num0,
    list: ['a''b''c']
  }

  handleClick = () => {
    this.setState({
      numthis.state.num + 1
    })
  }

  componentDidUpdate () {
    console.log('父组件更新了~')
  }

  render () {
    return (
      <>
        <button onClick={this.handleClick}>点击更新num</button>
        <p>{this.state.num}</p>
        <AComp list={this.state.list}/>
      </>
    )
  }
}

子组件:

import React from 'react'
import { isEqual } from 'lodash'

export default class AComp extends React.Component{

  componentDidUpdate () {
    console.log('子组件A更新了~')
  }

  shouldComponentUpdate (nextProps, nextState) {
    // 引入 lodash 的 isEqual 比较数组值是否相等
    if (isEqual(nextProps.listthis.props.list)) { 
      return false // 组件不更新
    }
    return true // 组件更新
  }

  render () {
    const { list } = this.props
    return(
      <ul>
        { list.map((item, index) =>{
          return (
            <li key={index}>
              {item}
            </li>
          )
        }) }
      </ul>
    )
  }
}

效果图:

那,说了这么多,到底和不可变值什么关系呢?

我们稍微修改下上述例子

那么有意思的就来了

父组件:

import React from 'react'
import AComp from '../components/AComp'

export default class Page extends React.Component {
  state = {
    num0,
    list: ['a''b''c']
  }

 // 修改的代码
  handleClick = () => {
    this.state.num++
    this.state.list.push('d')
    this.setState({
      numthis.state.num,
      listthis.state.list
    })
  }

  componentDidUpdate () {
    console.log('父组件更新了~')
  }

  render () {
    return (
      <>
        <button onClick={this.handleClick}>点击更新num</button>
        <p>{this.state.num}</p>
        <AComp list={this.state.list}/>
      </>
    )
  }
}

子组件:

import React from 'react'
import { isEqual } from 'lodash'

export default class AComp extends React.Component{

  componentDidUpdate () {
    console.log('子组件A更新了~')
  }


  shouldComponentUpdate (nextProps, nextState) {
    if (isEqual(nextProps.list,this.props.list)) { 
      // 引入 lodash 的 isEqual 比较数组值是否相等
      console.log('子组件的属性值'isEqual(nextProps.list,this.props.list))
      return false // 组件不更新
    }
    return true // 组件更新
  }

  render () {
    const { list } = this.props
    return(
      <ul>
        { list.map((item, index) =>{
          return (
            <li key={index}>
              {item}
            </li>
          )
        }) }
      </ul>
    )
  }
}

效果图:

这么写一看好像没有错误,但,其实有非常致命的错误

state 的 list 已经被修改了,子组件却没有更新!

但其实,如果你用shouldComponentUpdate在父组件上,也不会更新

为什么呢?

  this.state.list.push('d')
  this.setState({
    list: this.state.list
  })

因为push会改变原来的数组

也就是说this.state.list.push()其实已经改变了this.state.list

而还进行了一次this.setState,虽然没报错,但其实是错的

那乍一看

this.state.num++ //  this.state.num = this.state.num + 1
this.setState({
  num: this.state.num,
})

也是错的,为什么会更新?

因为shouldComponentUpdate默认是true

react作为很多人使用用的框架,是没办法阻止这些问题

所以,就暴露了一个钩子,让使用者自行使用

那,父组件的正确写法应该是:

import React from 'react'
import AComp from '../components/AComp'

export default class Page extends React.Component {
  state = {
    num0,
    list: ['a''b''c']
  }

  handleClick = () => {
    const arr = this.state.list.slice() // 进行一次数组复制
    arr.push('d')
    this.setState({
      numthis.state.num + 1,
      list: arr
    })
  }

  componentDidUpdate () {
    console.log('父组件更新了~')
  }

  render () {
    return (
      <>
        <button onClick={this.handleClick}>点击更新num</button>
        <p>{this.state.num}</p>
        <AComp list={this.state.list}/>
      </>
    )
  }
}

子组件不变

效果图:

所以说,小伙伴们应该注意的是:

①什么数组的api会改变数组,什么数组的api不会改变数组

②对象的深拷贝和浅拷贝

这也是前端面试经常考察的内容

当然,如果你觉得每次都要手写比较可以试试PureComponent

PureComponent会在每次更新前作一次浅比较

而,如果你想彻底拥抱不可变值可以去了解一下Immutability

毕竟,深拷贝还是比较消耗性能的~

感谢阅读