在React中,如果涉及到了多次setState,组件render几次。setState是同步的还是异步的。这是一个很常见的面试题。
而本篇文章,就是主要实现React中,对于这部分的性能优化,我们称之为批处理。例如当我有下面的JSX。
const root = document.querySelector('#root');
function App() {
const [num, setNum] = useState(0)
const click1 = () => {
setNum(num + 1)
setNum(num + 2)
setNum(num + 3)
}
return jsx("div", {
onClick: click1,
children: num
});
}
ReactDOM.createRoot(root).render(<App />)
对于当前的点击事件来说,只有最后的setNum(num + 3)是有效的。
但是在我们之前的实现中,对于这种连续三次setState,我们的代码就要处理三次,就要经过三次beginWork,completeWork,commitWork。
那我们能不能实现出一种方式,对于这种多次setState,最终之做最后一次处理。
目前,在我们的代码里,能够让组件更新的方式,就是通过setState,触发更新。而updateQueue就是用来保存更新的内容。
function enqueueUpdate(updateQueue, update) {
updateQueue.shared.pending = update
}
之前,在enqueueUpdate方法里,我们是直接进行赋值的。这没什么问题,因为之前每次赋值之后都会更新一下。
但是现在我们希望,最后只更新一次的话。我们就需要一个数据结构,可以保存多次更新的内容。这里使用的是链表结构,但在真正的React源码中,使用的是环形链表。
function enqueueUpdate(updateQueue, update) {
// updateQueue.shared.pending = update
let pending = updateQueue.shared.pending;
if(pending === null) {
updateQueue.shared.pending = update
}else{
while(pending.next != null) {
pending = pending.next;
}
pending.next = update
pending = pending.next;
}
}
OK,修改完之后,我们调用了三次setState,那么updateQueue中保存的应该是一个链表了。
在之前的processUpdateQueue方法里,也是直接更新就完了。但是现在updateQueue的结构发生了变化,所以对于processUpdateQueue,更新逻辑也要改变。
function processUpdateQueue(baseState, pendingUpdate) {
const result = {
memoizedState: baseState
}
if(pendingUpdate.next === undefined) {
const action = pendingUpdate.action;
//setState(() => {}) 传入方法
if(typeof action === 'function'){
result.memoizedState = action(baseState);
}else {
//setState()
result.memoizedState = action;
}
return result
}
while(pendingUpdate != null) {
const action = pendingUpdate.action;
//setState(() => {}) 传入方法
if(typeof action === 'function'){
baseState = action(baseState);
}else {
//setState()
baseState = action;
}
pendingUpdate = pendingUpdate.next;
}
result.memoizedState = baseState;
return result;
}
我们需要遍历pending,将所有的更新内容返回。
我们想一下,之前触发更新后,执行的机制是什么样子的。
也就是说,每次workLoop都只能拿到当前更新的内容。
如果我希望在第一次workLoop就可以拿到所有的更新内容,并且取消后面的workLoop。
有什么方法呢?微任务!!!!
我们可以将执行workLoop的过程放在微任务里,这样执行workLoop的时候,updateQueue的链表已经生成。
同时我们用一个标志位,取消后面的workLoop执行。
当workLoop执行完成后,再将标志位置反。
let isFinished = false;
export function syncWorkLoop(root,hostRootFilber) {
if(isFinished) {
return;
}
Promise.resolve(null).then(() => {
wookLoop(root,hostRootFilber);
})
isFinished = true;`在这里插入代码片`
}
const wookLoop = (root,hostRootFilber) => {
//其他代码。。。。。
isFinished = false;
}
最后,我们只要在filberHook中触发的逻辑里,替换workLoop即可:
function disaptchState(filber, hook, action) {
const update = createUpdate(action);
enqueueUpdate(hook.updateQueue, update);
workUpdateHook = hook;
syncWorkLoop(filber.return.stateNode);
}
通过这种方式,当我执行多次setState,最终只会render一次。