优先级插队
demo
function SlowPost({ data }) {
let startTime = performance.now();
while (performance.now() - startTime < 1) {
// Do nothing for 1 ms per item to emulate extremely slow code
}
return (
<li className="item">
Post #{data}
</li>
);
}
export default function Index(){
const [ number , setNumber ] = useState(0)
const [isPending, startTransition] = useTransition();
const buttonRef = useRef()
const handleConcurrentClick = () => {
const button = buttonRef.current
startTransition(() => {
setNumber((num) => num + 1)
})
setTimeout(() => {
button.click()
}, 2000)
}
const handleClick = () => {
setNumber((num) => num + 2)
}
let items = [];
for (let i = 0; i < 500; i++) {
items.push(<SlowPost key={i} data={number} />);
}
console.log('----组件渲染----')
return <div>
<button onClick={handleConcurrentClick}>触发更新</button>
<button ref={buttonRef} onClick={handleClick}>优先级更新</button>
<ul className="items">
{items}
</ul>
</div>
}
注意一个点,如果是
setTimeout(() => {
setCount((count) => count + 2)
}, 500)
这样的setState,虽然他的优先级是32,但是在getNextLanes计算优先级的时候,32比不过256
比如有两个任务优先级是256和32,组合起来就是288
function getNextLanes(root, wipLanes) {
// Early bailout if there's no pending work left.
var pendingLanes = root.pendingLanes;
if (pendingLanes === NoLanes) {
return NoLanes;
}
var nextLanes = NoLanes;
var suspendedLanes = root.suspendedLanes;
var pingedLanes = root.pingedLanes; // Do not work on any idle work until all the non-idle work has finished,
// even if the work is suspended.
var nonIdlePendingLanes = pendingLanes & NonIdleLanes;
if (nonIdlePendingLanes !== NoLanes) {
var nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
if (nonIdleUnblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
} else {
var nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
if (nonIdlePingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
}
}
} else {
// The only remaining work is Idle.
var unblockedLanes = pendingLanes & ~suspendedLanes;
if (unblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(unblockedLanes);
} else {
if (pingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(pingedLanes);
}
}
}
if (nextLanes === NoLanes) {
// This should only be reachable if we're suspended
// TODO: Consider warning in this path if a fallback timer is not scheduled.
return NoLanes;
} // If we're already in the middle of a render, switching lanes will interrupt
// it and we'll lose our progress. We should only do this if the new lanes are
// higher priority.
if (wipLanes !== NoLanes && wipLanes !== nextLanes && // If we already suspended with a delay, then interrupting is fine. Don't
// bother waiting until the root is complete.
(wipLanes & suspendedLanes) === NoLanes) {
var nextLane = getHighestPriorityLane(nextLanes);
var wipLane = getHighestPriorityLane(wipLanes);
if ( // Tests whether the next lane is equal or lower priority than the wip
// one. This works because the bits decrease in priority as you go left.
nextLane >= wipLane || // Default priority updates should not interrupt transition updates. The
// only difference between default updates and transition updates is that
// default updates do not support refresh transitions.
nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes) {
// Keep working on the existing in-progress tree. Do not interrupt.
return wipLanes;
}
}
return nextLanes;
}
nextLane计算出来是32,wipLanes是256,明明32更小但是返回的是256,这个问题困扰了我好久,还以为react18版本优先插队失效了呢。
低优先级任务执行
回到这个demo,首先处理startTransition,然后注册了一个2s的SetState回调,2s为了让取消任务更加清楚,去掉startTransition大概1s的耗时,scheduleTask会执行接近结束但是因为有更高优先级插入而取消。
startTransition会触发一次1s多点的同步render,然后进行优先级为256的scheduleTask
之后就按照schedule的调度进行fiber render
在这个过程中,root挂载的callbackNode一直都是优先级为256的scheduleTask
在第一次同步render的过程中,此时lane = 2,但是number的SetState的lane为256,所以第一次render只会进行isPending的更新。
优先级不够的更新会先clone这个update,然后放入hook.baseQueue
等轮到256的render
hook.memoizedState = newState; // 1
hook.baseState = newBaseState; // 1
hook.baseQueue = newBaseQueueLast; // null
queue.lastRenderedState = newState; // 1
注意⚠️
此时重新渲染Index, 此时workInProgress已经指向clone出来的fiber,hook也会从alternate clone一份出来,数据保存在new hook上,new hook绑定在 clone fiber之上,因此这个clone fiber的baseState是1,那么在被打断后,这个baseState是如何恢复初始值0的呢?
同步任务插入
等到2s后setTimtout触发回调,产生了优先级为2的同步任务
if (includesSyncLane(nextLanes)) {
// Synchronous work is always flushed at the end of the microtask, so we
// don't need to schedule an additional task.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackPriority = SyncLane;
root.callbackNode = null;
return SyncLane;
}
接着会取消这个callbackNode,把它的callback = null
这里是同步的插队,如果是异步的插队,注意优先级不能为32
接着走同步渲染的逻辑
此时hook.queue多了lane = 2的任务
注意⚠️
clone fiber 并不会打乱current节点的指向
之前sheduleTask构建的workInProgress已经过时了,这些数据保留在alternate(workInProgress就是从alternate来的)
此时alternate的baseState = 1
但是render还是以current clone当成workInProgress,那岂不是baseState又是以1为初始值了?
workInProgress.flags = current.flags & StaticMask;
workInProgress.childLanes = current.childLanes;
workInProgress.lanes = current.lanes;
workInProgress.child = current.child;
workInProgress.memoizedProps = current.memoizedProps;
workInProgress.memoizedState = current.memoizedState;
workInProgress.updateQueue = current.updateQueue; // Clone the dependencies object. This is mutated during the render phase, so
// it cannot be shared with the current fiber.
并不是,clone的时候会将current的hook给复制过来,所以baseState初始值还是0
由于new hook的时候,hook.queue和baseQueue没有进行深拷贝,所以alternate 和current共用一个queue和baseQueue,这个queue.lastRenderedState = 1不会清空,baseQueue也保留着 scheduleTask的(num)=>num + 1,后面会和(num)=>num + 2一起遍历
由于此时进行lane = 2的更新,(num)=>num + 1的lane是256,并不会执行,只会进行(num)=>num + 2的更新,于是就先渲染出dom num = 2的样子
等commitRoot的时候触发ensureRootIsScheduled再次渲染,重新执行lane=256的
scheduleTask