概述

setState中对于某个state多次修改,只执行一次(最后一次),所以可以将修改放在同一次中

import React, {Component} from 'react';

class Demotest extends Component {
  constructor(props) {
    super(props);
    this.state = {
      number: 1
    };
  }

  componentDidMount() {
    this.setState({ number: this.state.number + 1 });
     
  }

  addNumber(e) {
    this.setState({ number: this.state.number + 1 });
    this.setState({ number: this.state.number + 1 });
    this.setState({ number: this.state.number + 1 });
    console.log(this.state.number);
  }

  render() {
    return (
      <div>
        <div>
          <span>当前数字是{this.state.number}</span>
        </div>

        <br/>
        <div>
          <button onClick={e => {
            this.addNumber(e);
          }}>点击添加
          </button>
        </div>

      </div>
    );
  }
}

export default Demotest;

初始加载后

image.png

这时发现页面上显示的是2,控制台输出的却是1,按道理 componentDidMount 里的应该已经成功了,不然不会显示2,那为什么控制台输出的却是1 呢?

由于 setState 是异步的所以,所以同步代码执行结束后才会执行,所以在 console.log('componentDidMount: ', this.state.number);  执行的时候 state 还没有被改变,所以生命周期里的输出还是原来的值。
此时我们点击按钮,触发函数 addNumber  发现,函数里的三次 setState 只生效了一次 ,页面显示的数字变成了3,控制台输出了**2 **(对应上面代码20行),这是因为多次更新被合并,而异步的原因导致只输出了2。官方文档上明确说明,如果希望通过这里的状态更新一下个状态,需要在 setState 中使用函数来取得

image.png

  addNumber(e) {
    this.setState((nextState) => {
      console.log(nextState.number);
      return { number: nextState.number + 1 };
    });

    this.setState((nextState) => {
      console.log(nextState.number);
      return { number: nextState.number + 1 };
    });

    this.setState((nextState) => {
      console.log(nextState.number);
      return { number: nextState.number + 1 };
    });
  }

此时输出的是2、3、4

这里要说明的是当你在 ****willMount** 前设进行 state 的设置不会 render 的触发,而事件和可以 componentDidMount 触发 render , 而且原生的事件可以优先这个机制

  componentDidMount() {
    document.body.addEventListener('click', this.updateData, false);
  }

  updateData = () => {
    this.setState({
      number: this.state.number + 1
    });
    console.log('componentDidMount: ', this.state.number);
  };

详解setState

上面概述了下setState会出现的’合并’,下面引用官网的一段话

setState() 将对组件 state 的更改排入队列,并通知 React 需要使用更新后的 state 重新渲染此组件及其子组件。这是用于更新用户界面以响应事件处理器和处理服务器数据的主要方式
setState() 视为_请求_而不是立即更新组件的命令。为了更好的感知性能,React 会延迟调用它,然后通过一次传递更新多个组件。React 并不会保证 state 的变更会立即生效。
setState() 并不总是立即更新组件。它会批量推迟更新。这使得在调用 setState() 后立即读取 this.state 成为了隐患。为了消除隐患,请使用 componentDidUpdate 或者 setState 的回调函数(setState(updater, callback)),这两种方式都可以保证在应用更新后触发。如需基于之前的 state 来设置当前的 state,请阅读下述关于参数 updater 的内容。
除非 shouldComponentUpdate() 返回 false,否则 setState() 将始终执行重新渲染操作。如果可变对象被使用,且无法在 shouldComponentUpdate() 中实现条件渲染,那么仅在新旧状态不一时调用 setState()可以避免不必要的重新渲染


下面通过几个例子分析setState在实际应用中的运用和注意点

附上一段源码

Component.prototype.setState = function(partialState, callback) {
  // 校验是否符合三种情况
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    // 接受状态变量的对象以进行更新或者通过函数返回状态变量的对象
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
  enqueueSetState: function (publicInstance, partialState) {
    if (process.env.NODE_ENV !== 'production') {
      ReactInstrumentation.debugTool.onSetState();
      process.env.NODE_ENV !== 'production' ? warning(partialState != null, 'setState(...): You passed an undefined or null state object; ' + 'instead, use forceUpdate().') : void 0;
    }

    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');

    if (!internalInstance) {
      return;
    }

    var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
  }
function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance);
}

function enqueueUpdate(component) {
  ensureInjected();

  // Various parts of our code (such as ReactCompositeComponent's
  // _renderValidatedComponent) assume that calls to render aren't nested;
  // verify that that's the case. (This is called by each top-level update
  // function, like setState, forceUpdate, etc.; creation and
  // destruction of top-level components is guarded in ReactMount.)

  // 是批处理更新, 默认为false
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }

  dirtyComponents.push(component);
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}

setState的第一个参数

// 官网的api ---> 第一个参数可以是对象或者一个函数
setState(updater, [callback])

如果传入的是对象则会浅层合并到新的 state 中,后调用的 setState() 将覆盖同一周期内先调用 setState 的值

Object.assign(
  previousState,
  {quantity: state.quantity + 1},
  {quantity: state.quantity + 1},
  ...
)

相当于同个属性被合并。

如果第一个参数是函数,形式如下

(state, props) => stateChange

函数中接收的 state 和 props 都保证为最新, 是你的上次setState的状态值。

具体例子

import React, { Component } from 'react';
class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      number: 0
    }
  }
  
  // 运行一遍,不理解的对照注释看 
	 add () {
    // 进入队列 number: 0 + 222 = 222
    this.setState({
      number: this.state.number + 222
    })

    // 进入队列 number: 0 + 5 =5
     this.setState({
      number: this.state.number + 5
    })

    // 进入队列 number: 5 + 1 = 6
    this.setState((state, props) => {
      // 此时state是上次setState中的number ==>  5
      console.log('one', state);
      return {
        number: state.number + 1
      }
    })

    // 进入队列 number: 0 + 1 = 1
    this.setState({
      number: this.state.number + 1
    })

    // 进入队列: 1 + 2 = 3
    this.setState((state, props) => {
      // 此时state是上次setState中的number ==> 1
      console.log('two', state);
      return {
        number: state.number + 2
      }
    })

    // 进入队列: 3 + 1 = 4
    this.setState((state, props) => {
      // 此时state是上次setState中的number ==> 3
      console.log('three', state);
      return {
        number: state.number + 1
      }
    })
  }
  render () {
    return (
      <div>
        <button onClick={e =>{this.add()}}>add</button>
        {this.state.number}
      </div>
    );
  }
}

export default App;

输出分别为 5 、 1 、  3,最后页面显示的是4。

第二个参数

setState() 的第二个参数为可选的回调函数,它将在 setState 完成合并并重新渲染组件后执行。通常,我们建议使用 componentDidUpdate() 来代替此方式。

考虑如下场景:
在同个组件中一个具有进度条的页面需要不停的加载请求,只有当某次请求正确返回数据后出现新的内容区域。当数据返回后你将进度条比例设置为100,同时设置数据到state上。

this.ajax() {
	this.$post(xxx.com)
    .then(res => {
  		this.setState({
      	progress: 100,
        data: res
      })
  	})
    .catch(err => {console.log(err)}) 
}

设置为100%是为了数据到来后进度条立马拉满,然后在渲染对应的数据内容区域,这样写会出现进度条其实也拉满了,但是视觉效果没出来数据内容区域就出来的情况。所以用到第二个参数

this.ajax() {
	this.$post(xxx.com)
    .then(res => {
  		this.setState({
      	progress: 100,
      }, () => {
      	this.setState({
          data: res
        })
      })
  	})
    .catch(err => {console.log(err)}) 
}