更合理的使用setState

React中的setState()为我们提供了组件内的状态管理方案,

本文将从Component的角度来说明更合理的 setState()

在 React 文档的 State and Lifecycle一章中,明确的说明 setState() 的用法,向 setState() 中传入一个对象来对已有的 state 进行更新。

我们如果想要对这个 state 进行更新的话,就可以这样使用 setState()

this.setState({
  count: 1
});

你可能不知道的

最基本的用法世人皆知,但是,在 React 的文档下面,还写着,处理关于异步更新 state 的问题的时候,就不能简单地传入对象来进行更新了。这个时候,需要采用另外一种方式来对 state 进行更新。

setState() 不仅能够接受一个对象作为参数,还能够接受一个函数作为参数。函数的参数即为 state 的前一个状态以及 props。

所以,我们可以向下面这样来更新 state:

this.setState((prevState, props) => ({ count: prevState.count + 1 }));

这样写的话,能够达到同样的效果。那么,他们之间有什么区别呢?

区别

为了能够明确的看出 state 的更新,采用一个比较简单的例子来进行说明。

设置一个累加器,在 state 上设置一个 count 属性,同时,为其增加一个 increment 方法,通过这个 increment 方法来更新 count

此处,采用给 setState() 传入对象的方式来更新 state,同时,在此处设置每调用一次 increment 方法的时候,就调用两次 setState()。具体的原因我在后文中会讲解。

具体的代码如下:

class IncrementByObject extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
    this.increment = this.increment.bind(this);
  }

  // 此处设置调用两次 setState()
  increment() {
    this.setState({
      count: this.state.count + 1
    });

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

  render() {
    return (
      <div>
        <button onClick={this.increment}>IncrementByObject</button>
        <span>{this.state.count}</span>
      </div>
    );
  }
}

ReactDOM.render(
  <IncrementByObject />,
  document.getElementById('root')
);

这时候,点击 button 的时候,count 就会更新了。但是,可能与预期的有所差别。设置了点击一次就调用两次 setState(),但是,count 每一次却还是只增加了 1,所以这是为什么呢?

其实,在 React 内部,对于这种情况,采用的是对象合并的操作,就和我们所熟知的 Object.assign() 执行的结果一样。

比如,我们有以下的代码:

Object.assign({}, { a: 2, b: 3 }, { a: 1, c: 4 });

那么,最终得到的结果将会是 { a: 1, b: 3, c: 4 }。对象合并的操作,属性值将会以最后设置的属性的值为准,如果发现之前存在相同的属性,那么,这个属性将会被后设置的属性所替换。所以,也就不难理解为什么调用了两次 setState() 之后,count 依然只增加了 1 了。

用简短的代码说明就是这样:

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

// 同理于
Object.assign({}, this.state, { count: this.state.count + 1 });

以上是采用对象的方式传入 setState() 来更新 state 的说明。接下来再看看使用函数的方式来更新 state 会有怎么样的效果呢?


将上面的累加器采用另外的方式来实现一次,在 setState() 的时候,用传入一个函数的方式来更新 state。

class IncrementByFunction extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
    this.increment = this.increment.bind(this);
  }

  increment() {
    // 采用传入函数的方式来更新 state
    this.setState((prevState, props) => ({
      count: prevState.count + 1
    }));
    this.setState((prevState, props) => ({
      count: prevState.count + 1
    }));
  }

  render() {
    return (
      <div>
        <button onClick={this.increment}>IncrementByFunction</button>
        <span>{this.state.count}</span>
      </div>
    );
  }
}

ReactDOM.render(
  <IncrementByFunction />,
  document.getElementById('root')
);

当再次点击按钮的时候,就会发现,累加器就会每次增加 2 了。

可以通过查看 React 的源代码来找出这两种更新 state 的区别 (此处只展示通过传入函数进行更新的方式的部分源码)。

在 React 的源代码中,可以看到这样一句代码:

this.updater.enqueueSetState(this, partialState, callback, 'setState');

然后,enqueueSetState 函数中又会有这样的实现:

queue.push(partialState);
enqueueUpdate(internalInstance);

所以,与传入对象更新 state 的方式不同,传入函数来更新 state 的时候,React 会把更新 state 的函数加入到一个队列里面,然后,按照函数的顺序依次调用。同时,为每个函数传入 state 的前一个状态,这样,就能更合理的来更新 state 了。


问题所在

那么,这就是传入对象来更新 state 会导致的问题吗?当然,这只是问题之一,还不是主要的问题。

之前也说过,在处理异步更新的时候,需要用到传入函数的方式来更新state。这样,在更新下一个 state 的时候,我们能够正确的获取到之前的 state,并在在其基础之上进行相应的修改。而不是简单地执行所谓的对象合并。

所以说,我们建议,在使用 setState 的时候,采用传入函数来更新 state 的方式,这样也是一个更合理的方式。

 

setState

posted @ 2022-02-17 12:03  shansally  阅读(56)  评论(0编辑  收藏  举报