一,useState的简易实现
请查看实现简易版useState
二,合并更新 batchedUpdates
2.1 函数组件useState
- 假设有下面这段代码,点击change按钮后会打印什么
import React, { useState } from "react";
import ReactDOM from "react-dom";
function App() {
const [count, setCount] = useState(0);
console.log("render", count);
const handleBtnClick = () => {
setCount(1);
console.log("第一次setCount后", count);
setCount(2);
console.log("第二次setCount后", count);
};
return (
<div className="App">
<button onClick={handleBtnClick}>change</button>
<span>count:{count}</span>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
- 这里两次打印的count都是更新前的值0,且只重新render了一次,因为react在此做了性能优化,对两次setCount更新做了合并;

- 下面修改下代码,把setCount放在定时器回调里面
import React, { useState } from "react";
import ReactDOM from "react-dom";
function App() {
const [count, setCount] = useState(0);
console.log("render", count);
const handleBtnClick = () => {
setTimeout(() => {
setCount(1);
console.log("第一次setCount后", count);
setCount(2);
console.log("第二次setCount后", count);
});
};
return (
<div className="App">
<button onClick={handleBtnClick}>change</button>
<span>count:{count}</span>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("root"));
- 这里是先打印了render 1再,而且是render了两次,react在此没有按预期做合并setState的操作;

- 上面两个不同的结果不是setState同步异步的问题,实际上setState代码都是同步执行的,而且每次执行setState都会调用scheduleUpdateOnFiber尝试触发render。但react把触发setState的回调函数使用了batchedUpdates包裹,这个函数内部会设置executionContext这个全局变量,他在scheduleUpdateOnFiber回调中将控制是否调用flushSyncCallbackQueue来触发render。如果setState放在了setTimeout回调里面,那么在fn(点击事件回调函数handleBtnClick)执行完成后将丢失executionContext上下文,所以此时每次setState都可以触发render,又因为闭包的原因,所以count两次都是打印0;
function batchedUpdates(fn, a) {
var prevExecutionContext = executionContext;
executionContext |= BatchedContext;
try {
return fn(a);
} finally {
executionContext = prevExecutionContext;
if (executionContext === NoContext &&
!( ReactCurrentActQueue$1.isBatchingLegacy)) {
resetRenderTimer();
flushSyncCallbacksOnlyInLegacyMode();
}
}
}
export function scheduleUpdateOnFiber() {
...
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
if (lane === SyncLane) {
if (
(executionContext & LegacyUnbatchedContext) !== NoContext &&
(executionContext & (RenderContext | CommitContext)) === NoContext
) {
schedulePendingInteractions(root, lane);
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
schedulePendingInteractions(root, lane);
if (executionContext === NoContext) {
resetRenderTimer();
flushSyncCallbackQueue();
}
}
...
}
- 这里如果想要达到期望的在setTimeout中也合并两次setState做批量更新的效果,可以手动调用unstable_batchedUpdates包裹一下回调,这个函数会帮我们加上executionContext上下文
const handleBtnClick = () => {
setTimeout(() => {
unstable_batchedUpdates(() => {
setCount(1);
console.log("第一次setCount后", count);
setCount(2);
console.log("第二次setCount后", count);
});
});
};

2.2 class组件this.setState
class App extends React.Component {
state = {
count: 0,
};
handleBtnClick = () => {
const { count } = this.state;
this.setState({
count: count + 1,
});
console.log('第一次setState后',count);
this.setState({
count: count + 1,
});
console.log('第二次setState后',count);
};
render() {
const { count } = this.state;
console.log("render", count);
return (
<div className="App">
<button onClick={this.handleBtnClick}>++</button>
<span>count:{count}</span>
</div>
);
}
}

class App extends React.Component {
state = {
count: 0,
};
handleBtnClick = () => {
const { count } = this.state;
setTimeout(() => {
this.setState({
count: count + 1,
});
console.log("第一次setState后", count);
this.setState({
count: count + 1,
});
console.log("第二次setState后", count);
});
};
render() {
const { count } = this.state;
console.log("render", count);
return (
<div className="App">
<button onClick={this.handleBtnClick}>++</button>
<span>count:{count}</span>
</div>
);
}
}

- 可以看到和函数组件的情况事一样的,区别在函数组件是调用dispatchAction触发更新,而class组件是调用this.updater.enqueueSetState触发更新;
3 React18的合并更新机制