React 源码Scheduler(二)React的调度流程

本文源码基于 React 16.8.6 (March 27, 2019),仅记录一些我的阅读源码的分享与体会。javascript

欢迎你们交流和探讨java

前言

上一节中,笔者介绍了浏览器中调度算法的种类,并基于此实现了一个简单的时间分片调度。node

React 的调度流程借鉴了浏览器中 requestIdleCallback 的模式,实现了时间片的分割与超时任务的调度管理功能。算法

同时,做为跨平台框架的 React,将各个平台功能的底层实现抽象出一层 HostConfig 的 API 层,如此一来既保证了各平台 API 接口的统一性和健壮性,也便于构建 mock api 以供测试,值得咱们借鉴学习。api

在本节中,咱们将一块儿深刻 React 源码中,探究其内部调度的实现。浏览器

Scheduler

React 调度算法的源码位于 packages/scheduler/src/Scheduler.js 文件。在阅读源码以前,为了让你们对于该算法有一个总体的认识,笔者制做了以下类图:框架

抛开函数部分暂不谈,Scheduler 数据成员主要分为任务优先级设定,不一样优先级任务超时时间设定和一些记录当前任务状态的私有成员变量。函数

在 React 中,任务优先级由高至低可依次分为 ImmediateUserBlockingNormalLowIdle。同时每种任务也有着各自的超时时间,避免任务陷入饿死状态。该任务的分类就是 React 中基于优先级的时间分片调度算法基础。post

调度的执行过程

跟随源码,咱们找到了调度算法的入口 unstable_scheduleCallback。外部环境经过该函数的调用添加任务至优先级队列,正式打开调度流程的大门。性能

scheduleCallback

function unstable_scheduleCallback(priorityLevel, callback, deprecated_options) {
  // 经过 options 的timeout属性或者任务的优先级获取任务的超时时间
  var startTime =
    currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();

  var expirationTime;
  if (deprecated_options.timeout === 'number') {
    // 若是有设置 timeout 属性
    expirationTime = startTime + deprecated_options.timeout;
  } else {
    // 不然根据优先级肯定超时时间
    switch (priorityLevel) {
      case ImmediatePriority:
        expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
        break;
      // ...
    }
  }

  var newNode = {
    callback,
    priorityLevel: priorityLevel,
    expirationTime,
    next: null,
    previous: null,
  };
  if (firstCallbackNode === null) {
  // 若是初次调用,则直接进行调度
    firstCallbackNode = newNode.next = newNode.previous = newNode;
    scheduleHostCallbackIfNeeded();
  } else {
    // 遍历节点按超时时间从小到大的顺序,将新节点插入
    var next = null;
    var node = firstCallbackNode;
    do {
      if (node.expirationTime > expirationTime) {
        next = node;
        break;
      }
      node = node.next;
    } while (node !== firstCallbackNode);
    if (next === null) {
      next = firstCallbackNode;
    } else if (next === firstCallbackNode) {
      firstCallbackNode = newNode;
      scheduleHostCallbackIfNeeded();
    }
    // 插入节点列表
    var previous = next.previous;
    previous.next = next.previous = newNode;
    newNode.next = next;
    newNode.previous = previous;
  }

  return newNode;
}
复制代码

scheduleCallback 函数中,运用了一个双向循环队列 firstCallbackNode 做为调度节点的存储。函数一共作了三件事。

  • 计算超时时间 expirationTime
  • 创建一个 callBackNode,按照超时时间从小到达的顺序插入队列
  • 尝试经过 scheduleHostCallbackIfNeeded 进行调度

超时时间的设置,保证了任务在最坏的状况下仍旧能被最终执行,firstCallbackNode 的队列记录了每个最小化的原子任务(即该任务没法再进行中断切换),以便在调度时执行。接下来让咱们走进 scheduleHostCallbackIfNeeded,

scheduleHostCallbackIfNeeded

function scheduleHostCallbackIfNeeded() {
  // 任务执行中,直接返回
  if (isPerformingWork) {
    return;
  }
  if (firstCallbackNode !== null) {
    var expirationTime = firstCallbackNode.expirationTime;
    // 若是节点处理调度中但未执行,中断处理
    if (isHostCallbackScheduled) {
      cancelHostCallback();
    } else {
      isHostCallbackScheduled = true;
    }
    requestHostCallback(flushWork, expirationTime);
  }
}
复制代码

scheduleHostCallbackIfNeeded 函数作的事情也很简单,在任务队列创建好以后。若是当前任务正在执行中,则直接退出调度,防止屡次重复进入调度形成的性能损失。同时,若是任务正在调度但还没有执行,则说明新进任务优先级更高,中断原先任务调度执行新任务。虽然任务的回调函数都是 flushWork, 但优先级更高的任务拥有更小的 expirationTime,所以能保证任务更快执行。

flushWork

通过了上述两步调度预处理后,咱们进入了真正执行调度任务的地方。

function flushWork(didUserCallbackTimeout) {
  //...
  isHostCallbackScheduled = false;

  isPerformingWork = true;
  const previousDidTimeout = currentHostCallbackDidTimeout;
  currentHostCallbackDidTimeout = didUserCallbackTimeout;
  try {
    if (didUserCallbackTimeout) {
      // 调度超时,执行所有超时任务
      while (firstCallbackNode !== null) {
        var currentTime = getCurrentTime();
        if (firstCallbackNode.expirationTime <= currentTime) {
          do {
            flushFirstCallback();
          } while (
            firstCallbackNode !== null &&
            firstCallbackNode.expirationTime <= currentTime // 若是任务超时
          );
          continue;
        }
        break;
      }
    } else {
      // 调度未超时,则执行任务直到超时挂起
      if (firstCallbackNode !== null) {
        do {
          flushFirstCallback();
        } while (firstCallbackNode !== null && !shouldYieldToHost());
      }
    }
  } finally {
    isPerformingWork = false;
    currentHostCallbackDidTimeout = previousDidTimeout;
    // 检查是否有遗留任务未执行
    scheduleHostCallbackIfNeeded();
  }
}
复制代码

正如 requestIdleCallback 的方案,在执行任务时,函数能经过 didUserCallbackTimeout 变量识别调度任务是否已超时,同时能经过 shouldYieldToHost 函数获取到当前状态,便是否仍有剩余时间进行下一项任务的执行。

若进入函数时调度任务已经超时,则说明这个任务已经等过久了,再不让执行就要饿死了!所以,便得到了在不打断的状况下执行全部已超时的任务的权限。若当前调度还没有超时,则在规定的时效内,尽量多的执行任务。当该次调度执行完毕(不论是任务执行完或者由于中断暂停执行),在任务执行完毕后从新执行 scheduleHostCallbackIfNeeded 为下一次的任务调度作准备。

flushFirstCallback

该函数是回调任务最终执行之处,作的事情概括起来也就三点。

  • 从队列中获取并移除 firstCallbackNode
  • 进行 firstCallbackNode 回调函数的执行
  • 若回调函数结果还是一个函数,则构建并加入队列
function flushFirstCallback() {
  const currentlyFlushingCallback = firstCallbackNode;
  //... 从队列中去除 firstCallbackNode

  // 简写对应值
  var callback = currentlyFlushingCallback.callback;
  var expirationTime = currentlyFlushingCallback.expirationTime;
  var priorityLevel = currentlyFlushingCallback.priorityLevel;
  var previousPriorityLevel = currentPriorityLevel;
  var previousExpirationTime = currentExpirationTime;
  currentPriorityLevel = priorityLevel;
  currentExpirationTime = expirationTime;
  var continuationCallback;
  try {
    const didUserCallbackTimeout =
      currentHostCallbackDidTimeout ||
      // 当即执行优先级总认为是超时的
      priorityLevel === ImmediatePriority;
    continuationCallback = callback(didUserCallbackTimeout);
  } catch (error) {
    throw error;
  } finally {
    // 恢复现场
    currentPriorityLevel = previousPriorityLevel;
    currentExpirationTime = previousExpirationTime;
  }

  if (typeof continuationCallback === 'function') {
    //... 构造新节点,插入列表,如 scheduleCallback 所作
  }
}
复制代码

总结

至此,React 的基础调度流程便算是走了一遍,让咱们最后经过一个流程图对整个流程作一个梳理。

每个调度流程,都由 scheduleCallback 函数为入口,经由检查器 scheduleHostCallbackIfNeeded 将任务标记为调度状态,在 flushWork 中循环调用执行任务,最后在任务执行完毕 firstCallbackNode 为空时,由 scheduleHostCallbackIfNeeded 函数确认任务执行完毕,结束该调度流程。

在阅读过程当中,或许有一些小伙伴发现,诸如 requestHostCallbackcancelHostCallback 等函数咱们并无介绍内部实现。这些即是咱们开头所说的 React 基于不一样平台作的抽象层接口。在下一篇也是最后一篇中,咱们将走进这些函数的背后,学习在浏览器的平台上 React 是如何模拟时间分片的。

相关文章
相关标签/搜索