React学习笔记[4]✨~关于setState你必须要知道的👻

229 阅读4分钟

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

一、React中为什么使用setState

在使用React时,并不能通过直接修改state而让React重新渲染界面,必须通过setState才行:

  • 因为如果直接修改state中的内容,React是无法监听到其中的数据发生了改变
  • 这是由于React并没有实现类似于Vue2中的Object.defineProperty 或 Vue3中的Proxy的方式来对数据进行劫持、从而监听到数据的变化

二、使用setState的三种方式

  1. 基本使用方式
this.setState({
  message: "Hello React"
})
  1. 传入一个回调函数

该回调函数会将之前的state与props传递进来

使用该方式的好处为:

可以在回调函数中编写处理state的逻辑

可以获取到之前的state与props

this.setState((state, props) => {
  // 1.编写一些对新的state处理逻辑
  // 2.可以获取之前的state和props值
  console.log(this.state.message, this.props)
  return {
    message: "Hello React"
  }
})
  1. 如果期望修改数据后来处理一些逻辑,此时可采用第三种方式:

传入需要更新的内容及回调函数

该回调函数会数据更新后立即被执行

this.setState({ message: "Hello React" }, () => {
  console.log("修改后的数据:", this.state.message) // 修改后的数据: Hello React
})

三、setState是同步的开始异步的

setState是异步的

import React, { Component } from 'react'

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

    this.state = {
      counter: 0
    }
  }
  increment() {
    console.log("increment...")
    this.setState({
      counter: this.state.counter + 1
    })
    console.log('counter...', this.state.counter)
  }

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

    return (
      <div>
        <h2>当前计数: {counter}</h2>
        <button onClick={e => this.increment()}> + 1</button>
      </div>
    )
  }
}

export default App

在使用setState后,输出的counter仍是执行+1之前的数据,可见setState是异步的

为什么setState要设计为异步的

setState之所以要设计为异步的主要原因有两个:

原因1

setState设计为异步的可以显著提升性能

  • 如果每次调用setState都进行一次更新那么就意味着render函数会被频繁调用,界面会被频繁的重新渲染,这样效率是很低的
  • React采用的方法是获取到多个更新,然后进行批量更新(setState将会对组件state的更改放入一个队列中,并通知React需要使用更新后的state重新渲染此组件及子组件。React将setState视为请求而不是立即更新组件的命令,为了更好的提高性能,React会延迟调用它,然后通过一次传递来更新多个组件

比如,在多次通过setState来修改counter时,render函数只会重新渲染一次

import React, { Component } from 'react'

function Hello(props) {
  return <h2>{props.message}</h2>
}

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

    this.state = {
      counter: 0
    }
  }
  increment() {
    console.log("increment...")
    this.setState({
      counter: this.state.counter + 1
    })
    this.setState({
      counter: this.state.counter + 1
    })
    this.setState({
      counter: this.state.counter + 1
    })
  }

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

    return (
      <div>
        <h2>当前计数: {counter}</h2>
        <button onClick={e => this.increment()}> + 1</button>
      </div>
    )
  }
}

export default App

需要注意的是,这里虽然使用了三次setState,但是在对counter进行更新的时候设置进去的this.state.counter此时为0,所以三次setState均相当于this.setState({ counter: 0 + 1 })

如果想让结果变为3,可写为:

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

原因2

如果同步更新了state,由于还未执行到render函数,那么state和props不能保持同步

state与props不同步的话会导致很多bug,也不方便问题的排查

四、如何获取到setState后的异步结果

如果想获取到setState后的结果,有两种方式:

方式1

通过setState的回调函数来获取

  • setState可接收两个参数,第二个参数为回调函数,该回调函数会在state更新后立即被执行
this.setState({ message: "Hello React" }, () => {
  console.log("修改后的数据:", this.state.message) // 修改后的数据: Hello React
})

方式2

在生命周期函数 componentDidUpdate 中获取更新后的数据

componentDidUpdate() {
  console.log(this.state.message)
}

五、setState一定是异步的吗

React18之前,setState是否为异步分为两种情况:

  1. 在组件生命周期或React合成事件中,setState是异步的
  2. 在setTimeout或者原生dom事件中,setState是同步的

而React18后,setState默认是异步的

在React18后,如果希望同步拿到setState后的结果,需要执行flushSync操作,但这可能会损害性能,慎用!

import React, { Component } from 'react'
import { flushSync } from 'react-dom'

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

    this.state = {
      counter: 0
    }
  }
  increment() {
    flushSync(() => {
      this.setState({
        counter: this.state.counter + 1
      })
    })
    console.log('counter....', this.state.counter)
  }

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

    return (
      <div>
        <h2>当前计数: {counter}</h2>
        <button onClick={e => this.increment()}> + 1</button>
      </div>
    )
  }
}

export default App