setState为什么不会同步更新组件状态

setState为什么不会同步更新组件状态

上次写了一篇《setState:这个API设计到底怎么样》,其中陈述了setState不会同步更新组件状态的事实,有朋友问:为什么说“setState肯定还是不能立刻更新this.state,不然React整个概念就被推翻了”?

我特别喜欢这样“为什么”的问题,无论做什么事,不能只是满足于只是知道一个知识点,而就该多问“为什么”,我们做技术的更应该这样。

那么,今天就来说一说,为什么setState不能改成同步更新组件状态呢?

(本文中所说的setState的使用情景都是React中标准用法,对于“非标准用法”导致this.state同步更新的情况,请看后续文章 setState何时同步 - 知乎专栏

让我们反过来想,假如setState改成是同步更新状态,那么React会是怎样一副模样。

假设,我们现在有机会来对React做一个重大设计调整,把setState的功能设定为同步更改this.state,也就是说,当setState函数返回的时候,this.state已经体现了状态的改变。

那就有两个设计的问题就直接摆在我们面前。


  1. setState更新状态之后要不要触发一次更新过程?
  2. 如何去触发更新过程?

对这两个问题,我们有三个选择答案。
  1. setState自动触发同步的组件更新过程;
  2. setState自动触发异步的组件更新过程;
  3. 干脆,setState根本不触发组件更新过程,让开发者显示驱动更新过程。

我们逐个看看各种选择,看他们这些选择是不是行得通,有什么优劣。

第一个选择:setState自动触发同步的组件更新过程

如果这样,也就是setState调用返回时,一个完整更新过程已经走完了,这样的设计,应该是不行的,因为每一次setState都会引发一次组件更新太浪费了啊!

setState引发的组件更新过程,包含生命周期函数有四个。

  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

每一次setState调用都走一圈生命周期,光是想一想也会觉得会带来性能的问题,其实这四个函数都是纯函数,性能应该还好,但是render函数返回的结果会拿去做Virtual DOM比较和更新DOM树,这个就比较费时间。

目前React会将setState的效果放在队列中,积攒着一次引发更新过程,为的就是把Virtual DOM和DOM树操作降到最小,用于提高性能。

好吧,我们退一步,就当我们对React和当今CPU和浏览器的性能充满信心,不在乎这点性能损耗,但是,这样的设计还是问题。

一个shouldComponentUpdate函数的参数是这样。

shouldComponentUpdate(nextProps, nextState) {
  //在这个函数被调用时,this.state还没有被改变
}

目前的React设计,shouldComponentUpdate被调用时,this.state并没有被改变,setState产生的状态改变是通过参数nextState来体现的,componentWillUpdate也是一样。

所以,为了让基于现有React体系的很多代码实现不要完蛋,我们肯定不能在shouldComponentUpdate之前修改this.state,修改this.state的时机肯定只能是在调用componentWillUpdate和render函数之间。

也就是说,即使setState号称“同步更新”this.state,实际上还是不能立即更新,因为setState引发的生命周期函数shouldComponentUpdate和componentWillUpdate里,this.state还没有改变。

听起来,解决得并不是很彻底,似乎把事情搞得更复杂了。

第二个选择:setState自动触发异步的组件更新过程

这种选择下,setState返回时,this.state已经被改变了,但是并没有立即引发更新过程,React依然将setState产生的结果放在队列里,等到时机合适时走更新过程。

这样肯定不行啊,如果setState把this.state改了,那shouldComponentUpdate和componentWillUpdate咋办?这两个函数一直就假设执行时this.state并没有被改变啊。

这种选择无疑是行不通的。


第三个选择:setState根本不触发组件更新过程

前两个选择都不怎样,那就看这第三个选择,setState只修改this.state,并不出发组件更新过程,那我们就需要另外一个函数用来主动触发更新状态,可是……如果真的这样的话,还需要setState干吗?


你看,setState既然和组件的更新过程没有关系,那我们直接操作this.state好了,对不对?

如果你真的喜欢这种方式,其实都不用重新设计React,现在的React就可以这么玩:直接操作this.state来同步修改组件状态,让后调用this.setState,不用任何参数,相当于空放一枪,唯一的目的就是主动触发一次更新状态。

这样,《setState:这个API设计到底怎么样》中的incrementMultiple函数就可以这么写。


  incrementMultiple() {
    this.state.count = this.state.count + 1;
    this.state.count = this.state.count + 1;
    this.state.count = this.state.count + 1;

    this.setState();
  }

三次都是直接读取this.state.count,三次都是直接修改this.state.count,结果正确,每次调用incrementMultiple真的能让this.state上的count值增加3,而且组件重行绘制。

觉得怎么样?觉得这是一个高招,还是一个阴招?

反正我看到自己写出来的这招,反应也是:呵呵。

如果用这种方法来写code,那么React也就不够React了,算不上Reactive

React何以称为React

React虽然并不像Rx.js那样高举Reactive Programming(响应式编程)的大旗,但是依然体现了Reactive Programming的思想。

Reactive Programming通俗说就是这样的编程风格:改变一个东西,另一个东西会做出响应发生改变,而不用我们的Code去主动让另一个东西做出改变。

大家都用过Excel,Excel就代表了Reactive Programming。

请把想象有这么一个Excel表格,在一个格子A1里填上1,在另一个格子A2里加上公式=A1\times 2,这时候A2里显示的就是2,接下来,我们把A1改成2,A2里就变成了4。

没错,这就是Reactive Programming,因为我们设定好公式之后,只要改变一个地方,另一个地方就自动发生了变化,而不需要去按个按钮什么的去调用那个公式。

还记得关于React的那个公式吗?UI = f(state) ,我们的代码就是那个f,和Excel表格中的公式一个性质。在React中,当我们通过setState改变了组件状态,那组件的UI就会自动发生变化,这就是Reactive Programming的体现。

如果我们通过直接修改this.state,然后调用一次setState,就像是改变了Excel表格里A1的值,然后还要再按一个按钮去改变A2的值……看起来怎么样?很不Reactive。

所以,要我说怎么看上面直接修改this.state.count的方法,就是:咱们都玩上React了,就不要再回到解放前了。

总结一下,看了上面三个“重新设计React”选择,似乎让setState同步更新组件状态不是个好的选择。

个人建议,大家别直接去操作this.state,一定要抵挡住这个诱惑,不然我会后悔把这个阴招透露出来。

实际上,我很好奇,什么样的实际需求希望setState能够同步修改this.state呢?我并没有遇到过这种需求,也许是我处理的情况还不够复杂,但是,我相信肯定有更好的对策。

如果你真的遇到具体场景想要setState同步更改状态,可以在评论中留言,我会帮大家想一想怎么样处理。

【更新】后续文章 setState何时同步 - 知乎专栏

编辑于 2017-03-29

文章被以下专栏收录