React中为什么使用setState,同步AND异步

366 阅读5分钟

为什么使用setState

  • 开发中我们并不能直接通过修改state的值来让界面发生更新:
    • 因为我们修改了state之后,希望React根据最新的State来重新渲染界面,但是这种方式的修改React并不知道数据发生了变化;
    • React并没有实现类似于Vue2中的Object.defineProperty或者Vue3中的Proxy的方式来监听数据的变化;
    • 我们必须通过setState来告知React数据已经发生了变化;
  • setState方法是从Component中继承过来的

setState异步更新

setState(nextState, callback?)

  • nextState:一个对象或者函数。

    • 如果你传递一个对象作为 nextState,它将浅层合并到 this.state 中。
    • 如果你传递一个函数作为 nextState,它将被视为 更新函数。它必须是个纯函数,应该以已加载的 state 和 props 作为参数,并且应该返回要浅层合并到 this.state 中的对象。React 会将你的更新函数放入队列中并重新渲染你的组件。在下一次渲染期间,React 将通过应用队列中的所有的更新函数来计算下一个 state。
  • 可选的 callback:如果你指定该函数,React 将在提交更新后调用你提供的 callback

返回值 

setState 不会返回任何值。

注意 

  • setState 视为 请求 而会不是立即更新组件的命令。当多个组件更新它们的 state 来响应事件时,React 将批量更新它们,并在这次事件结束时将它们一并重新渲染。在极少数情况下,你需要强制同步应用特定的 state 更新,这时你可以将其包装在 flushSync 中,但这可能会损害性能。
  • setState 不会立即更新 this.state。这让在调用 setState 之后立即读取 setState 成为了一个潜在的陷阱。相反请使用 componentDidUpdate 或设置 setState的 callback 参数,其中任何一个都保证读取 state 将在 state 的更新后触发。如果需要根据前一个 state 来设置 state,那么可以传递给 nextState 一个函数
import React, { Component } from 'react'

export class App extends Component {
  constructor() {
    super()
    this.state = {
      message: "林夕",
    }
  }
  changeMessage() {
    // setState是异步的
    this.setState({
      message:"修改后的林夕"
    })
    //这里会发现当我们进行修改后再次调用并不会打印我们修改后的数据,而是打印修改之前的数据
    // 说明这里修改数据是异步执行,完成后才会修改我们state中的数据
    console.log(this.state.message);
   
  }
  render() {
    const { message } = this.state
    return (
      <div>
        <h1>{"message值:" + message}</h1>
        <button onClick={e => this.changeMessage()}>修改message</button>
      </div>
    )
  }
}

export default App
  • 为什么setState设计为异步呢?
    • setState设计为异步其实之前在GitHub上也有很多的讨论;
    • React核心成员(Redux的作者)Dan Abramov也有对应的回复,有兴趣的可以参考一下;
    • github.com/facebook/re…
  • 示例
import React, { Component } from 'react'

export class App extends Component {
  constructor() {
    super()
    this.state = { count: 0 }
  }
  add() {
    // //我们这里执行了两次修改但是最终渲染render函数只执行一次。
    // this.setState({ count: this.state.count + 1 })
    // this.setState({ count: this.state.count + 1 })
    // 如果需要递增的话我们要使用这种函数方式来执行,但是render函数依旧只会执行一次
    this.setState((state)=>({count: state.count + 1}))
    this.setState((state)=>({count: state.count + 1}))
  }
  render() {
    const { count } = this.state
    console.log("render函数被执行");
    return (
      <div>
        <h1>{"count值:" + count}</h1>
        <button onClick={e => this.add()}>修改count</button>
      </div>
    )
  }
}

export default App
  • 总结:
    • setState设计为异步,可以显著的提升性能;
    • 如果每次调用 setState都进行一次更新,那么意味着render函数会被频繁调用,界面重新渲染,这样效率是很低的;
    • 最好的办法应该是获取到多个更新,之后进行批量更新;
  • 如果同步更新了state,但是还没有执行render函数,那么state和props不能保持同步;
    • state和props不能保持一致性,会在开发中产生很多的问题;

例如上面例子如果是同步执行的话count作为参数传递给子组件,若修改count后没有执行render函数,那么子组件获取的数据一九是没有更新的数据即为0

如何获取异步的结果

  • 但是为了能拿到更新后的值这时候我们需要对setState传第二个参数callback,同时介绍了第二种用法
changeMessage() {
    // setState是异步的
    // 1.基本使用setstate在React的事件处理中是一个异步调用
    this.setState({
      message:"修改后的林夕"
    },()=>{
      // 修改完成后执行此函数
      console.log(this.state.message);
    })
    //这里会发现当我们进行修改后再次调用并不会打印我们修改后的数据,而是打印修改之前的数据
    // 说明这里修改数据是异步执行,完成后才会修改我们state中的数据
    console.log(this.state.message);
    // 2.setState传入回调函数
    // this.setState((state, props)=>{
    //   // 当前的回调函数会将之前的state 和props 传递进来
    //   return{
    //     message:"我是回调函数修改后的林夕"
    //   }
    // })
  }

也可以在生命周期函数中获取更新后的值使用componentDidUpdate(prevProps, prevState, snapshot?)

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

总结

在React18之前:

  • 在组件生命周期或React合成事件中,setState是异步;
  • 在setTimeout或者原生dom事件中,setState是同步;
  add() {
    // 在react18之前,setTimeout中setstate操作,是同步操作
    // 在react18之后,setTimeout中setstate异步换作(批处理)
    setTimeout(()=>{
      this.setState({ count: this.state.count + 1 })
      console.log(this.state.count);
    },0)
  }
  componentDidMount(){
    const btnElement=document.getElementById("btn")
    btnElement.addEventListener("click",()=>{
      this.setState({ count: this.state.count + 1 })
      console.log(this.state.count);
    })
  }

setState默认是异步的

如果需要同步拿到可以使用`flushSync – React 中文文档

慎用:可能影响性能

修改示例

import { flushSync } from "react-dom"
add() {
    setTimeout(()=>{
      flushSync(()=>{
        this.setState({ count: this.state.count + 1 })
      })
      console.log(this.state.count);
    },0)
  }