setState 是一个异步函数,具体表现在两个方面,在一个组件中连续执行 setState 方法,会发生合并;在一个组件树中,多个组件连续执行 setState 方法,多个组件的 setState 方法会发生合并。setState 方法的执行会引起组件 render 函数的执行,判断 setState 方法是否发生合并的依据是:组件中 render 函数执行的次数是否能和 setState 方法执行的次数对应上。
1. 组件中 setState 方法的合并
直接上代码:
import React, { Fragment, Component } from 'react';
import ReactDOM from 'react-dom';
class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('click happened');
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
}
render() {
console.log('App render');
const { count } = this.state;
return (
<Fragment>
<div>{ count }</div>
<button onClick={ this.handleClick }>Click</button>
</Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
代码思路很简单,点击组件的按钮,执行两次组件的 setState 方法,观察组件 render 函数的执行次数,结果如下图:
上面 demo 中 setState 方法的参数是一个对象,setState 方法的参数还可以是一个函数,setState 方法的参数如果是一个函数,在发生合并时,与参数是对象是有差异的,直接上代码与结果:
import React, { Fragment, Component } from 'react';
import ReactDOM from 'react-dom';
class App extends Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
console.log('click happened');
this.setState((state) => ({ count: state.count + 1 }));
this.setState((state) => ({ count: state.count + 1 }));
}
render() {
console.log('App render');
const { count } = this.state;
return (
<Fragment>
<div>{ count }</div>
<button onClick={ this.handleClick }>Click</button>
</Fragment>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
2. 多个组件 setState 方法的合并
直接上代码:
import React, { Fragment, Component } from 'react';
import ReactDOM from 'react-dom';
const setStateArr = [];
class App extends Component {
constructor(props) {
super(props);
this.state = {
appCount: 0,
};
this.setStateHappen = this.setStateHappen.bind(this);
setStateArr.push(this.setStateHappen);
this.handleClick = this.handleClick.bind(this);
}
setStateHappen() {
console.log('App setState happend');
this.setState({ appCount: 1 });
}
handleClick() {
console.log('App click happend');
setStateArr.forEach((setStateFunc) => {
setStateFunc();
});
}
render() {
console.log('App render');
const { appCount } = this.state;
return (
<Fragment>
<div>appCount: { appCount }</div>
<Children />
<button onClick={ this.handleClick }>App Click</button>
</Fragment>
);
}
}
class Children extends Component {
constructor(props) {
super(props);
this.state = {
childrenCount: 0,
};
this.setStateHappen = this.setStateHappen.bind(this);
setStateArr.push(this.setStateHappen);
}
setStateHappen() {
console.log('Children setState happend');
this.setState({ childrenCount: 1 });
}
render() {
console.log('Children render');
const { childrenCount } = this.state;
return (
<div>childrenCount: { childrenCount }</div>
);
}
}
ReactDOM.render(<App />, document.getElementById('root'));
代码的思路很简单,点击父组件的按钮时,连续执行父组件和子组件的 setState 方法,观察父组件和子组件 render 函数的执行次数。在我以前的理解中,setState 方法会引起组件 render 函数的执行,我预想的结果是,当我点击按钮时,执行父组件的 setState 方法,父组件 setState 方法的执行引起父组件 render 函数的执行,父组件 render 函数的执行引起子组件 render 函数的执行,执行子组件 setState 方法,子组件的 setState 方法引起子组件 render 函数的执行,所以,我预想的结果是,子组件的 render 函数会执行两次。实际结果如下图:
另,setState 设计成异步的,主要是从性能方面考虑,避免过多的 render 函数执行,虚拟 DOM 的比较。