setState 同步更新

我们在上文中提及,为了提高性能React将setState设置为批次更新,即是异步操作函数,并不能以顺序控制流的方式设置某些事件,我们也不能依赖于this.state来计算未来状态。典型的譬如我们希望在从服务端抓取数据并且渲染到界面之后,再隐藏加载进度条或者外部加载提示:


componentDidMount() {
    fetch('https://example.com')
        .then((res) => res.json())
        .then(
            (something) => {
                this.setState({ something });
                StatusBar.setNetworkActivityIndicatorVisible(false);
            }
        );
}


因为setState函数并不会阻塞等待状态更新完毕,因此setNetworkActivityIndicatorVisible有可能先于数据渲染完毕就执行。我们可以选择在componentWillUpdate与componentDidUpdate这两个生命周期的回调函数中执行setNetworkActivityIndicatorVisible,但是会让代码变得破碎,可读性也不好。实际上在项目开发中我们更频繁遇见此类问题的场景是以某个变量控制元素可见性:


this.setState({showForm : !this.state.showForm});


我们预期的效果是每次事件触发后改变表单的可见性,但是在大型应用程序中如果事件的触发速度快于setState的更新速度,那么我们的值计算完全就是错的。本节就是讨论两种方式来保证setState的同步更新。

完成回调

setState函数的第二个参数允许传入回调函数,在状态更新完毕后进行调用,譬如:


this.setState({
      load: !this.state.load,
      count: this.state.count + 1
    }, () => {
      console.log(this.state.count);
      console.log('加载完成')
    });


这里的回调函数用法相信大家很熟悉,就是JavaScript异步编程相关知识,我们可以引入Promise来封装setState:


setStateAsync(state) {
    return new Promise((resolve) => {
      this.setState(state, resolve)
    });
  }


setStateAsync 返回的是Promise对象,在调用时我们可以使用Async/Await语法来优化代码风格:


async componentDidMount() {
    StatusBar.setNetworkActivityIndicatorVisible(true)
    const res = await fetch('https://api.ipify.org?format=json')
    const {ip} = await res.json()
    await this.setStateAsync({ipAddress: ip})
    StatusBar.setNetworkActivityIndicatorVisible(false)
  }


这里我们就可以保证在setState渲染完毕之后调用外部状态栏将网络请求状态修改为已结束,整个组件的完整定义为:


class AwesomeProject extends Component {
  state = {}
  setStateAsync(state) {
    ...
  }
  async componentDidMount() {
   ...
  }
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          My IP is {this.state.ipAddress || 'Unknown'}
        </Text>
      </View>
    );
  }
}


传入状态计算函数

除了使用回调函数的方式监听状态更新结果之外,React还允许我们传入某个状态计算函数而不是对象来作为第一个参数。状态计算函数能够为我们提供可信赖的组件的State与Props值,即会自动地将我们的状态更新操作添加到队列中并等待前面的更新完毕后传入最新的状态值:


this.setState(function(prevState, props){
      return {showForm: !prevState.showForm}
   });


这里我们以简单的计数器为例,我们希望用户点击按钮之后将计数值连加两次,基本的组件为:


class Counter extends React.Component{
  constructor(props){
    super(props);
    this.state = {count : 0} 
    this.incrementCount = this.incrementCount.bind(this)
  }
  incrementCount(){
    ...
  }
  render(){
    return <div>
              <button onClick={this.incrementCount}>Increment</button>
              <div>{this.state.count}</div>
          </div>
  }
}


直观的写法我们可以连续调用两次setState函数,这边的用法可能看起来有点怪异,不过更多的是为了说明异步更新带来的数据不可预测问题。


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


上述代码的效果是每次点击之后计数值只会加1,实际上第二个setState并没有等待第一个setState执行完毕就开始执行了,因此其依赖的当前计数值完全是错的。我们当然可以使用上文提及的setStateAsync来进行同步控制,不过这里我们使用状态计算函数来保证同步性:


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


这里的第二个setState传入的prevState值就是第一个setState执行完毕之后的计数值,也顺利保证了连续自增两次。