scheduler 源码

为何是要有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(废弃), MutationObservernode

[引自大神论述]react



上图来自知乎文章chrome

  • 主线程负责解析编译和调度异步事件循环调度
  • 异步队列和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来polyfillapp

requestIdleCallback Polyfill方案

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


全部准备工做都作好了, 接下来咱们逐步来分析Scheduler源码ide

一、 调度基本常量定义

// 枚举

// 当即执行的任务
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;

二、 调度所需变量的定义

// 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';

三、 调度方法 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);
}

五、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();
  };
}

六、调度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 采用分段执行的策略

八、 其余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源码 感受仍是挺复杂的 固然收获也是比较大的 尤为是对于浏览执行机制有了更深刻的认识 尤为调度思路让人影响时刻, 固然分析确定会有不全面或者误差的地方 欢迎大佬们指正函数

相关文章
相关标签/搜索