setState详细解析

322 阅读3分钟

1.为什么使用setState

开发过程中我们不能直接通过修改state的值来让界面发生更新。

我们修改state之后,希望React根据最新的State来重新渲染界面,但是下面这种对state进行直接修改,React并不知道数据发生了变化。

所以,我们必须通过 setState 的方式来修改 state,以此来告知React此时数据已经发生改变。

import React, { Component } from 'react'

export default class App extends Component {
  constructor(props) {
    super(props);
    
    this.state = {
      counter: 0
    }
  }

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

  increment() {
    this.state.counter += 1; //直接修改state
    console.log(this.state.counter)
  }
}

上述中 increment() 修改如下:


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

setState方法是从Component中继承过来的。

image.png

2.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() {
    // setState是异步更新
    this.setState({
      message: "你好,裹着新的光"
    })
    console.log(this.state.message); // 输出 Hello World

  }
}

以上代码最终在控制台输出的是 Hello World。 这说明 setState 是异步的操作,并不是在执行完 setState 之后拿到 state 的最新结果。

2.1.为什么 setState 设计是异步的呢?

RFClarification: why is setState asynchronous? · Issue #11527 · facebook/react (github.com)

  • setState设计为异步,可以显著提升性能。
    • 如果每次调用setState都想进行一次更新,那么意味着 render 函数会被频繁调用,界面一次次的重新渲染,这样效率是很低的。
    • 最好的方法就是获取多个更新后,之后进行批量更新。
  • 如果同步更新了 state ,但是还没有执行 render 函数,那么state 和 props 不能保持同步。
    • state 与 props 不能保持一致性,会在开发中产生很多问题。
import React, { Component } from 'react';

function Home(props) {
  // Hello World
  return <h1>{props.message}</h1>
}

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>
        <Home message={this.state.message}/>  //将message传给组件 Home
      </div>
    )
  }

  changeText() {
    // 2.setState是异步更新
    this.setState({
      message: "你好,裹着新的光"
    })
    console.log(this.state.message); // Hello World

  }
}

点击按钮前:

image.png 点击按钮后:

image.png

2.2.如何获取异步更新的结果

  • 方式一:setState 回调函数
    • setState接受两个参数:第二个参数是一个回调函数,这个回调函数会在更新后执行

image.png

  • 方式二:也可在生命周期函数中进行
componentDidUpdate(prevProps, prevState, snapshot)
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() {
  
    // 方式一: 获取异步更新后的数据
    // setState(更新的state, 回调函数)
    this.setState({
      message: "你好啊,李银河"
    }, () => {
      console.log(this.state.message);
    })
  }
}

image.png

tips:根据源码,执行顺序是先执行 componentDidUpdate() ,再执行回调函数。两个函数都可获取更新后的结果。

3.setState是同步更新

3.1.情况一: 将setState放入到定时器中

  changeText() {
    // 情况一: 将setState放入到定时器中
    setTimeout(() => {
      this.setState({
        message: "你好啊,李银河"
      })
      console.log(this.state.message);
    }, 0);
  }

image.png

3.2.情况二: 将setState放入到原生的DOM事件中

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 id="btn">改变文本</button>
      </div>
    )
  }

  componentDidMount() {
    document.getElementById("btn").addEventListener("click", (e) => {
      this.setState({
        message: "你好啊,李银河"
      })
      console.log(this.state.message);
    })
  }

}

image.png

总结:

  • 在组件生命周期或者React合成事件中,setState是异步;
  • 在setTimeout或者原生DOM事件中,setState是同步;

4.setState数据的合并

import React, { Component } from 'react';

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

    this.state = {
      message: "Hello World",
      name: "zhLeon521"
    }
  }

  render() {
    return (
      <div>
        <h2>{this.state.message}</h2>
        <h2>{this.state.name}</h2>
        <button onClick={e => this.changeText()}>改变文本</button>
      </div>
    )
  }

  changeText() {
    // 了解真相你才能获得真正的自由
    this.setState({
      message: "你好啊,裹着心的光"
    });

    // Object.assign({}, this.state, {message: "你好啊,裹着心的光"})
  }
}

image.png

image.png

  • 通过setState去修改message,是不会对name产生影响的;
  • 源码中其实是有对 原对象 和 新对象进行合并的:
Object.assign({}, this.state, {message: "你好啊,裹着心的光"})

5.setState本身的合并

5.1.setState本身被合并

import React, { Component } from 'react'

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

    this.state = {
      counter: 0
    }
  }

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

  increment() {
    // 1.setState本身被合并
    this.setState({
      counter: this.state.counter + 1
    });
    this.setState({
      counter: this.state.counter + 1
    });
    this.setState({
      counter: this.state.counter + 1
    });

  }
}

点击前: image.png

点击后: image.png

5.2.setState合并时进行累加

setState 可以传入一个函数,源码中有这个

  increment() {
    // 2.setState合并时进行累加
    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
      }
    });
    this.setState((prevState, props) => {
      return {
        counter: prevState.counter + 1
      }
    });
  }

点击前: image.png

点击后: image.png