react学习系列——优先级插队

176 阅读4分钟

优先级插队

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

53aa56c7b5ede287f01f58635170cbf4.png

在这个过程中,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

接着走同步渲染的逻辑

887646f3735ff039b7d456a4929d1597.png

此时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