20

在学习或使用过一阵子React后,你可能会发现一个在setState方法的特性,以下面这个简单例子来说明:

export default class SelectBox extends React.Component {
  constructor(props) {
    super(props)
    this.state={value: ''}
  }

  handleChange = (e) => {
    this.setState({value: e.target.value})
    console.log(this.state.value)
  }

  render() {
      return(
         <div>
             <select onChange={this.handleChange} value={this.state.value}>
                <option value="JavaScript" key={1}>JavaScript</option>
                <option value="Angular2" key={2}>Angular2</option>
                <option value="React" key={3}>React</option>
             </select>
             <h1>{this.state.value}</h1>
         </div>
      )
   }
}

我们在handleChange方法中,呼叫setState来更新选项的值,然后在控制台中输出这个值。看起来一切都是很符合逻辑,但你如果一执行就会发现,在控制台中输出的this.state.value,并不会在呼叫setState方法后立即就变动。像下面的执行的结果图一样:

执行结果

当然,如果你直接输出的是e.target.value,一定是正确的值,但在某些情况下,我们要取用的并不是这个事件的值,而是要更动过后的state(状态)值。

如果要在setState方法后,直接取用更动后的state值,正确的使用方式,在官方文件中的说明,需要利用setState的第二传参,传入一个回调(callback)函式,改为像下面这样的代码:

this.setState({value: e.target.value}, function(){ console.log(this.state.value) })

另一个方式则是用componentDidUpdate()这个生命周期方法,把确定state更新后要执行的代码放在里面,如下面的代码:

componentDidUpdate(){
  console.log(this.state.value)
}

为什么一定要这样作的主要原因是:

setState这个方法,它在React中的执行行为可以认为"异步的"

虽然setState并非使用了setTimeout或promise的那种进入到事件回圈(Event loop)的异步执行,但它的执行行为在React库中时,的确是异步的,也就是有延时执行的行为。以官方文件中较精确的说法 - "它不是保证同步的"。

setState方法与包含在其中的执行是一个很复杂的过程,这段程式码从React最初的版本到现在,也有无数次的修改。它的工作除了要更动this.state之外,还要负责触发重新渲染(render),这里面要经过React核心中diff演算法,最终才能决定是否要进行重渲染,以及如何渲染。而且为了批次与效能的理由,多个setState呼叫有可能在执行过程中还需要被合并,所以它被设计以异步的或延时的来进行执行是相当合理的。

那么setState会在何时以同步的方式来执行,也就是立即更动this.state?答案是在React库控制之外时,它就会以同步的方式来执行,在下面两篇文章中,都有类似的例子:

但大部份的使用情况下,我们都是使用了React库中的表单组件,例如select、input、button等等,它们都是React库中人造的组件与事件,是处于React库的控制之下,在这个情况下,setState就会以异步的方式执行。所以一般来说,我们会认为setState就是异步执行,并不是用原始码来看它是不是有使用像setTimeoutPromise之类的方式转为JavaScript的异步执行方式,而是以它在React库的控制之下,以执行行为与顺序来认定。

以下是翻自官方setState原代码的注解,官网的说明也是类似:

不保证this.state会立即更新,所以在调用这个方法后存取this.state可能会回传旧的值。

不保证呼叫setState就会同步地执行,而它们也可能最终被被批量调用(多次呼叫的情况下)。你可以提供额外的回调(callback),回调(callback)将会在setState实际被完成时被执行。

因此,很早就有开发者提出来关于setState常令初学者感到怪异的执行情况,在某些情况下会造成执行后会看到不连续的结果。除了setState方法有异步执行的行为外,它还有几个被提出来的特殊行为:

1. setState可能会引发不必要的渲染(renders)

state本身的设计是无法直接更改,setState的设计是用来更动state值,也会触发重新渲染(re-render),按照逻辑就是反正不管如何,只要开发者呼叫setState,React就去作整个视图的重新渲染就是。所以setState必定会作重新渲染的执行,只是要如何渲染是由React来决定。

重新渲染(re-render)指的主要是页面上视图(View)的重新再呈现,这是React原本的核心设计,但这个设计是有一些问题的。最主要的是state(状态)并不一定单纯只用来记录与视图(View)有关的状态,也有可能是某个内部控制用的属性值,或是只套用在内部使用的资料。当你改变了这些与视图无关的state(状态)值,以现在的React设计来说,照样要触发重新渲染的执行过程,这在某些复杂的应用时,由于造成不必要的渲染,也有可能造成效能上的问题。

当然,React提供了shouldComponentUpdate方法让开发者可以自行判断,自行提供对应的解决方式。也有Performance Tools可以进行剖析检测。算得上是一些补强的作法。

2. setState无法完全掌控应用中所有组件的状态

state(状态)是独立于每个组件内部的,而且它是个不能直接更动的对象,这个设计当然是为了要保持组件的封装与独立性,但所以如果当要开发一个复杂的应用时,必定需要使用那些能掌控所有组件资料,以及能提供各组件间资料互动的函式库,例如Flux, Redux或MobX等等。

React组件目前只能透过各种生命周期的方法,与外部资源、计时器或DOM事件来进行挂勾(Hook),这些都无法直接使用setState方法来进行管理,因此setState并没有办法完全掌控一个应用中所有组件的状态,它比较像是每个组件中的都有的一种接口方法,单纯要依靠setState方法来管控整个React应用,完全是不足够的。

以上说明参考自这篇文章: 3 Reasons why I stopped using React.setState


eyesofkids
3.7k 声望130 粉丝

勤勉的React/JS开发者。