Schedule 调度过程

153 阅读6分钟

引言

在React16版本之前,React架构是分为两部分的:Reconcile 和 Render。假设我们现在有一个很大的项目,我们在比较深的子组件里触发了更新,那么 v16 之前的版本是会全量进行递归更新的,假设递归更新的时间超过了 50ms,用户就会感觉到明显的卡顿。v16 的诞生就是为了解决这个问题,React 团队重构了架构,新增了一个 Schedule 的角色,这个角色可以进行任务的调度,让项目有了更高的性能。这篇文章主要是讲一下React 中 Schedule 的作用。

原理

这里放一张别人画好的图吧,出处我会在文末粘出来。

image.png 这张图就可以很形象的表示整个工作过程,由于 Schedule 比较复杂,本次讲解只讲解涉及到渲染方面的内容。

ensureRootIsScheduled

入口函数是ensureRootIsScheduled,我们来看一下这部分具体做了什么

/...

newCallbackNode = scheduleCallback$1(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));

有些代码略过吧,如果React 版本是 18 的话,会进入一个scheduleCallback$1开启调度。

scheduleCallback$1

function unstable_scheduleCallback(priorityLevel, callback, options) {
  var currentTime = exports.unstable_now();
  var startTime;

  if (typeof options === 'object' && options !== null) {
    var delay = options.delay;

    if (typeof delay === 'number' && delay > 0) {
      startTime = currentTime + delay;
    } else {
      startTime = currentTime;
    }
  } else {
    startTime = currentTime;
  }

  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;
  var newTask = {
    id: taskIdCounter++,
    callback: callback,
    priorityLevel: priorityLevel,
    startTime: startTime,
    expirationTime: expirationTime,
    sortIndex: -1
  };

  if (startTime > currentTime) {
    // This is a delayed task.
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);

    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      } // Schedule a timeout.


      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    // wait until the next time we yield.


    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }

  return newTask;
}

生成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;

此部分会根据传入的任务优先级去生成一个expirationTime,expirationTime很重要!!!后面 React 会根据这个属性判断任务是否过期,从而解决饥饿问题。

开启任务调度

  var newTask = {
    id: taskIdCounter++,
    callback: callback,
    priorityLevel: priorityLevel,
    startTime: startTime,
    expirationTime: expirationTime,
    sortIndex: -1
  };

  if (startTime > currentTime) {
    // This is a delayed task.
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);

    if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
      // All tasks are delayed, and this is the task with the earliest delay.
      if (isHostTimeoutScheduled) {
        // Cancel an existing timeout.
        cancelHostTimeout();
      } else {
        isHostTimeoutScheduled = true;
      } // Schedule a timeout.


      requestHostTimeout(handleTimeout, startTime - currentTime);
    }
  } else {
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);
    // wait until the next time we yield.


    if (!isHostCallbackScheduled && !isPerformingWork) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);
    }
  }
  1. 首先根据 startTime 和 currentTime 进行比较,如果开始时间大于当前时间,说明任务属于延迟任务,会 push 进入timerQueue,反之会 push 进入 taskQueue。
  2. 这里 push 方法其实维护了一个小顶堆,小顶堆的好处就是每次取出任务的时候,都是优先级最高的任务。
  3. 执行requestHostCallback 开启任务的调度requestHostCallback(flushWork);

任务调度

function requestHostCallback(callback) {
  scheduledHostCallback = callback;

  if (!isMessageLoopRunning) {
    isMessageLoopRunning = true;
    schedulePerformWorkUntilDeadline();
  }
}
  schedulePerformWorkUntilDeadline = function () {
    localSetImmediate(performWorkUntilDeadline);
  };
} else if (typeof MessageChannel !== 'undefined') {
  // DOM and Worker environments.
  // We prefer MessageChannel because of the 4ms setTimeout clamping.
  var channel = new MessageChannel();
  var port = channel.port2;
  channel.port1.onmessage = performWorkUntilDeadline;

  schedulePerformWorkUntilDeadline = function () {
    port.postMessage(null);
  };
} else {
  // We should only fallback here in non-browser environments.
  schedulePerformWorkUntilDeadline = function () {
    localSetTimeout(performWorkUntilDeadline, 0);
  };
}

这里可以看到主要是执行了一个schedulePerformWorkUntilDeadline,而schedulePerformWorkUntilDeadline函数根据不同的运行时会调用不同的任务调度方法,方便兼容不同的浏览器。

我们主要分析主流浏览器环境即可。可以看到他使用 MessageChannel 开启了一个宏任务,那么这里就跟事件循环有关系了。如果有同学对 MessageChannel 不熟悉的,可以访问这个链接看看这篇文章:developer.mozilla.org/zh-CN/docs/…. 当浏览器有空闲状态后,就会执行我们启动的这次宏任务。

performWorkUntilDeadline

performWorkUntilDeadline这个函数就是 React 注册的,接收宏任务的函数,通过这个函数来执行具体的任务调度。

/... 
try {
      // workLoop返回的flag, 如果 workLoop 返回一个 true,会在开启一个调度者,在下一轮事件循环进行调度。
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
      if (hasMoreWork) {
        // If there's more work, schedule the next message event at the end
        // of the preceding one.
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }

可以看到具体的逻辑就是调用scheduledHostCallback来执行任务,同时这里有一个可恢复的逻辑,如果当前scheduledHostCallback返回了一个 true,那么会再次开启新一轮的宏任务,在下一轮的事件循环去继续处理任务。

scheduledHostCallback

 try {
        return workLoop(hasTimeRemaining, initialTime);
      } catch (error) {
        if (currentTask !== null) {
          var currentTime = exports.unstable_now();
          markTaskErrored(currentTask, currentTime);
          currentTask.isQueued = false;
        }

        throw error;
      }

内部会调用一个flushWork,flushWork 里的主要逻辑就是我上面粘贴的函数体,可以看到主要逻辑就是调用了一个 workLoop。 那么任务是否可恢复,与workLoop 的返回值是息息相关的。

workLoop

function workLoop(hasTimeRemaining, initialTime) {
  var currentTime = initialTime;
  advanceTimers(currentTime);
  //  取出任务的最高优先级
  currentTask = peek(taskQueue);
  console.log('currentTask==',currentTask)

  while (currentTask !== null && !(enableSchedulerDebugging )) {
  // 判断当前任务是否过期
    if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())) {
      // This currentTask hasn't expired, and we've reached the deadline.
      break;
    }

    var callback = currentTask.callback;

    if (typeof callback === 'function') {
      currentTask.callback = null;
      currentPriorityLevel = currentTask.priorityLevel;
      var didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      // 执行真正的调度任务,如果返回值是一个函数,继续执行 | 退出循环,下一轮调度继续执行。
      var continuationCallback = callback(didUserCallbackTimeout);
      currentTime = exports.unstable_now();

      if (typeof continuationCallback === 'function') {
        currentTask.callback = continuationCallback;
      } else {

        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }

      advanceTimers(currentTime);
    } else {
      pop(taskQueue);
    }

    currentTask = peek(taskQueue);
  } // Return whether there's additional work


  if (currentTask !== null) {
    return true;
  } else {
    var firstTimer = peek(timerQueue);

    if (firstTimer !== null) {
      requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
    }

    return false;
  }
}
  1. 取出当前优先级最高的任务。
  2. 判断当前任务是否可以继续执行(任务是否过期 | hasTimeRemaining为 true,执行时间是否超过了 50ms)
  3. 通过 callback 进入 beginWork 和 completeWork 阶段
  4. 如果 callback 返回值是一个函数,说明当前任务没执行完毕,在下一轮事件循环还会继续执行。
  5. 所有任务执行完毕之后退出循环,结束调度。

下面来分析一下callback 具体干了什么

callback

callback 会被赋值为performConcurrentWorkOnRoot,我们可以看一下performConcurrentWorkOnRoot的实现逻辑

/...
  var shouldTimeSlice = !includesBlockingLane(root, lanes) && !includesExpiredLane(root, lanes) && ( !didTimeout);
/ ...
  var exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);
    if (root.callbackNode === originalCallbackNode) {
    // The task node scheduled for this root is the same one that's
    // currently executed. Need to return a continuation.
    /*KaSong*/logHook('continuationCallback', root);
    return performConcurrentWorkOnRoot.bind(null, root);
  }

  return null;

主要逻辑就是根据shouldTimeSlice判断调用renderRootConcurrent还是renderRootSync。这两个方法的区别是一个可以被打断, 一个不可以被打断。

最后会根据callbackNode判断是否需要继续调度还是结束调度。

下面我们选一个来讲,renderRootConcurrent

renderRootConcurrent

  do {
    try {
      workLoopConcurrent();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
  function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

主要逻辑就是进入 workLoopConcurrent以及判断workInProgress和shouldYield来决定是否需要继续调度。 shouldYield就是之前那个方法,判断当前执行时间是否超过了 50ms。

总结

整个过程基本上分析的差不多了,其实就是那张大图上所展示的,有两个 workLoop,一个大的 workLoop 用于判断当前任务是否需要继续调度还是结束调度。小的 workLoop 是用来判断单个 fiberNode 节点生成是否时间过长,如果时间过长,React 会结束这次渲染,在下一次事件循环继续开启任务来执行。