React菜鸟入门之setState

1,583 阅读6分钟

一、setState 这个磨人的小妖精!

作为一名入职前基本没有接触过React的小菜鸟,在接手的第一个练手项目中,很快就遇到了许多React初学者都会遇到的问题-setState。

let promiseArr = [];
this.setState({
    topStoryIds: res.data
})
for (let i = 0; i < 30; i++)
{
    promiseArr.push(axios.get('https://hacker-news.firebaseio.com/v0/item/' this.state.newStoryIds[i] .json?print=pretty'));
}

最初的设想是先通过this.setState设置state中的数据,然后再取用this.state。当然,结果不出所料的悲剧了。。苦思不得,不停的打断点,找原因,一度怀疑自己脑子瓦特了。。

image.png

就在我即将准备砸键盘的时候,突然想起来之前看文档的时候,好像提到过setState有个啥“异步更新”的东东,如一道闪电划过我的脑海。

二、setState是个啥

setState,是React官方推出的更新state的用法。通过调用setState,React能够知道state发生了变化,并调用render方法将变化展现到视图。

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

了解了基本概念及用法,我们来看一下setState的注意点:

  • setState通过引发一次组件的更新过程来引发重新绘制;
  • 多次setState函数调用产生的效果会合并;
  • setState不会立刻改变React组件中state的值。

setState通过引发一次组件的更新过程来引发重新绘制 setState调用引起的React的更新生命周期函数4个函数(比修改prop引发的生命周期少一个componentWillReceiveProps函数),这4个函数依次被调用。

  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

那么state何时被更新呢,我们用一个小栗子来探索一哈。

  handleClick = () => {
    this.setState({ count: this.state + 1, });
  }

  shouldComponentUpdate() {
    console.log('shouldComponentUpdate',this.state.count);
    return true;
  }

  componentWillUpdate() {
    console.log('componentWillUpdate',this.state.count);
  }

  render() {
    console.log('render',this.state.count);
  }

  componentDidUpdate() {
    console.log('componentDidUpdate',this.state.count);
  }

对应的控制台信息如下:

shouldComponentUpdate 0
componentWillUpdate 0
render [object Object]1
componentDidUpdate [object Object]1

由此可知,state一直到render函数执行的时候,才会被更新,在这之前,state一直保持为更新前的状态。(或者,当shouldComponentUpdate函数返回false,这时候更新过程就被中断了,render函数也不会被调用了,这时候React不会放弃掉对this.state的更新的,所以虽然不调用render,依然会更新this.state。)

多次setState函数调用产生的效果会合并 Talk is cheap, show me your code!

handleClick = () => {
    this.setState({ count: this.state.count + 1, });
    console.log('第一次加一', this.state.count);
    this.setState({ count: this.state.count + 1, });
    console.log('第二次加一', this.state.count);
}

render() {
  console.log('render加一', this.state.count);    
  return (
    <div className="App">
      <button onClick={this.handleClick}>count + 1</button>
    </div>
  );
}

对应的渲染得到的视图即一个button:

image.png
单击button后,控制台的输出结果如下:

第一次加一 0
第二次加一 0
render加一 [object Object]1

如上所示,我们在handleClick中进行了两次setState操作,但对应的render却只执行了一次,说明了React将两次setState合并为了一次,进行merge后统一更新。 其实想想也很容易理解,若每次setState都触发一次更新行为的话,那么将造成多么大的性能浪费。所以,从性能角度考虑,setState“多次setState函数调用产生的效果会合并”这一特性是合理而有必要的。 但是,相信善于观察的你一定会有一个疑问:为什么上例中render打印的this.state.count不是2,而是1呢?明明handleClick中进行了两次加一操作啊!这也是我最开始看到这个栗子时候的一个疑问。

三、setState的异步更新机制

setState不会立刻改变React组件中state的值 其实,问题的答案在前面已经给出了,即“state一直到render函数执行的时候,才会被更新”。 回到上面的栗子中去:

handleClick = () => {
    this.setState({ count: this.state.count + 1, });
    console.log('第一次加一', this.state.count);
    this.setState({ count: this.state.count + 1, });
    console.log('第二次加一', this.state.count);
}

表面上看,handleClick对this.state.count执行了两次加一操作,最终的this.state.count应该等于2,但是,因为React的异步更新机制,导致this.state.count直到render函数执行前依然未得到更新,即上面的两次this.state.count + 1操作是冗余,与下面的代码等价:

handleClick = () => {
    const count = this.state.count;
    this.setState({ count: count + 1, });
    console.log('第一次加一', this.state.count);
    this.setState({ count: count + 1, });
    console.log('第二次加一', this.state.count);
}

显而易见,这样的更新是不会得到预想的结果的。此时的this.state.count只是对state的一个“快照”,不管执行多少次加一操作,其最终结果都只相当于一次。

四、setState会同步更新吗?

setState函数存在的一个重要意义就是它可以驱动视图的更新。如果仅仅想要改变state,我们可以直接对this.state对象进行操作:

  handleClick = () => {
    this.state.count++;
    console.log('this.state.count: ', this.state.count);
  }

  render() {
    console.log('触发更新');
    ...   
  }

控制台输出:

this.state.count:  1

可以看到,state值确实进行了操作,但是render函数却没有得到执行。这样的更新操作是没有意义的。 既然如此,那么setState会进行同步更新吗?答案是肯定的。 在React中,如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。 在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state。 但是,setState的同步更新会导致严重的性能问题,我们在实际开发过程中应尽量避免使用。

五、耍个流氓?

既然直接操作this.state会导致无法触发re-render过程,而setState又具有异步更新这一让人又爱又恨的特性,我们何不二者合二为一?

  handleClick = () => {
    this.state.count++;
    this.state.count++;
    this.state.count++;
    console.log('this.state.count: ', this.state.count);
    this.setState({});
  }

  render() {
    console.log('触发更新了呢!');   
    console.log('render',this.state.count);
    ...
  }

控制台信息:

this.state.count:  3
触发更新了呢!
render 3

image.png
结果居然通过了!!!这样的操作,既避免了异步更新导致的同时多次更新state时的无效问题,又解决了直接操作this.state时不会触发更新的问题,岂不是两全其美? 这个问题仁者见仁,不同人眼中也许会有不同的答案。但我相信,React的作者设计这个框架的初衷肯定不在于此。

总结自setState:这个API设计到底怎么样 setState何时同步更新状态