React中setState的详解

192 阅读5分钟

为什么要使用setState

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

如图,如果我们点击了+1,并修改了this.state的值,这个值是修改了,但是React并不知道(界面毫无反应)。Vue(Object.defineProperty,Proxy)修改了是会自动响应的,但是React和小程序都是不可以的。React必须通过setState。 image.png 我们必须告诉React修改了值,并且让它渲染最新的界面。(通过setState)

image.png

这样我们不经疑惑了,这里setState我们能直接用吗,这里(类中)也没定义啊。

我们这里App继承了Component,Component里面实现了这个方法。我们打开源码看看。(在react/index中找到component直接点进去) image.png

setState的异步更新

import React, { Component } from 'react'
export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      message: "Hello World"
    }
  }
  render() {
    return (
      <div>
        <h2>当前文本: {this.state.message}</h2>
        <button onClick={e => this.changeText()}>改变文本</button>
      </div>
    )
  }
  changeText() {
    this.setState({
      message:"你好啊张三"
    })
    console.log(this.state.message)
  }
}

image.png 我们看下打印的结果,我们会神奇的发现(这里文本已经变化了,打印的值还是HelloWorld)

image.png

为什么呢?因为这个setState的值是异步的更新,(不等待setState执行完,直接执行console.log)我们并不能在执行完setstate后立马拿到最新的state的结果。

为什么setState设计为异步呢?之前在Gihub上有过许多讨论。React核心成员(Redux)作者做出了解释。github.com/facebook/re…

中文翻译后得到的结论,我们做个总结。

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

另外我们这么也是框架的一个思想,如果我们同步更新这个东西,意味着我们一旦setState,这个state的东西就立马发生改变了,而render会慢一点,因为我们要重新返回一个reactElement这个对象,还要进行diff算法,之后再更新到真实dom里,这个render会慢一步的。 假如说我们这里有个子组件。

关于第二点。比如我们有个Home的子组件。 这里props保存的还是HelloWorld image.png

image.png

因为这是同步更新,render函数还没有执行完。这里Home(父组件里的),这个this.state.message还是原来的。而子组件里,自己的值没经过render函数。这个在更新的一刻,state和props会不一样。 这在开发中很不好。

如果我们想拿到更新之后的结果。

方式一:我们需要知道setState有两个参数(更新的state,回调函数)。这个回调函数会等到我们更新结束后,再回调它。

image.png

方式二:在compoonentDidUpdate中获取 (这个还要快于方式一,这个需要看源代码,不过知道结论就行了)

image.png

代码:

import React, { Component } from 'react'
export default class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      message: "Hello World"
    }
  }
  render() {
    return (
      <div>
        <h2>当前文本: {this.state.message}</h2>
        <button onClick={e => this.changeText()}>改变文本</button>
      </div>
    )
  }
  
  componentDidUpdate() {
    // 方式二: 获取异步更新的state
    console.log(this.state.message);
  }

  changeText() {
    this.setState({
      message:"你好啊张三"
    },() => {
      console.log(this.state.message);
    })
  }
}

有一些方式让setState同步更新

我们讲两种情况。

情况一:在定时器中 image.png

情况二:

image.png

异步与同步的情况总结:

  • 在组件生命周期或React合成事件(比如onClick这种)中,setState是异步
  • 在setTimeout或者原生dom事件中,setState是同步

React使用合成对象可以很方便的跑在浏览器(dom),移动端(控件)。

源码中查看它异步的操作。 image.png

image.png

这个updater要在下图找,我们可以看到这就是updater调用的equeue。。。方法 image.png

点进去 image.png

image.png 上下文。其实,合成对象/生命周期与定时器,它们不是一个上下文。react会根据上下文和事件的情况返回一个sync(同步处理),还有就是Batched(批量处理)

setState数据的合并

我们思考一个问题。 image.png

其实源码里是这样的。这里会帮我们合并的。 image.png

我们打开源码来看。

image.png

setState本身的合并

我们思考一个问题。 image.png 结果是1。

这个东西是会合并的。源码中有个do while循环,对多个state进行合并。这个源码可以自己去看。

其实我们是可以阻止它合并的,让它们进行累加。这也得看源码。

  increament() {
    this.setState((prevState,props) => {
      return {
        counter: prevState.counter + 1
      }
    })

    this.setState((prevState,props) => {
      return {
        counter: prevState.counter + 1
      }
    })

    this.setState((prevState,props) => {
      return {
        counter: prevState.counter + 1
      }
    })
  }

setState的数据需保持不可变

如果我们不写shouldComponentUpdate(SCU)优化,我们怎么写都行,但是写了SCU代码,就得保持setState不变。不过SCU最好是要写的。继承PureComponent跟实现了SCU的情况是一样的。

官方文档也有提到过 image.png

我们要演示这样一个案例 image.png

我们在开发中最好不要这么做,在shouldComponentUpdate(SCU)中会对state前后进行判断。这样子直接破坏了原来的数据,没有对比了。 image.png

如果我们手动加了这个函数,对比的前后就是同一个数组了。根本不可能加进去。(如果想知道为什么就要了解指针这个概念了) image.png

image.png

image.png

在开发中我们要这么做,如图 image.png

如果我们要年龄加一,要这么做 image.png