React18源码: task任务调度和时间分片

任务队列管理

  • 调度的目的是为了消费任务,接下来就具体分析任务队列是如何管理与实现的

  • 在 Scheduler.js 中,维护了一个 taskQueue,

  • 任务队列管理就是围绕这个 taskQueue 展开

    js 复制代码
    // Tasks are stored on a min heap
    var taskQueue - [];
    var timerQueue = [];
  • 注意

    • taskQueue一个堆数
    • 源码中除了 taskQueue 队列之外还有一个 timerQueue 队列, 这个队列是预留给延时任务使用的

创建任务

  • 在 unstable_scheduleCallback 函数中

    js 复制代码
    // 省略部分无关代码
    function unstable_scheduleCallback(prioritylevel, callback, options) {
      // 1. 获取当前时间
      var currentTime = getCurrentTime();
      var startTime;
      if (typeof options === 'object' && options !== null) {
        //从函数调用关系来看,,所有调用 unstable_scheduleCallback 都未传入options
        // 所以省略延时任务相关的代码
      } else {
        startTime = currentTime;
      }
      //  2. 根据传入的优先级,设置任务的过期时间 expirationTime
      var timeout;
      switch (priorityLevel) {
        case ImmediatePriority:
          timeout = IMMEDIATE_PRIORITY_TIMEOUT;
          break;
        case UserBlockingPriority:
          timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
          break;
        case IdlePriority:
          timeout = IDLE_PRIORITY_TIMEOUT;
          break;
        case LowPriority:
          timeout = LOW_PRIORITY_TIMEOUT;
          break;
        case NormalPriority:
        default:
            timeout = NORMAL_PRIORITY_TIMEOUT;
            break
      }
    
      var expirationTime = startTime + timeout;
      // 3.创建新任务
      var newTask = {
        id: taskIdCounter ++,
        callback,
        priorityLevel,
        startTime,
        expirationTime,
        sortIndex: -1,
      }
      if (startTime > currentTime) {
      } else {
        newTask.sortIndex = expirationTime;
        // 4. 加入任务队列
        push(taskQueue, newTask);
        // 5.请求调度
        if (!isHostCallbackScheduled && !isPerformingWork) {
          isHostCallbackScheduled = true;
          requestHostCallback(flushWork);
        }
      }
      return newTask;
    }

消费任务

  • 创建任务之后,最后请求调度 requestHostCallback(flushwork)(创建任务源码中的第5步)

  • flushWork 函数作为参数被传入调度中心内核等待回调

  • requestHostCallback 函数是调度内核中的一个

  • 在调度中心中只需下一个事件循环就会执行回调,最终执行 flushwork

    js 复制代码
    // 省略无关代码
    function flushWork(hasTimeRemaining, initialTime) {
      //1.做好全局标记,表示现在已经进入调度阶段
      isHostCallbackScheduled = false;
      isPerformingWork - true;
      const previousPrioritylevel = currentPriorityLevel;
      try {
        // 2.循环消费队列
        return workLoop(hasTimeRemaining, initialTime);
      } finally {
        // 3.还原全局标记
        currentTask = null;
        currentPriorityLevel = previousPriorityLevel;
        isPerformingWork = false;
      }
    }
  • flushwork中调用了 workLoop 队列消费的主要逻辑是在workLoop函数中

  • 这就是前面提到的任务调度循环

    js 复制代码
    //省略部分无关代码
    function workLoop(hasTimeRemaining, initialTime) {
      let currentTime = initialTime; //保存当前时间,用于判断任务是否过期
      currentTask = peek(taskQueue); //获取队列中的第一个任务
      while (currentTask !== null) {
        if(
          currentTask.expirationTime > currentTime &&
          (!hasTimeRemaining || shouldYieldToHost())
        ) {
          // 虽然currentTask没有过期,但是执行时间超过了限制(毕竟只有5ms, shouldYieldToHost()返回true)
          break;
        }
        const callback = currentTask.callback;
        if (typeof callback === 'function') {
          currentTask. callback = null;
          currentPrioritylevel = currentTask.prioritylevel;
          const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
          // 执行回调
          const continuationCallback = callback(didUserCallbackTimeout);
          currentTime = getCurrentTime();
          // 回调完成,判断是否还有连续(派生)回调
          if (typeof continuationCallback === 'function') {
            // 产生了连续回调(如fiber树太大,出现了中断渲染),保留currentTask
            currentTask.callback = continuationCallback;
          } else {
            // 把currentTask移出队列
            if (currentTask === peek(taskQueue)) {
              pop(taskQueue);
            }
          }
        } else {
          // 如果任务被取消(这时currentTosk.callback ~ null),将其移出队列
          pop(taskQueue);
        }
        // 更currentTask
        currentTask = peek(taskQueue);
      }
      if (currentTask !== null) {
        return true; // 如果 task 队列没有清空,返回 true。寻待调度中心下一次回调
      } else {
        return false; // task 队列已经清空,返回false.
      }
    }
  • workLoop 就是一个大循环,虽然代码也不多,但是非常精髓

  • 在此处实现了时间切片(time slicing)和fiber树的可中断渲染

  • 这2大特性的实现,都集中于这个while循环

  • 每一次while循环的退出就是一个时间切片,深入分析while循环的退出条件:

    • 1.队列被完全清空:这种情况就是很正常的情况,一气呵成,没有遇到任何阻碍.
    • 2.执行超时:在消费taskQueue时,在执行 task.callback之前,都会检测是否超时,所以超时检测是以task为单位
      • 如果某个 task.callback 执行时间太长(如:fiber树很大,或逻辑很重)也会造成超时
      • 所以在执行task.cal1back过程中,也需要一种机制检测是否超时,如果超时了就立刻暂停task.callback的执行.

时间切片原理

  • 消费任务队列的过程中,可以消费1~n个task,甚至清空整个queue.
  • 但是在每一次具体执行task.callback之前都要进行超时检测,如果超时可以立即退出循环并等待下一次调用.

可中断渲染原理

  • 在时间切片的基础之上,如果单个task.callback执行时间就很长(假设200ms)
  • 就需要task.callback自己能够检测是否超时,所以在fiber树构造过程中
  • 每构造完成一个单元,都会检测一次超时,如遇超时就退出fiber树构造循环,并返回一个新的回调函数
  • 就是 continuationCallback 并等待下一次回调继续未完成的fiber树构造

节流防抖{#throttle-debounce}

  • 通过以上分析,已经覆盖了 scheduler 包中的核心原理

  • 现在再次回到 react-reconciler包中,在调度过程中的关键路径中,还需要理解一些细节

  • 在 Renconciler 运行流程中总结的4个阶段中,注册调度任务属于第2个阶段

  • 核心逻辑位于ensureRootIsScheduled函数中

    js 复制代码
    // 省略部分无关代码
    function ensureRootIsscheduled(root: FiberRoot, currentTime: number) {
      // 前半部分:判断是否需要注册新的调度
      const existingcallbackNode = root.callbackNode;
      const nextLanes = getNextLanes(
        root,
        root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
      );
      const newCallbackPriority = returnNextLanesPriority();
      if (nextLanes === NoLanes) {
        return;
      }
      // 节流防抖
      if (existingcallbackNode !== null) {
        const existingcallbackpriority = root.callbackpriority;
        if (existingCallbackPriority === newCallbackPriority){
          return;
        }
        cancelCallback(existingcallbackNode);
      }
    
      // 后半部分:注册调度任务省略代码
      // 更新标记
      root.callbackPriority = newcallbackPriority;
      root.callbackNode = newcallbackNode;
    }
  • 正常情况下,ensureRootIsScheduled 函数会与scheduler包通信,最后注册一个task并等待回调.

  • 1.在task注册完成之后,会设置fiberRoot对象上的属性,代表现在已经处于调度进行中

  • 2.再次进入ensureRootIsScheduled时

    • 比如连续2次 setState,第2次 setState同样会触发
    • reconciler运作流程中的调度阶段,如果发现处于调度中
    • 则需要一些节流和防抖措施,进而保证调度性能.
    • a.节流
      • 判断条件:existingCallbackPriority == newCallbackPriority
      • 新旧更新的优先级相同,如连续多次执行setState
      • 则无需注册新task(继续沿用上一个优先级相同的task),直接退出调用
    • b.防抖
      • 判断条件: existingCallbackPriority !== newCallbackPriority
      • 新旧更新的优先级不同,则取消旧task, 重新注册新task
  • 总结

    • 主要分析了scheduler包中调度原理
    • 也就是React两大工作循环中的任务调度循环
    • 时间切片和可中断渲染等特性在任务调度循环中的实现
    • scheduler包是React运行时的心脏,为了提升调度性能
    • 注册task之前,在react-reconciler包中做了节流和防抖等措施
相关推荐
卸任7 小时前
Vibe coding工具使用心得
前端·ai编程
brien97 小时前
为什么模型标称 1M,Google Antigravity 聊到 256K 却突然失忆?我们给它做了个原生行内监控圆环
前端
修炼的dance8 小时前
录屏关键帧间隔多少合适?六档实测,2秒到10秒小了75.6%
前端
Lank_M8 小时前
关键帧从1秒拉到10秒,录屏小87.3%,摄像头小10.8%
前端
补码缩写8 小时前
用 ffprobe 拆录屏,八成字节在 29 个关键帧上
前端
百思不得小李8 小时前
一个复杂的动态表单是如何实现的
前端·设计模式
陈随易8 小时前
Cmder 停更 17 个月,2.0 支持 Windows Terminal
前端·后端·程序员
风骏时光牛马8 小时前
前沿视界
前端
kyriewen8 小时前
我扫了 814 个依赖,97 个还敢要 Node 10
前端·javascript·程序员
陈随易8 小时前
3000 万人在用的翻译插件,重写一遍就是一门生意
前端·后端·程序员