一、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。当然,结果不出所料的悲剧了。。苦思不得,不停的打断点,找原因,一度怀疑自己脑子瓦特了。。
就在我即将准备砸键盘的时候,突然想起来之前看文档的时候,好像提到过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:
第一次加一 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
复制代码