scheduler 源码分析

本文深入探讨React Scheduler的工作原理,解析其如何通过不同的优先级管理组件的渲染任务,以提高复杂应用的操作性能。从异步队列到任务调度,再到调度器的API详解,帮助读者全面理解Scheduler在React中的角色。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么是要有scheduler

首先要从js的是单线程模型来说起,Javascript执行是会经历静态编译,动态解释和事件循环做任务调度的过程,大致的流程如下(注意,该流程是以chrome浏览器内核为标准的执行流程,在node或者其他浏览器中,执行流程会有所差异,但是核心思想是差不多的。从这里面我们很直观的认识到js的线程模型是怎么工作的。那跟scheduler有什么关系呢,我们都知道React16采用了fiber架构,这样的架构下,使用链表结构代替原有的函数嵌套,避免无法控制组件渲染的过程的问题,Fiber让React内部会动态灵活的管理所有组件的渲染任务,可以中断暂停某一个组件的渲染,所以,对于复杂型应用来说,对于某一个交互动作的反馈型任务,我们是可以对其进行拆解,一步步的做交互反馈,避免在一个页面重绘时间周期内做过多的事情,这样就能减少应用的长任务,最大化提升应用操作性能,更好的利用有限的时间,那么,我们现在可以只聚焦在任务管理上,一起来研究一下React到底是如何调度组件的任务执行的,这里说渲染感觉不太准确

macrotask: setTimeout, setInterval, setImmediate,MessageChannel, I/O, UI rendering netWork
microtask: process.nextTick, Promises, Object.observe(废弃), MutationObserver

[引自大神论述]


上图来自[知乎文章](https://zhuanlan.zhihu.com/p/48254036)
  • 主线程负责解析编译和调度异步事件循环调度
  • 异步队列和V8通讯 通过polling Check来实现
  • 异步队列分成macotask 和 microtask

macrotask

一般情况下,在没有特别说明的情况下我们会把macrotask称为task queues ,在一次的事件循环中,他只会执行一次

microtask

  • 在每一次事件循环中,macrotask 只会提取一个执行,而 microtask 会一直提取,直到 microtasks 队列清空。

  • 那么也就是说如果我的某个 microtask 任务又推入了一个任务进入 microtasks 队列,那么在主线程完成该任务之后,仍然会继续运行 microtasks 任务直到任务队列耗尽

  • 而事件循环每次只会入栈一个 macrotask ,主线程执行完该任务后又会先检查 microtasks 队列并完成里面的所有任务后再执行 macrotask

console.log('start');
setTimeout(function() {
  console.log('macrotask');
}, 0);
Promise.resolve().then(function() {
  console.log('microtask');
}).then(function() {
  console.log('microtask');
});
console.log(' end');
复制代码

根据上述理论自己试试程序的运行结果, 为什么我们在分析scheduler源码之前先要介绍下异步队列,因为了解清楚js异步队列才会让我们更加清晰知道scheduler是怎么使用调度方法来更好的安排代码执行时机。

浏览器的执行页面绘制过程


图片出自同一地方 执行JS(具体流程在上面有描述)--->计算Style--->构建布局模型(Layout)--->绘制图层样式(Paint)--->组合计算渲染呈现结果(Composite)

  • 一个完成的过程称之为一帧
  • 一般设备的刷新频率是60Hz (还有更大的 scheduler 最大设定为 120hz) 也就是按照理想情况来说一秒钟有60帧 那么一帧的平均时间是 1000 / 60 大约是 16.7ms 也就是我们一帧的执行时间不能超过 16.7 否则就会出现丢失帧和卡顿情况
  • 帧的渲染是在处理完流程之后进行的
  • 帧的渲染是在独立的UI线程去执行 是有GPU等
  • 剩余的时间为空闲时间
  • 在离散型 交互动作中不一定要求需要16.7 ms的时间
  • 对于离散型交互,上一帧的渲染到下一帧的渲染时间是属于系统空闲时间,经过亲测,Input输入,最快的单字符输入时间平均是33ms(通过持续按同一个键来触发),相当于,上一帧到下一帧中间会存在大于16.4ms的空闲时间,就是说任何离散型交互,最小的系统空闲时间也有16.4ms,也就是说,离散型交互的最短帧长一般是33ms

requestIdleCallback

在帧的渲染中当执行完流程和UI绘制之后 会有一部分空闲时间,如果我们能掌握这个时间加一充分利用就更加理想 那如何知道一帧进入这个空闲时间呢,浏览器目前提供了这个回调 requestIdleCallback 即浏览器空闲时

var handle = window.requestIdleCallback(callback[, options]);
复制代码
  • requestIdleCallback回调调用时机是在回调注册完成的上一帧渲染到下一帧渲染之间的空闲时间执行
  • callback 是要执行的回调函数,会传入 deadline 对象作为参数,deadline 包含:
  • timeRemaining:剩余时间,单位 ms,指的是该帧剩余时间。
  • didTimeout:布尔型,true 表示该帧里面没有执行回调,超时了。
  • option: { timeout : 即超时时间, 不提供浏览器自己去计算 }
  • 如果给定 timeout,那到了时间,不管有没有剩余时间,都会立刻执行回调 callback。

requestAnimationFrame

  • 以前我们知道: requestAnimationFrame回调只会在当前页面激活状态下执行,可以大大节省CPU开销
  • requestAnimationFrame回调参数是回调被调用的时间,也就是当前帧的起始时间(可以通过这个时间去判断 到底有么有超时)
  • 系统控制回调的执行时机恰好在回调注册完成后的下一帧渲染周期的起点的开始执行,控制js计算的到屏幕响应的精确性,避免步调不一致而导致丢帧

目前浏览器对于requestIdleCallback的支持不是特别完整,所以react团队放弃了requestIdleCallback的使用 自己用requestAnimationFrame和MessageChannel来polyfill

requestIdleCallback Polyfill方案

很简单,33毫秒,但是时间并不总是33ms,这个时间是React认为的一个可以接受的最大值,如果运行设备能做到大于30fps,那么它会去调整这个值(通常情况下可以调整到16.6ms)。调整策略是用当前每帧的总时间与实际每帧的时间进行比较,当实际时间小于当前时间且稳定(前后两次都小于当前时间),那么就会认为这个值是有效的,然后将每帧时间调整为该值(取前后两次中时间大的值),还有就是requestAnimationFrame回调的第一个参数,每一帧的起始时间,最终借助requestAnimationFrame让一批扁平的任务恰好控制在一块一块的33ms这样的时间片内执行即可


所有准备工作都做好了, 接下来我们逐步来分析Scheduler源码

1、 调度基本常量定义

// 枚举

// 立即执行的任务
var ImmediatePriority = 1;

// 用户阻塞优先级
var UserBlockingPriority = 2;

// 一般的优先级
var NormalPriority = 3;

// 低级的优先级
var LowPriority = 4;

// 空闲的优先级
var IdlePriority = 5;

// 我们可以理解 优先级越高 过期时间就越短 反之 越长

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
// 最大整数
var maxSigned31BitInt = 1073741823;

// Times out immediately
// 超时的优先级时间 说明没有剩余时间了 需要立即被调度
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
// 事件的过期时间 250 ms
var USER_BLOCKING_PRIORITY = 250;

// 一般优先级的过期时间 5000 ms
var NORMAL_PRIORITY_TIMEOUT = 5000;

// 优先级低的 10000ms
var LOW_PRIORITY_TIMEOUT = 10000;

// Never times out 空闲的任务 有没有限制了 也就是最大整数
var IDLE_PRIORITY = maxSigned31BitInt;

复制代码

2、 调度所需变量的定义

// Callbacks are stored as a circular, doubly linked list.
// 回调保存为了循环的双向链表
var firstCallbackNode = null;

// 当前是否过期
var currentDidTimeout = false;
// Pausing the scheduler is useful for debugging.

// 调度是否中断
var isSchedulerPaused = false;

// 默认当前的优先级为一般优先级 
var currentPriorityLevel = NormalPriority;

// 当前时间开始时间
var currentEventStartTime = -1;

// 当前过期时间
var currentExpirationTime = -1;

// This is set when a callback is being executed, to prevent re-entrancy.
// 当前是否执行callback 调度
var isExecutingCallback = false;

// 是否有回调呗调度
var isHostCallbackScheduled = false;

// 支持performance.now 函数
var hasNativePerformanceNow = typeof performance === 'object' && typeof performance.now === 'function';
复制代码

3、 调度方法 unstable_scheduleCallback


function unstable_scheduleCallback(callback, deprecated_options) {

 //开始时间
  var startTime = currentEventStartTime !== -1 ? currentEventStartTime : exports.unstable_now();

  var expirationTime;
  
  // 过期时间 这里模拟的是 requestIdleCallback options的 timeout的定义
  // 如果这里指定了 timeout 就会计算出 过期时间
 // 如果么有指定就会根据 调度程序的优先级去计算  比如 普通是 5000 低级是 10000 空闲就永远不会过期等....
  if (typeof deprecated_options === 'object' && deprecated_options !== null && typeof deprecated_options.timeout === 'number') {
    // FIXME: Remove this branch once we lift expiration times out of React.
    expirationTime = startTime + deprecated_options.timeout;
  } else {
    switch (currentPriorityLevel) {
      case ImmediatePriority:
        expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
        break;
      case UserBlockingPriority:
        expirationTime = startTime + USER_BLOCKING_PRIORITY;
        break;
      case IdlePriority:
        expirationTime = startTime + IDLE_PRIORITY;
        break;
      case LowPriority:
        expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
        break;
      case NormalPriority:
      default:
        expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
    }
  }

  // 新建一个节点
  var newNode = {
    callback: callback,
    priorityLevel: currentPriorityLevel,
    expirationTime: expirationTime,
    next: null,
    previous: null
  };

  // Insert the new callback into the list, ordered first by expiration, then
  // by insertion. So the new callback is inserted any other callback with
  // equal expiration.
  
  // 将新回调插入列表,首先按到期排序,然后按插入排序。所以新的回调插入到任何callback都拥有相同的过期时间
  
  // 如果链表是空的 则 重新构建
  if (firstCallbackNode === null) {
    // This is the first callback in the list.
    firstCallbackNode = newNode.next = newNode.previous = newNode;
    ensureHostCallbackIsScheduled();
  } else {
    var next = null;
    var node = firstCallbackNode;
    
    // 先将链表根据过期时间进行排序 遍历查找 寻找比当前过期时间大的节点
    do {
      if (node.expirationTime > expirationTime) {
        // The new callback expires before this one.
        next = node;
        break;
      }
      node = node.next;
    } while (node !== firstCallbackNode);
 
    // 没有找到比当前更靠后的 元素 说明当前的节点是最不优先的
    if (next === null) {
      // No callback with a later expiration was found, which means the new
      // callback has the latest expiration in the list.
      // 当前新加入的节点是最后面的 指针指向链表头
      next = firstCallbackNode;
    } else if (next === firstCallbackNode) {
      // The new callback has the earliest expiration in the entire list.
      
      // 说明所有的任务
      firstCallbackNode = newNode;
      ensureHostCallbackIsScheduled();
    }


    //将新的node 加入到链表 维护一下循环链表
    var previous = next.previous;
    previous.next = next.previous = newNode;
    newNode.next = next;
    newNode.previous = previous;
  }

  return newNode;
}
复制代码

4. ensureHostCallbackIsScheduled 遇到优先级高的 需要特别处理

function ensureHostCallbackIsScheduled() {
  
  // 调度正在执行 返回 也就是不能打断已经在执行的
  if (isExecutingCallback) {
    // Don't schedule work yet; wait until the next time we yield.
    return;
  }
  // Schedule the host callback using the earliest expiration in the list.
  // 让优先级最高的 进行调度 如果存在已经在调度的 直接取消
  var expirationTime = firstCallbackNode.expirationTime;
  if (!isHostCallbackScheduled) {
    isHostCallbackScheduled = true;
  } else {
    // Cancel the existing host callback.
    // 取消正在调度的callback
    cancelHostCallback();
  }
  // 发起调度
  requestHostCallback(flushWork, expirationTime);
}

复制代码

5、requestAnimationFrameWithTimeout

var localSetTimeout = typeof setTimeout === 'function' ? setTimeout : undefined;
var localClearTimeout = typeof clearTimeout === 'function' ? clearTimeout : undefined;

// We don't expect either of these to necessarily be defined, but we will error
// later if they are missing on the client.
var localRequestAnimationFrame = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : undefined;
var localCancelAnimationFrame = typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;

// requestAnimationFrame does not run when the tab is in the background. If
// we're backgrounded we prefer for that work to happen so that the page
// continues to load in the background. So we also schedule a 'setTimeout' as
// a fallback.
// TODO: Need a better heuristic for backgrounded work.
var ANIMATION_FRAME_TIMEOUT = 100;
var rAFID;
var rAFTimeoutID;
/**
* 是解决网页选项卡如果在未激活状态下requestAnimationFrame不会被触发的问题,
*这样的话,调度器是可以在后台继续做调度的,一方面也能提升用户体验,
* 同时后台执行的时间间隔是以100ms为步长,这个是一个最佳实践,100ms是不会影响用户体验同时也不影响CPU能耗的一个折中时间间隔
为什么要用 settimeout 因为requestAnimationFrame不会在tab不激活的情况下不执行 
*/
var requestAnimationFrameWithTimeout = function (callback) {
  // schedule rAF and also a setTimeout
  rAFID = localRequestAnimationFrame(function (timestamp) {
    // cancel the setTimeout
    localClearTimeout(rAFTimeoutID);
    callback(timestamp);
  });
  rAFTimeoutID = localSetTimeout(function () {
    // cancel the requestAnimationFrame
    localCancelAnimationFrame(rAFID);
    callback(exports.unstable_now());
  }, ANIMATION_FRAME_TIMEOUT);
};

if (hasNativePerformanceNow) {
  var Performance = performance;
  exports.unstable_now = function () {
    return Performance.now();
  };
} else {
  exports.unstable_now = function () {
    return localDate.now();
  };
}
复制代码

6、调度requestHostCallback

这里react 做了特别的兼容处理 注入方式和不支持window或者 MessageChannel的方式 这里不做主要分析 因为 比较简单,这里将主要研究现代浏览器的处理方式

var requestHostCallback;
var cancelHostCallback;
var shouldYieldToHost;

var globalValue = null;
if (typeof window !== 'undefined') {
  globalValue = window;
} else if (typeof global !== 'undefined') {
  globalValue = global;
}

if (globalValue && globalValue._schedMock) {
  // Dynamic injection, only for testing purposes.
  // 动态注入 用于测试目的
  var globalImpl = globalValue._schedMock;
  requestHostCallback = globalImpl[0];
  cancelHostCallback = globalImpl[1];
  shouldYieldToHost = globalImpl[2];
  exports.unstable_now = globalImpl[3];
} else if (

// 非DOM环境
// If Scheduler runs in a non-DOM environment, it falls back to a naive
// implementation using setTimeout.
typeof window === 'undefined' ||
// Check if MessageChannel is supported, too.
typeof MessageChannel !== 'function') {
  // If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
  // fallback to a naive implementation.
  var _callback = null;
  var _flushCallback = function (didTimeout) {
    if (_callback !== null) {
      try {
        _callback(didTimeout);
      } finally {
        _callback = null;
      }
    }
  };
  requestHostCallback = function (cb, ms) {
  
  //这里的调度就直接在settimeout 去执行 也就是直接放入macrotask队列 这里应该是下下策 
    if (_callback !== null) {
      // Protect against re-entrancy.
      setTimeout(requestHostCallback, 0, cb);
    } else {
      _callback = cb;
      setTimeout(_flushCallback, 0, false);
    }
  };
  cancelHostCallback = function () {
    _callback = null;
  };
  shouldYieldToHost = function () {
    return false;
  };
} else {
  if (typeof console !== 'undefined') {
    // TODO: Remove fb.me link
    if (typeof localRequestAnimationFrame !== 'function') {
      console.error("This browser doesn't support requestAnimationFrame. " + 'Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills');
    }
    if (typeof localCancelAnimationFrame !== 'function') {
      console.error("This browser doesn't support cancelAnimationFrame. " + 'Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills');
    }
  }

  // 调度的callback
  var scheduledHostCallback = null;
  // 消息发送中标识
  var isMessageEventScheduled = false;
  // 过期时间
  var timeoutTime = -1;
  // rAF 轮询启动状态
  var isAnimationFrameScheduled = false;
  
  // 任务执行中标识
  var isFlushingHostCallback = false;
  // 下一帧期望完成时间点,用于判断重绘后 js 线程是否空闲,还是长期占用
  var frameDeadline = 0;
  // We start out assuming that we run at 30fps but then the heuristic tracking
  // will adjust this value to a faster fps if we get more frequent animation
  // frames.
  /**
  * 
   我们假设我们以30fps运行,然后进行启发式跟踪
   如果我们获得更频繁的动画,我会将此值调整为更快的fps
   帧
   默认33  为什么是33 因为我们假定每秒30帧固定评率刷新 也就是 一帧需要33ms
  */
  var previousFrameTime = 33;
  var activeFrameTime = 33;
  
  //以此推断线程是否空闲,好添加并处理新任
  shouldYieldToHost = function () {
    return frameDeadline <= exports.unstable_now();
  };

  // We use the postMessage trick to defer idle work until after the repaint.
  // 使用postMessage 来跟踪判断重绘是否完成
  var channel = new MessageChannel();
  var port = channel.port2;
  
  // 当port1 发送消息后 这里在帧重绘完成后 进入message回调 接着处理我们
  // callback
  channel.port1.onmessage = function (event) {
  
    isMessageEventScheduled = false;
    var prevScheduledCallback = scheduledHostCallback;
    var prevTimeoutTime = timeoutTime;
    scheduledHostCallback = null;
    timeoutTime = -1;

    var currentTime = exports.unstable_now();

    var didTimeout = false;
    // 说明没有时间了 当前帧给与这个callback的时间没有了
    if (frameDeadline - currentTime <= 0) {
      // There's no time left in this idle period. Check if the callback has
      // a timeout and whether it's been exceeded.
      // 检查当前callback 是否过期 和 是否被执行
      // previousFrameTime 小于等于 currentTime 时,scheduler
      // 认为线程不是空闲的,对于超时的任务将立即执行,
      // 对于未超时的任务将在下次重绘后予以处理
      // 显然是超时的 并且没有被取消 直接执行 并且给与timeout 为空
      if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
        // Exceeded the timeout. Invoke the callback even though there's no
        // time left.
        didTimeout = true;
      } else {
        // No timeout.
        //没有超时 如果没有安排轮询 就开启轮询
        if (!isAnimationFrameScheduled) {
          // Schedule another animation callback so we retry later.
          isAnimationFrameScheduled = true;
          requestAnimationFrameWithTimeout(animationTick);
        }
        // Exit without invoking the callback.
        // 不执行callback 直接退出 让轮询去调度
        scheduledHostCallback = prevScheduledCallback;
        timeoutTime = prevTimeoutTime;
        return;
      }
    }

    // 执行callback 这里应该是终点了 到这里为止 调度分析完了
    // 执行完成的调度 返回的只有 是否已经过期(didTimeout)
    if (prevScheduledCallback !== null) {
      isFlushingHostCallback = true;
      try {
        prevScheduledCallback(didTimeout);
      } finally {
        isFlushingHostCallback = false;
      }
    }
  };
 
  // 这里作为rAF的callback 处理函数
  /**
  * 在 animateTick 中,scheduler 将计算下一帧期望完成时间点 previousFrameTime,
  然后通过 port.postMessage 方法发送消息。等到 port1 接受到消息时,schdulear
  将 previousFrameTime 与 currentTime 作比较:当 previousFrameTime 小于等于 currentTime 时,
  scheduler 认为线程不是空闲的,对于超时的任务将立即执行,对于未超时的任务将在下次重绘后予以处理;
  当 previousFrameTime 大于 currentTime 时,线程就是空闲的,scheduler 将立即执行。这一处理机制在
  port1.onMessage 监听函数中实现(作为 macrotasks,port1 接受消息的时机将随着线程的空闲程度起变化)。
  
  */
  var animationTick = function (rafTime) {
    //轮询了 这里进入
    if (scheduledHostCallback !== null) {
      // Eagerly schedule the next animation callback at the beginning of the
      // frame. If the scheduler queue is not empty at the end of the frame, it
      // will continue flushing inside that callback. If the queue *is* empty,
      // then it will exit immediately. Posting the callback at the start of the
      // frame ensures it's fired within the earliest possible frame. If we
      // waited until the end of the frame to post the callback, we risk the
      // browser skipping a frame and not firing the callback until the frame
      // after that.
      /**
      * 
    * 最先在帧的开头安排下一个回调。如果调度程序队列在帧的末尾不为空,
    * 它将继续在该回调内刷新。如果队列*为*空,则它将立即退出
   *。在帧的开头触发回调可确保在最早的帧内触发。要是我们
   *等到帧结束后触发回调,我们冒着浏览器丢帧的风险,
    *并且在此帧之后的不会触发回调。
      */
      requestAnimationFrameWithTimeout(animationTick);
    } else {
      // No pending work. Exit.
      isAnimationFrameScheduled = false;
      return;
    }
   
    // 调度的时间rafTime - frameDeadline 下一帧预到期 + 一帧的多少  = 给下一帧留下的时间 
    var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
    
    // 帧的频率小于当前的 说明处理的时间都是比较短的
    // 其实这里做了下调整 如果当前的设备的更新频率大于我们设定的 30fps 
    // 我们就需要取更新的频率的最大值 这里的最大值的更新频率 最大值 
    // 我们需要澄清一个问题 频率越大 一帧花费的时间就越短 
    if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) {
      if (nextFrameTime < 8) {
        // 预防性代码  也就是说 不支持刷新频率大于120hz 如果大于120 就当120处理  也就是说 一帧只有8ms
        // Defensive coding. We don't support higher frame rates than 120hz.
        // If the calculated frame time gets lower than 8, it is probably a bug.
        nextFrameTime = 8;
      }
      // If one frame goes long, then the next one can be short to catch up.
      // If two frames are short in a row, then that's an indication that we
      // actually have a higher frame rate than what we're currently optimizing.
      // We adjust our heuristic dynamically accordingly. For example, if we're
      // running on 120hz display or 90hz VR display.
      // Take the max of the two in case one of them was an anomaly due to
      // missed frame deadlines.
      
      
     //如果一帧长,那么下一帧可能很短。
    //  如果两个帧连续短,那么这表明我们实际上具有比我们当前优化
    //的帧速率更高的帧速率。我们相应地动态调整启发式。例如,如果我们是
     // 在120hz显示屏或90hz VR显示屏上运行。
      // 取两个中的最大值,以防其中一个因错过帧截止日期而异常。
      activeFrameTime = nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
      
      
    } else {
      previousFrameTime = nextFrameTime;
    }
    
    // 计算下一帧过期时间
    frameDeadline = rafTime + activeFrameTime;
    // 如果还么发送消息 就触发message  让帧重绘完成后 进行调度callback
    if (!isMessageEventScheduled) {
      isMessageEventScheduled = true;
      port.postMessage(undefined);
    }
  };


  // 
  requestHostCallback = function (callback, absoluteTimeout) {
    // 设置当前的callback
    scheduledHostCallback = callback;
    // 设置过期时间
    timeoutTime = absoluteTimeout;
    //当前如果有任务正在执行中(意为当前没有重绘任务,重绘线程是空闲的)
    // 或者所添加的任务需要立即执行,scheduler 直接调用 port.postMessage 发送消息,跳过 rAF 
    // 轮询,以使任务得到即时执行
    if (isFlushingHostCallback || absoluteTimeout < 0) {
      // Don't wait for the next frame. Continue working ASAP, in a new event.
      port.postMessage(undefined);
    } else if (!isAnimationFrameScheduled) {
      // If rAF didn't already schedule one, we need to schedule a frame.
      // 如果raf 没有进行调度 安排一个新的 rAF轮询
      // 如果rAF 没有发挥作用 在使用settimeout 去作为预备去调度
      // TODO: If this rAF doesn't materialize because the browser throttles, we
      // might want to still have setTimeout trigger rIC as a backup to ensure
      // that we keep performing work.
      isAnimationFrameScheduled = true;
      
      //如果 rAF 轮询未启动,调用 requestAnimationFrameWithTimeout(animationTick) 启动轮询
      requestAnimationFrameWithTimeout(animationTick);
    }
  };

  cancelHostCallback = function () {
    scheduledHostCallback = null;
    isMessageEventScheduled = false;
    timeoutTime = -1;
  };
}
复制代码

7 执行到调度程序 callback 这里callback是怎么执行的(flushWork)

  • flushFirstCallback

flushFirstCallback 从双向链表中取出首个任务节点并执行。若首个任务节点的 callback 返回函数,使用该函数构建新的 callbackNode 任务节点,并将该任务节点插入双向链表中:若该任务节点的优先级最高、且不只包含一个任务节点,调用 ensureHostCallbackIsScheduled,在下一次重绘后酌情执行双向链表中的任务节点;否则只将新创建的任务节点添加到双向链表中


function flushFirstCallback() {
  var flushedNode = firstCallbackNode;

  // Remove the node from the list before calling the callback. That way the
  // list is in a consistent state even if the callback throws.
  var next = firstCallbackNode.next;
  if (firstCallbackNode === next) {
    // This is the last callback in the list.
    firstCallbackNode = null;
    next = null;
  } else {
    var lastCallbackNode = firstCallbackNode.previous;
    firstCallbackNode = lastCallbackNode.next = next;
    next.previous = lastCallbackNode;
  }

  flushedNode.next = flushedNode.previous = null;

  // Now it's safe to call the callback.
  var callback = flushedNode.callback;
  var expirationTime = flushedNode.expirationTime;
  var priorityLevel = flushedNode.priorityLevel;
  var previousPriorityLevel = currentPriorityLevel;
  var previousExpirationTime = currentExpirationTime;
  currentPriorityLevel = priorityLevel;
  currentExpirationTime = expirationTime;
  var continuationCallback;
  try {
    continuationCallback = callback();
  } finally {
    // 恢复当一次的优先级
    currentPriorityLevel = previousPriorityLevel;
    currentExpirationTime = previousExpirationTime;
  }

  // A callback may return a continuation. The continuation should be scheduled
  // with the same priority and expiration as the just-finished callback
  //. 如果callback 返回的还是 function 需要重新调度
  // 跟新加入一个节点是一样的 就不在分析了
  if (typeof continuationCallback === 'function') {
    var continuationNode = {
      callback: continuationCallback,
      priorityLevel: priorityLevel,
      expirationTime: expirationTime,
      next: null,
      previous: null
    };

    // Insert the new callback into the list, sorted by its expiration. This is
    // almost the same as the code in `scheduleCallback`, except the callback
    // is inserted into the list *before* callbacks of equal expiration instead
    // of after.
    if (firstCallbackNode === null) {
      // This is the first callback in the list.
      firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
    } else {
      var nextAfterContinuation = null;
      var node = firstCallbackNode;
      do {
        if (node.expirationTime >= expirationTime) {
          // This callback expires at or after the continuation. We will insert
          // the continuation *before* this callback.
          nextAfterContinuation = node;
          break;
        }
        node = node.next;
      } while (node !== firstCallbackNode);

      if (nextAfterContinuation === null) {
        // No equal or lower priority callback was found, which means the new
        // callback is the lowest priority callback in the list.
        nextAfterContinuation = firstCallbackNode;
      } else if (nextAfterContinuation === firstCallbackNode) {
        // The new callback is the highest priority callback in the list.
        firstCallbackNode = continuationNode;
        ensureHostCallbackIsScheduled();
      }

      var previous = nextAfterContinuation.previous;
      previous.next = nextAfterContinuation.previous = continuationNode;
      continuationNode.next = nextAfterContinuation;
      continuationNode.previous = previous;
    }
  }
}
复制代码
  • flushImmediateWork

基于 flushFirstCallback,flushImmediateWork 函数用于执行双向链表中所有优先级为 ImmediatePriority 的任务节点。如果双向链表不只包含优先级为 ImmediatePriority 的任务节点,flushImmediateWork 将调用 ensureHostCallbackIsScheduled 等待下次重绘后执行剩余的任务节点。

function flushImmediateWork() {
  if (
  // Confirm we've exited the outer most event handler
  // 确认我们退出了最外层的事件handler
  // 执行所有立即执行的callback 
  currentEventStartTime === -1 && firstCallbackNode !== null && firstCallbackNode.priorityLevel === ImmediatePriority) {
    isExecutingCallback = true;
    try {
      do {
        flushFirstCallback();
      } while (
      // Keep flushing until there are no more immediate callbacks
      firstCallbackNode !== null && firstCallbackNode.priorityLevel === ImmediatePriority);
    } finally {
      isExecutingCallback = false;
      
      // 还有其他优先级的  依次轮询调度
      if (firstCallbackNode !== null) {
        // There's still work remaining. Request another callback.
        ensureHostCallbackIsScheduled();
      } else {
        isHostCallbackScheduled = false;
      }
    }
  }
}
复制代码
  • flushWork

flushWork 作为 requestHostCallback 函数的参数,获得的首个实参 didTimeout 为是否超时的标识。如果超时,flushWork 通过调用 flushFirstCallback 批量执行所有未超时的任务节点;若果没有超时,flushWork 将在下一帧未完成前(通过 shouldYieldToHost 函数判断)尽可能地执行任务节点。等上述条件逻辑执行完成后,如果双向链表非空,调用 ensureHostCallbackIsScheduled 等待下次重绘后执行剩余的任务节点。特别的,当双向链表中还存在 ImmediatePriority 优先级的任务节点,flushWork 将调用 flushImmediateWork 批量执行这些任务节点。

function flushWork(didTimeout) {
  // Exit right away if we're currently paused
  // 暂停情况下 直接退出
  if (enableSchedulerDebugging && isSchedulerPaused) {
    return;
  }

  isExecutingCallback = true;
  var previousDidTimeout = currentDidTimeout;
  currentDidTimeout = didTimeout;
  try {
    // 如果已经超时
    if (didTimeout) {
      // Flush all the expired callbacks without yielding.
      // 让firstCallbackNode 双向链表去消耗
      while (firstCallbackNode !== null && !(enableSchedulerDebugging && isSchedulerPaused)) {
        // TODO Wrap in feature flag
        // Read the current time. Flush all the callbacks that expire at or
        // earlier than that time. Then read the current time again and repeat.
        // This optimizes for as few performance.now calls as possible.
        var currentTime = exports.unstable_now();
        
        // 已经过期的 直接执行
        if (firstCallbackNode.expirationTime <= currentTime) {
          do {
            flushFirstCallback();
          } while (firstCallbackNode !== null && firstCallbackNode.expirationTime <= currentTime && !(enableSchedulerDebugging && isSchedulerPaused));
          continue;
        }
        break;
      }
    } else {
      // Keep flushing callbacks until we run out of time in the frame.
      if (firstCallbackNode !== null) {
        do {
          if (enableSchedulerDebugging && isSchedulerPaused) {
            break;
          }
          flushFirstCallback();
          // 没有超时 也不用放入下一帧的的直接执行
        } while (firstCallbackNode !== null && !shouldYieldToHost());
      }
    }
  } finally {
    isExecutingCallback = false;
    currentDidTimeout = previousDidTimeout;
    // 没有处理玩的继续的执行
    if (firstCallbackNode !== null) {
      // There's still work remaining. Request another callback.
      ensureHostCallbackIsScheduled();
    } else {
      isHostCallbackScheduled = false;
    }
    // Before exiting, flush all the immediate work that was scheduled.
    // 退出之前将所有立即执行的任务去执行
    flushImmediateWork();
  }
}

复制代码

因为 scheduler 使用首个任务节点的超时时间点作为 requestHostCallback 函数的次参(在 ensureHostCallbackIsScheduled 函数中处理)。因此,如果首个任务节点的优先级为 ImmediatePriority,flushWork 所获得参数 didTimeout 也将是否值,其执行逻辑将是执行所有优先级为 ImmediatePriority 的任务节点,再调用 ensureHostCallbackIsScheduled 等待下一次重绘时执行其余任务节点。如果首个任务节点的优先级为 UserBlockingPriority 等,flushWork 将执行同优先级的任务节点,再调用 ensureHostCallbackIsScheduled 等待下一次重绘时执行其余任务节点。所有对不同优先级的任务节点,scheduler 采用分段执行的策略

8、 其他API

  • unstable_runWithPriority
function unstable_runWithPriority(priorityLevel, eventHandler) {
  switch (priorityLevel) {
    case ImmediatePriority:
    case UserBlockingPriority:
    case NormalPriority:
    case LowPriority:
    case IdlePriority:
      break;
    default:
      priorityLevel = NormalPriority;
  }

  var previousPriorityLevel = currentPriorityLevel;
  var previousEventStartTime = currentEventStartTime;
  currentPriorityLevel = priorityLevel;
  currentEventStartTime = getCurrentTime();

  try {
    return eventHandler();
  } finally {
    currentPriorityLevel = previousPriorityLevel;
    currentEventStartTime = previousEventStartTime;

    // Before exiting, flush all the immediate work that was scheduled.
    flushImmediateWork();
  }
}
复制代码

unstable_runWithPriority(priorityLevel, eventHandler) 将 currentPriorityLevel 缓存设置为 priorityLevel,随后再执行 eventHandler,最后调用 flushImmediateWork 函数执行所有优先级为 ImmediatePriority 的任务节点,其余任务节点等待下次重绘后再执行。可以设想,当 eventHandler 为 unstable_scheduleCallback 函数时,将影响所添加任务节点的优先级,并立即执行 ImmediatePriority 优先级的任务。其实就是给执行eventHandler 设置优先级

  • unstable_wrapCallback
function unstable_wrapCallback(callback) {
  var parentPriorityLevel = currentPriorityLevel;
  return function () {
    // This is a fork of runWithPriority, inlined for performance.
    var previousPriorityLevel = currentPriorityLevel;
    var previousEventStartTime = currentEventStartTime;
    currentPriorityLevel = parentPriorityLevel;
    currentEventStartTime = exports.unstable_now();

    try {
      return callback.apply(this, arguments);
    } finally {
      currentPriorityLevel = previousPriorityLevel;
      currentEventStartTime = previousEventStartTime;
      flushImmediateWork();
    }
  };
}
复制代码

unstable_wrapCallback(callback) 记录当前的优先级 currentPriorityLevel,返回函数处理效果如 unstable_runWithPriority,对于 callback 中新添加的任务节点将使用所记录的 currentPriorityLevel 作为优先级。 这里可以返回的是function 将作为新的节点去插入被调度

9 其他

  • unstable_pauseExecution 通过将 isSchedulerPaused 置为 true,打断 scheduler 处理任务节点。
  • unstable_continueExecution 取消打断状态,使 scheduler 恢复处理任务节点。
  • unstable_getFirstCallbackNode 获取双向链表中的首个任务节点。
  • unstable_cancelCallback(callbackNode) 从双向链表中移除指定任务节点。
  • unstable_getCurrentPriorityLevel 获取当前优先级 currentPriorityLevel 缓存。
  • unstable_shouldYield 是否需要被打断。
  • unstable_now 获取当前时间。

10 总结

读完scheduler源码 感觉还是挺复杂的 当然收获也是比较大的 尤其是对于浏览执行机制有了更深入的认识 尤其调度思路让人影响时刻, 当然分析肯定会有不全面或者偏差的地方 欢迎大佬们指正

转载于:https://juejin.im/post/5c89de97f265da2dba0266cf

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值