- setState 是同步更新还是异步更新?
- 多次调用 setState 函数,React 会不会进行合并操作?
首先是第一个问题,答:setState
有时是同步更新的,而有时却是异步更新。
一般情况下,setState
基本是异步更新,例如:
// handleClick 是一个事件函数
// 当点击按钮时,count 就会 1
handleClick () {
this.setState({
count: this.state.count 1
});
// 每次点击,打印的结果总是上一次的值
console.log(this.state.count);
}
下面的例子是 setState
同步更新的例子:
clickUpdateCount () {
this.setState({
count: this.state.count 1
});
}
handleClick () {
setTimeout(() => {
this.clickUpdateCount();
// 打印结果与 count 一致
console.log(this.state.count);
},0);
}
把 setState
放在定时器里就会同步更新。放在自定义事件函数里也会同步更新,例如:
constructor () {
super()
this.state = {
count: 0
}
this.clickUpdateCount = this.clickUpdateCount.bind(this);
}
clickUpdateCount () {
this.setState({
count: this.state.count 1
}); // 直接打印,结果与 count 一致
console.log(this.state.count);
}
componentDidMount(){
document.addEventListener('click', this.clickUpdateCount, false);
}
componentWillUnmount(){
document.removeEventListener('click', this.clickUpdateCount, false);
}
如果不使用定时器或者添加自定义函数获得更新后的状态,可以给 setState
函数传入第二个参数,该参数是一个函数,它会在 state 更新完成后调用。
this.setState({
count: this.state.count 1
}, () => {
console.log(this.state.count);
});
更新合并
一般情况下,多次调用 setState
函数 React 会把 state 对象合并到当前的 state。例如:
clickUpdateCount () {
this.setState({
count: this.state.count 1
});
this.setState({
count: this.state.count 1
});
this.setState({
count: this.state.count 1
});
}
虽然多次调用了 setState
,但是点击按钮时 count 只增加 1。如果不想合并操作,可以把这些操作放在定时器当中,或者自定义事件当中。或者给 setState
的第一个参数传入函数,例如:
clickUpdateCount () {
// prevState 是更新前的 state,props 是父组件传来的属性
this.setState(function(prevState, props) {
return { // 返回更新后的 state
count: prevState.count 1
}
});
this.setState(function(prevState, props) {
return {
count: prevState.count 1
}
});
this.setState(function(prevState, props) {
return {
count: prevState.count 1
}
});
}
下图是 setState
调用时的大致流程。
图中如果条件是 true,则组件会异步更新,而如果是 false
,则会同步更新。是否处于 batchUpdate,可以用一个例子来解释,比如下面的代码:
class A extends React.Component{
state = {
count: 0
}
render(){
return <div></div>
}
increment(){
// 处于 batchUpdate
// isBatchingUpdates = true
this.setState({
count: this.count 1
});
// isBatchingUpdates = false
}
}
class B extends React.Component{
state = {
count: 0
}
render(){
return <div></div>
}
increment(){
// 处于 batchUpdate
// isBatchingUpdates = true
setTimeout(() => {
this.setState({
count: this.count 1
});
},0);
// isBatchingUpdates = false
}
}
isBatchingUpdates
的初始值是 true
,当没有定时器时调用 setState
时该值还是 true,就会异步执行,而 setState
用定时器包裹后,定时器回调还没执行 isBatchingUpdates
就变成了 false
,setState 就会同步执行。
props 或者 state 变化时,会调用 UpdateComponent
更新 props 和 state,然后调用 render 函数,生成新的虚拟节点(Vnode),然后 patch(oldVnode, newVnode),老的虚拟 DOM 和新的虚拟 DOM 对比更新视图。patch
分为两个阶段:
- reconciliation 阶段:执行 diff 算法(纯 JS 计算);
- commit 阶段:将 diff 结果渲染到 DOM 上。
由于 JavaScript 是单线程,且和 DOM 渲染共用一个线程,当组件足够复杂时,组件更新时计算量和渲染量都很大,同时再有 DOM 操作需求(比如动画、拖拽等),这可能会导致页面卡顿。
React 考虑性能优化,就把 patch 分成了两个阶段,在 reconciliation 阶段将任务拆分,拆分成多个子任务(commit 不能拆分,reconciliation 阶段是纯 JS 计算,比较好控制),当 DOM 渲染时就暂停任务,浏览器空闲时再恢复计算。通过 window.requestIdleCallback API 可以让一些任务在浏览器空闲时调用。
关于 React fiber 和 Concurrent API 可以参考这篇文章:深入剖析 React Concurrent
setState 与 useState
setState 与 useState 功能相似,在一般情况下,useState
也会对多次调用更新函数做合并处理,例如:
let [count, setCount] = useState(0);
// 点击后只会相加一次
let handleClick = useCallback(() => {
setCount(count 1);
setCount(count 1);
},[count]);
如果 useState
更新 state 传入的也是函数时,就不会对数组做合并处理,这与 setState 行为一样。
let [count, setCount] = useState(0);
let handleClick = useCallback(() => {
// 每次点击按钮会增加 2
setCount((prevState) => {
return prevState;
});
setCount((prevState) => {
return prevState;
});
},[]);
useState
与 setState
不同的是:state
是对象时,setState
会自动合并对象,而 useState
不会。例如:
state = {
count: 0,
color: 'red'
};
handleClick(){
this.setState({
// 只更新了 count
count: this.state.count 1
});
}
render () {
return (
<div>
<h1>count: { this.state.count }</h1>
<h2>color: { this.state.color }</h2>
<button onClick={() => this.handleClick()}>Click 1</button>
</div>
);
}
运行时会发现,color
并不会丢失。而如果使用 useState
,只更新 count
,当点击按钮一次之后 color
就会丢失,例如:
let handleClick = useCallback(() => {
setData({
count: data.count 1
});
},[data.count]);
如果不让 color
丢失,可以使用这种方式:
setData({
...data, // 把老的对象也传进来
count: data.count 1 // 覆盖老的值
});
setState
可以同步更新,比如在外层包裹定时器,传入第二个回调参数可以拿到更新后的数据。但 useState
是行不通的,它是异步更新,要想及时拿到更新后的数据,就需要借助 useEffect
。
useEffect(() => {
console.log(data.count);
},[data.count]);
useEffect
在首次渲染完成后执行一次,之后会在 data.count
的值更新后执行回调函数。