关于 setState
setState 是异步的吗?
出于性能考虑,React 将 setState 进行异步处理,即调用 setState 时候不会马上更新,而是将 setState 的值放入一个队列,然后延时批量处理队列中的 state。在生命周期钩子和 React 合成事件中中调用 setState,会将数据添加到一个队列中,合并处理,这时候 setState 是异步的。而在其他的 React 控制之外的地方调用(比如原生 dom 事件)则不是异步的。
setState 异步特性,导致需要注意两个问题
- 设置完 state,获取数据的时机
- 由于异步特性,setState 改变 state 数据后,无法马上获取到 state 的最新值。
- 由于 setState 是异步的,如果想获取改变后的值,应该在 setState 的第二个参数回调函数中访问 state 的最新值。
- 修改数据依赖现有的数据
- 由于 setState 会异步合并处理,因此如果设置的 state 值依赖于之前的 state 值,需要给 setState 传一个函数, 函数参数是之前的 state,返回的值是新的改变的 state 属性。
import React from 'react';
class App extends React.Component {
state = {
digit: 1,
text: 'a'
};
componentDidMount() {
document
.getElementById('btn')
.addEventListener('click', this.onAddButtonClick);
}
onAddButtonClick = () => {
// bad
// const add1 = () => {
// this.setState({digit: this.state.digit + 1});
// };
// add1();
// add1();
// add1();
// good
const add1 = () => {
this.setState(state => {
return { digit: state.digit + 1 };
});
};
add1();
add1();
add1();
};
onAddButtonClick2 = () => {
// work
const add1 = () => {
this.setState({ digit: this.state.digit + 1 });
};
add1();
add1();
add1();
};
onChangeButtonClick = () => {
// bad
// this.setState({text: 'b'});
// console.log(this.state.text);
// good
this.setState({ text: 'b' }, () => {
console.log(this.state.text);
});
};
render() {
return (
<div>
<div>{this.state.digit}</div>
<button onClick={this.onAddButtonClick}>+3</button>
<button id="btn">+3(原生事件)</button>
<hr />
<div>{this.state.text}</div>
<button onClick={this.onChangeButtonClick}>修改文本</button>
</div>
);
}
}
export default App;React 更新界面的主要过程可以简单描述为,调用 setState 之后,React 会更新 state,然后调用组件的 render 得到新的 state 对应的虚拟 dom,然后对比当前的和更新后端虚拟 dom,判断是否需要更新 dom,以及如何更新 dom。
import React from 'react';
import './App.css';
class App extends React.Component {
state = {
digit: 1
};
componentDidMount() {
// 即使digit没有变,也还是会触发对比,调用render方法
this.setState({ digit: 1 });
}
render() {
console.log('render');
return <div>{this.state.digit}</div>;
}
}
export default App;由于 diff 工作量比较大,如果 setState 的值没有改变,其实是不需要进行 diff 的。如何让 state 没有改变的时候,不进行 diff 呢?可以使用 shouldComponentUpdate 这个生命周期钩子。
在调用 setState 更新数据后,React 会判断是否需要进行更新操作,如果没有 shouldComponentUpdate 这个生命周期钩子,则默认进行对比和更新工作,如果有 shouldComponentUpdate,则调用之,如果返回 true 则更新,返回 false 则不更新。利用这个钩子,我们可以对新的 state 和当前的 state 进行对比,如果有变化,返回 true,如果没有变化,返回 false。
React.PureComponent 实现了浅比较的 shouldComponentUpdate,因此我们的组件如果继承了 React.PureComponent 就会有了对比 state 决定是否更新的特性。
import React from 'react';
import './App.css';
class App extends React.PureComponent {
state = {
digit: 1
};
componentDidMount() {
// 继承PureComponent,当数据未改变时候,不会触发对比更新
this.setState({ digit: 1 });
}
render() {
console.log('render');
return <div>{this.state.digit}</div>;
}
}
export default App;但是使用 React.PureComponent 时候需要注意一个问题,浅比较是比较值是否相同,因此当 state 中的数据是一个对象,其中属性变化但引用不变、或者 state 中的数据是一个数组,数组中的元素变化但数组引用不变,这时候比较结果是两者相同,因此不会触发更新。如果希望在对象属性变化、数组元素变化时候触发更新,应该 setState 时候传入新的对象或数组。
import React from 'react';
import './App.css';
class App extends React.PureComponent {
state = {
obj: {
text: 'obj'
},
arr: [1, 2]
};
componentDidMount() {
this.state.obj.text = 'obj1';
this.state.arr.push(3);
// obj和arr引用未变,因此不会触发更新
// this.setState({
// obj: this.state.obj,
// arr: this.state.arr
// });
// obj和arr复制为新的对象/数组,因此会触发更新
this.setState({
obj: { ...this.state.obj },
arr: [...this.state.arr]
});
}
render() {
console.log('render');
return (
<div>
{this.state.obj.text}
<br />
{this.state.arr.join(',')}
</div>
);
}
}
export default App;通常情况下,我们应该让组件继承 React.PureComponent,并且对象和数组变化时候要传入新的引用。这样既可以利用 shouldComponentUpdate 的浅比较避免不必要的对比更新操作,也能保证在需要的时候更新视图。