likes
comments
collection
share

剖析React系列十三-react调度

作者站长头像
站长
· 阅读数 27

本系列是讲述从0开始实现一个react18的基本版本。通过实现一个基本版本,让大家深入了解React内部机制。 由于React源码通过Mono-repo 管理仓库,我们也是用pnpm提供的workspaces来管理我们的代码仓库,打包我们使用rollup进行打包。

仓库地址

本节对应的代码

系列文章:

  1. React实现系列一 - jsx
  2. 剖析React系列二-reconciler
  3. 剖析React系列三-打标记
  4. 剖析React系列四-commit
  5. 剖析React系列五-update流程
  6. 剖析React系列六-dispatch update流程
  7. 剖析React系列七-事件系统
  8. 剖析React系列八-同级节点diff
  9. 剖析React系列九-Fragment的部分实现逻辑
  10. 剖析React系列十- 调度<合并更新、优先级>
  11. 剖析React系列十一- useEffect的实现原理
  12. 剖析React系列十二-调度器的实现

上一讲中,我们讲解了关于scheduler包的源码相关的知识点,还通过一个例子讲解了高优先级分片执行,或者高优先级打断低优先级的情况。这一节我们主要是结合react的代码,讲解scheduler是如何和react相结合的。

开始之前我们首先要明白一个基本知识,在react中,render阶段是通过调度可以中断的,但是commit阶段是不可以中断的。

所以我们接下来讲解的调度都是用来中断render(调和)阶段。

调度入口scheduleUpdateOnFiber

不管是首页渲染,还是我们通过useState触发的更新操作,react内部都会走到scheduleUpdateOnFiber中,开始调度执行。

scheduleUpdateOnFiber中主要功能是更新可能从不同的fiberNode中触发。但是react的每次更新都是从根节点开始。所以scheduleUpdateOnFiber就是向上遍历到根节点。

export function scheduleUpdateOnFiber(fiber: FiberNode, lane: Lane) {
  // fiberRootNode
  let root = markUpdateFromFiberToRoot(fiber);
  markRootUpdated(root, lane);
  ensureRootIsScheduled(root);
}

开始调度ensureRootIsScheduled

我们真正的调度开始逻辑是在ensureRootIsScheduled中,它会比较不同的优先级,然后实现不同的逻辑。

主要逻辑分为这几个步骤:

  1. 获取当前任务的最高优先级
  2. 对比上一个优先级和此次优先级,如果相同就退出执行。如果不同就证明此次比上一次高,取消上一次正在执行的任务
  3. 判断是任务的优先级是不是同步执行的任务,如果是同步执行的话,就通过微任务批量执行。如果不是的话,就通过宏任务开启调度。

它主要是使用了我们上一讲scheduler中的如下方法:

  • unstable_cancelCallback: 取消正在执行的任务
  • scheduleCallback: 调度器的开始工作
/**
 * schedule调度阶段入口
 */
function ensureRootIsScheduled(root: FiberRootNode) {
  let updateLane = getHighestPriorityLane(root.pendingLanes);
  // 获取当前的callback
  const existingCallback = root.callbackNode;

  if (updateLane === NoLane) {
    if (existingCallback !== null) {
      unstable_cancelCallback(existingCallback);
    }
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
  }

  const curPriority = updateLane;
  const prevPriority = root.callbackPriority;
  if (curPriority === prevPriority) {
    // 如果之前的优先级等于当前的优先级, 不需要重新的调度,scheduler会自动的获取performConcurrentWorkOnRoot的返回函数继续调度
    // (return performConcurrentWorkOnRoot.bind(null, root); 中继续调度)
    return;
  }

  // 当前产生了更高优先级调度,取消之前的调度
  if (existingCallback !== null) {
    unstable_cancelCallback(existingCallback);
  }

  let newCallbackNode = null;

  if (updateLane === SyncLane) {
    // 同步优先级  用微任务调度
    scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root, updateLane));
    scheduleMicroTask(flushSyncCallbacks);
  } else {
    // 其他优先级 用宏任务调度
    // 将react-lane 转换成 调度器的优先级
    const schedulerPriority = lanesToSchedulerPriority(updateLane);
    newCallbackNode = scheduleCallback(
      schedulerPriority,
      // @ts-ignore
      performConcurrentWorkOnRoot.bind(null, root)
    );
  }
  // 保存当前的调度任务以及调度任务的优先级
  root.callbackNode = newCallbackNode;
  root.callbackPriority = curPriority;
}

我们接下来看看performConcurrentWorkOnRoot内部执行的逻辑。

调度performConcurrentWorkOnRoot

performConcurrentWorkOnRootscheduler真正执行的函数。也是react包和scheduler包相交的入口,是时间分片的关键地方。

它接受一个scheduler传递的任务是否超时的参数didTimeout,如果任务超时,就同步执行超时的任务。可以用户解决饥饿问题(一个任务一直没有被执行)。

主要逻辑是:

  1. 根据任务的优先级和是否超时,判断是否同步执行needSync
  2. 调用render阶段真正的调和函数renderRoot
  3. 重新执行调度ensureRootIsScheduled,用于并发更新中断后,继续调度。(上一讲中调度函数返回一个函数继续执行)
  4. 如果调和完成,开始进入commit阶段
/**
 * 并发更新的render入口 -> scheduler时间切片执行的函数
 * @didTimeout: 调度器传入 -> 任务是否过期
 */
function performConcurrentWorkOnRoot(
  root: FiberRootNode,
  didTimeout: boolean
): any {
  // 并发开始的时候,需要保证useEffect回调已经执行
  // 因为useEffect的执行会触发更新,可能产生更高优先级的更新。
  // function App() {
  //   useEffect(() => {
  //      updatexxx() // 如果触发了更高级别的更新
  //   }, [])
  // }
  const curCallback = root.callbackNode;
  let didFlushPassiveEffect = flushPassiveEffects(root.pendingPassiveEffects);
  if (didFlushPassiveEffect) {
    // 这里表示:useEffect执行,触发了更新,并产生了比当前的更新优先级更高的更新,取消本次的调度
    if (root.callbackNode !== curCallback) {
      return null;
    }
  }

  const lane = getHighestPriorityLane(root.pendingLanes);
  const curCallbackNode = root.callbackNode;
  // 防御性编程
  if (lane === NoLane) {
    return null;
  }

  const needSync = lane === SyncLane || didTimeout;

  // render阶段
  const exitStatus = renderRoot(root, lane, !needSync);

  // 再次执行调度,用于判断之后root.callbackNode === curCallbackNode,
  // 因为如果并发过程中,优先级没有变,在执行调度后,由于curPriority === prevPriority,直接返回,导致curCallbackNode相等,继续调度
  // 如果有更高优先级的调度的话,本次调度直接返回null,停止调度
  ensureRootIsScheduled(root);

  // 中断
  if (exitStatus === RootInComplete) {
    // ensureRootIsScheduled中有更高的优先级插入进来, 停止之前的调度
    if (root.callbackNode !== curCallbackNode) {
      return null;
    }
    // 继续调度
    return performConcurrentWorkOnRoot.bind(null, root);
  }

  // 已经更新完
  if (exitStatus === RootCompleted) {
    const finishedWork = root.current.alternate;
    root.finishedWork = finishedWork;
    root.finishedLane = lane;
    wipRootRenderLane = NoLane;

    commitRoot(root);
  }
}

从上面可以看出,内部调用了renderRoot方法,然后根据renderRoot方法返回的状态(1. 中断。2. 执行完)判断不同的逻辑。我们看看renderRoot是如何返回任务是否执行完成的。

render真正执行的位置renderRoot

它的重要逻辑就是根据我们传入的优先级,判断是否是同步还是并发的执行任务调度。

以及根据调度的状态返回是否是调度完成,还是由于时间分片被中断。

/**
 * 并发和同步更新的入口(render阶段)
 * @param root
 * @param lane
 * @param shouldTimeSlice
 */
function renderRoot(root: FiberRootNode, lane: Lane, shouldTimeSlice: boolean) {
  if (__DEV__) {
    console.log(`开始${shouldTimeSlice ? "并发" : "同步"}render更新`);
  }

  // 由于并发更新会不断的执行,但是并不需要更新,所以我们需要判断优先级看看是否需要初始化
  // 如果wipRootRenderLane 不等于 当前更新的lane, 就需要重新初始化,从根部开始调度
  if (wipRootRenderLane !== lane) {
    // 初始化,将workInProgress 指向第一个fiberNode
    prepareFreshStack(root, lane);
  }

  do {
    try {
      shouldTimeSlice ? workLoopConcurrent() : workLoopSync();
      break;
    } catch (e) {
      workInProgress = null;
    }
  } while (true);

  // 中断执行
  if (shouldTimeSlice && workInProgress !== null) {
    return RootInComplete;
  }
  if (!shouldTimeSlice && workInProgress !== null && __DEV__) {
    console.error(`render阶段结束时wip不应该为null`);
  }

  //render阶段执行完
  return RootCompleted;
}

从上面的代码,我们可以看出几个点:.

  1. prepareFreshStack这个函数标记着是否需要从根fiberRootNode开始执行。这里很重要,用于并发被中断后,不从头部执行,而是继续上一次的调度。
  2. 根据传入的shouldTimeSlice来判断执行的workLoop方法
  3. 返回中断RootInComplete或者完成RootCompleted

workLoopConcurrentworkLoopSync

从名字中,我们可以猜出大概的意思了。

  • workLoopSync: 代表同步执行,不会被中断。
  • workLoopConcurrent: 时间分片,可以被中断, 通过调度器scheduler提供的方法,判断时间是否用完。如果用完就停止循环,将主动权返回给浏览器,等待下一次执行。
// 同步更新
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// 并发更新
function workLoopConcurrent() {
  while (workInProgress !== null && !unstable_shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

总结

上面我们大致的看了一下代码的执行流程,接下来,我们来通过几个例子来讲解执行的流程。

例子 - 同一个任务时间分片

假如我们刚刚开始渲染页面的时候,默认是普通的优先级,所以会使用时间分片去切割一个大的任务。通过上面的主流程分析的大致应该明白了整个执行过程。

  1. 刚刚开始会进入scheduleUpdateOnFiber, 去开始调度。
  2. ensureRootIsScheduled进入,获取本次的优先级,例如普通优先级
  3. 调用调度器的scheduleCallback,去获取时间分片
  4. 并发更新调用performConcurrentWorkOnRoot,开始进行render阶段的渲染
  5. 进入render阶段,调用renderRoot,此时第一次渲染wipRootRenderLane为Nolane, 此时的任务更新的lane4。 所以执行prepareFreshStack从根节点开始调度。
    if (wipRootRenderLane !== lane) {
      prepareFreshStack(root, lane);
    }
  1. 由于本次是并发更新,接下来调用workLoopConcurrent
  2. workLoopConcurrent中,当时间分片用完后,unstable_shouldYield返回true,应该中断,将控制权还给浏览器

到这一步,第一次浏览器和react之间的交互已经完成。接下来等待浏览器下一次空闲时间,再次调度。

// 中断
if (exitStatus === RootInComplete) {
  // ensureRootIsScheduled中有更高的优先级插入进来, 停止之前的调度
  if (root.callbackNode !== curCallbackNode) {
    return null;
  }
  // 继续调度
  return performConcurrentWorkOnRoot.bind(null, root);
}

在中断的过程中,我们返回的函数performConcurrentWorkOnRoot.bind(null, root);scheduler调度器会在下次空闲的时候,再次调用performConcurrentWorkOnRoot

再次调度:

  1. performConcurrentWorkOnRoot执行, 再次获取优先级,此时优先级没有变化,还是返回上一次被中断的任务的优先级
  2. 继续调用render阶段的renderRoot
  3. 进入renderRoot后发现wipRootRenderLane === lane优先级没有变化,就不会调用prepareFreshStack再次从头开始调度。而是继续上一次的调度点继续调度。这样就可以将一个大的任务,切分成很多小的任务。而不堵塞浏览器的渲染。
  4. 接下来继续等待unstable_shouldYield返回true。中断本次的workLoopConcurrent

重复这个流程。直到任务完成或者有更高优先级的任务进入,打断了本次更新的任务。 剖析React系列十三-react调度

例子 - 高优先级任务打断正在执行低优先级任务

例如,当我们在useEffect中更新视图的时候,突然来了一个更高优先级的任务。这个时候就出现了高优先级打断低优先级的情况。

  1. 当一个低优先级的任务正在渲染过程中。一个高优先级的任务突然被插入
  2. scheduleUpdateOnFiber被执行,其他的流程都任务进行中的步骤一样。主要的区别是要取消之前的更新,以及从头开始遍历
  3. ensureRootIsScheduled中取消上一个低优先级任务
    // 当前产生了更高优先级调度,取消之前的调度
    if (existingCallback !== null) {
      unstable_cancelCallback(existingCallback);
    }
    
  4. 由于优先级现在不同,在renderRoot中就需要从头开始调度
    if (wipRootRenderLane !== lane) {
      // 初始化,将workInProgress 指向第一个fiberNode
      prepareFreshStack(root, lane);
    }
    
转载自:https://juejin.cn/post/7212101192745500728
评论
请登录