在学习或使用过一阵子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
就是异步执行,并不是用原始码来看它是不是有使用像setTimeout
或Promise
之类的方式转为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应用,完全是不足够的。