React 的理念是数据驱动 UI,state 是 React 里面管理数据的重要概念。本文梳理一下操作组件内部 state 的重要 API setState 的用法。

基本使用

每个「会用」React 的开发者都知道更新组件内部的状态时,应该使用 setState,不过,笔者确实在真实的业务代码中看到过下面的写法:

// Wrong
this.state.comment = 'Hello';

很直接很暴力,动下脑子,如果可以这么写,React 还有必要专门搞一个 setState 的 API 吗?有且只有 constructor 一个例外可以直接给 this.state 赋值

class SomeComponent extends React.Component {
  constructor() {
    super();
    
    this.state = {};
  }
  // ...
}

或者:

class SomeComponent extends React.Component {
  // ES6+
  state = {};

  // ...
}

言归正传,setState API 接收两个参数:

setState(updater[, callback])
  • updater: 必选,我们比较熟悉的形式是一个对象,对象的值为要更新的状态;
  • callback: 可选,参数更新后的回调。

setState() 把组件的状态变化加入队列,并通知 React 该组件(及其子组件)需要使用新的状态重新渲染。

状态更新是异步的

看到 callback 这个参数,应该自然而然地想到「异步」。

没错 setState() 是个异步操作。基于性能考量,React 不保证 setState() 立即执行,也就是说会有一个延迟,然后再批量更新。

如果需要在状态确实更新以后执行某些操作,可以使用 componentDidUpdate 生命周期方法 或者 setState 回调参数。

还有其他办法保证取到的状态是最新的吗?答案是肯定的,别急,后面会提到的。

合并更新

来看一个例子:

class StateMergeDemo extends React.Component {
  state = {
    clicked: 0,
  };

  increaseClickTimes = (id) => {
    this.setState({
      clicked: this.state.clicked + 1,
    }, () => {
      console.log(`click ${id}: clicked value -> ${this.state.clicked}`);
    });
  };

  handleClick = () => {
    this.increaseClickTimes(1);
    this.increaseClickTimes(2);
    this.increaseClickTimes(3);
  };

  render() {
    return (
      <div>
        <p>clicked: {this.state.clicked}</p>
        <p onClick={this.handleClick}><button>Click me</button></p>
      </div>
    )
  }
}

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

See the Pen React State Updates are Merged by hegfirose (@minwe) on CodePen.

上面的代码中,点击按钮时调用了三次 setState(),有的同学可能会认为每点击一次,clicked 状态会增加 3,实际上只增加了 1。

前面提到,setState() 会延迟、批量执行。在一个周期的批量执行过程中,当 setState() 第一个参数为一个对象时,并不是依次执行队列中的 setState() 调用,而是先将对象浅合并(shallow merge),再执行更新。

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

所以,上面的演示中,每次点击计数只增加了 1。

函数式 setState

setState(updater[, callback])

前面提到,setState()updater 参数我们比较熟悉的形式是对象,言外之意,还有我们不太熟悉的形式——函数:

this.setState((prevState, props) => stateChange[, callback]);

该函数接收两个参数,分别是组件的上一个状态 prevState 和当前属性 props。这两个参数的值都能保证是最新的。该函数需要返回一个对象,对象的值为要修改的状态,React 会将返回的对象与当前状态浅合并,然后执行更新。如果不需要更新,可以返回 null

这就是所谓的「函数式 setState」。函数式 setState 会按照调用顺序加入队列,然后依次执行更新,每次 setState() 的 updater 函数的参数拿到的状态都是最新的,可以用于某些有状态依赖或者需要按一定顺序更新的场景。

调整一下刚才的演示代码:

  // ...
  increaseClickTimes = (id) => {
    this.setState({
-      clicked: this.state.clicked + 1,
+      return {
+        clicked: prevState.clicked + 1,
+      }
    }, () => {
      console.log(`click ${id}: clicked value -> ${this.state.clicked}`);
    });
  };
  // ...

See the Pen React State: functional setState by hegfirose (@minwe) on CodePen.

如你所愿,每次点击都会增加 3 次计数。

不知你是否留意到 callback 中的日志:

click 1: clicked value -> 3
click 2: clicked value -> 3
click 3: clicked value -> 3

也就是说,一个周期内的函数式 setState,其执行顺序是:

updater1()
updater2()
updater3()
callback1()
callback2()
callback3()

而不是:

updater1()
callback1()
updater2()
callback2()
updater3()
callback3()

借助函数式 setState,我们可以把状态更新逻辑拆分到组件外部独立文件中,方便维护、复用:

// 组件外部
function increaseTime(state, props) {
  return { clicked : state.clicked + 1 };
}

// 组件内部引用
class StateMergeDemo {
  // ...
  // inside your component class
  increaseClickTimes () {
    this.setState(increaseTime)
  }
  // ...
}

是不是看着有点面熟,有点 Redux 的感觉。

小结

本文梳理了 setState() 的用法以及其使用时容易踩坑的点,至于 setState() 背后更底层的实现,后续会再整理。

参考链接

延伸阅读