React 中 setState 同步更新策略

2017年5月7日 1.29k 次阅读 0 条评论 0 人点赞

参看文章:https://facebook.github.io/react/docs/react-component.html#setstate

https://zhuanlan.zhihu.com/p/24781259

setState 同步更新

为了提高性能 React 将 setState 设置为批次更新,即是异步操作函数,并不能以顺序控制流的方式设置某些事件,我们也不能依赖于 this.state 来计算未来状态。this.setState 会调用 render 方法,但并不会立即改变 state 的值, state 是在 render 方法中赋值。 所以执行 this.setState 后立即获取 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 通过引发一次组件的更新过程来引发重新绘制。多次 setState 函数调用产生的效果会合并。直观的写法我们可以连续调用两次 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 执行完毕之后的计数值,也顺利保证了连续自增两次。

 

我就是我,是颜色不一样的烟火。

文章评论(0)