React源码分析=; scheduler分析

Posted 刘翾

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React源码分析=; scheduler分析相关的知识,希望对你有一定的参考价值。

文章目录

1. 前言

为了读代码更加有效率,提前看了一篇如何阅读源码的文章:https://zxc0328.github.io/2018/05/01/react-source-reading-howto/
因此此次本人阅读源码主要想看懂以下6个问题:

  1. ReactDOM.render()是如何挂载到真实DOM上的
  2. setState实现原理,为什么是异步的
  3. 生命周期结合2号问题一起看
  4. react16的fiber架构是什么
  5. jsx是如何解析的
  6. react hook是如何做到的

分析代码基于React V16.8.6,react源码目录截图如下图所示:

所需要看的代码一个库就够了,https://github.com/facebook/react/


先看的react-dom代码, 一点点单步到了scheduler, 这个包的代码看起来不多(可能是我第一次看框架源码, 看的有点恶心… 一堆全局变量, 各个函数来回调用, 看了好几天, 如果下面哪里有问题, 还请各位同行指点), 那就先来梳理下这个包吧.

scheduler这个包主要是在react做diff做任务分配机制, 核心机制类似于requestidlecallback,

window.requestIdleCallback()会在浏览器空闲时期依次调用函数, 这就可以让开发者在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这样延迟敏感的事件产生影响。

但这个函数支持度太惨

react则使用requestAnimationFramepostMessage来模拟实现的requestidlecallback. 工作原理是调度requestAnimationFrame,存储帧开始的时间,然后调度postMessage,后者在绘制后进行调度。

该包主要流程是把所有任务通过双向链表连接起来, 通过requestAnimationFrame来在浏览器每帧的空闲时间循环处理所有任务, 直到链表为空为止.


2. getCurrentTime

这个函数后面会经常用到的, 先到前面来说下, 先看代码:

// packages\\scheduler\\src\\forks\\SchedulerHostConfig.default.js
const hasNativePerformanceNow =
  typeof performance === 'object' && typeof performance.now === 'function';
const localDate = Date;

if (hasNativePerformanceNow) 
  const Performance = performance;
  getCurrentTime = function() 
    return Performance.now();
  ;
 else 
  getCurrentTime = function() 
    // 该方法在 ECMA-262 第五版中被标准化, Date.now() === new Date().getTime();
    // 出处 https://developer.mozilla.org/zh-CN/docs/Web/javascript/Reference/Global_Objects/Date/now#Compatibility
    return localDate.now();
  ;

performance.now()Date.now() 不同的是,返回了以微秒(百万分之一秒)为单位的时间,更加精准。

并且与 Date.now() 会受系统程序执行阻塞的影响不同,performance.now() 的时间是以恒定速率递增的,不受系统时间的影响(系统时间可被人为或软件调整)。

注意Date.now()输出的是 UNIX 时间,即距离 1970 的时间,而performance.now()输出的是相对于 time origin(页面初始化: https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#The_time_origin) 的时间。

使用 Date.now() 的差值并非绝对精确,因为计算时间时受系统限制(可能阻塞)。但使用 performance.now() 的差值,并不影响我们计算程序执行的精确时间。

3. unstable_scheduleCallback函数

  • 函数前面的unstable表示不稳定的意思, 之后还会有变动.
  • 这个方法主要就是将任务组成双向链表, 并按照过期时间作为优先级.

先定义优先级, 代码如下:

// packages\\scheduler\\src\\Scheduler.js

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// 这是32位系统V8引擎里最大的整数
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
var maxSigned31BitInt = 1073741823;
// Times out immediately 立即过期
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY = maxSigned31BitInt;

主函数代码如下:

// packages\\scheduler\\src\\Scheduler.js

// 组成双向链表, 开始安排任务
function unstable_scheduleCallback(priorityLevel,  callback,  deprecated_options) 
  var startTime =
    currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();

  // 过期时间 = 加入时间 + 优先级时间
  var expirationTime;
  if (
    typeof deprecated_options === 'object' &&
    deprecated_options !== null &&
    typeof deprecated_options.timeout === 'number'
  ) 
    expirationTime = startTime + deprecated_options.timeout;
   else 
    // 根据不同的优先级, 赋予不同的过期时间
    switch (priorityLevel) 
      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;
    
  
  
// 未完

上面先计算一下callback的过期时间, 接下来创建链表节点, 并组成链表, 代码如下:

// packages\\scheduler\\src\\Scheduler.js

// 续上
// 基于上面的优先级和过期时间创建一个节点
var newNode = 
    callback,
    priorityLevel: priorityLevel,
    expirationTime,
    next: null,
    previous: null,
  ;

  if (firstCallbackNode === null) 
    // This is the first callback in the list. 如果firstCallbackNode没有, 说明是第一个节点
    firstCallbackNode = newNode.next = newNode.previous = newNode;
    scheduleHostCallbackIfNeeded(); // 之后再说, 先忽略
   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;
      scheduleHostCallbackIfNeeded();
    

    var previous = next.previous;
    previous.next = next.previous = newNode;
    newNode.next = next;
    newNode.previous = previous;
  

  return newNode;

这部分当初第一次看的时候也看了很久, 看明白之后才发现原来创建双向链表居然这么简单, 就是有点绕, 双向链表的定义:

双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
https://baike.baidu.com/item/双向链表/2968731?fr=aladdin

这部分创建初始双链表其实就只有用了一句话firstCallbackNode = newNode.next = newNode.previous = newNode;, 从右向左赋值, 也就是说这段代码中用的变量全都指向newNode这个对象的地址.


4. scheduleHostCallbackIfNeeded

// packages\\scheduler\\src\\Scheduler.js
// This is set while performing work, to prevent re-entrancy.
var isPerformingWork = false;
var isHostCallbackScheduled = false;

function scheduleHostCallbackIfNeeded() 
  if (isPerformingWork) 
    // Don't schedule work yet; wait until the next time we yield.
    // 有个work正在执行
    return;
  

  if (firstCallbackNode !== null) 
    // Schedule the host callback using the earliest expiration in the list.
    // firstCallbackNode的过期时间是最早的
    var expirationTime = firstCallbackNode.expirationTime;
    if (isHostCallbackScheduled) 
      // Cancel the existing host callback.
      // 取消存在的回调函数
      cancelHostCallback();
     else 
      isHostCallbackScheduled = true;
    
    requestHostCallback(flushWork, expirationTime);
  

requestHostCallback这个函数内部主要是通过本文开头所讲的requestAnimationFramepostMessage来模拟实现的requestidlecallback调度任务执行这个flushWork, 那么我们先来看下这个flushWork.

4.1. flushWork

这个函数的参数didUserCallbackTimeout只会有两种情况:

  1. 当前帧没过期;didUserCallbackTimeout = false
  2. 当前帧过期且当前任务过期didUserCallbackTimeout = true
// packages\\scheduler\\src\\Scheduler.js

function flushWork(didUserCallbackTimeout) 
  // Exit right away if we're currently paused
  if (enableSchedulerDebugging && isSchedulerPaused) 
    return;
  

  // We'll need a new host callback the next time work is scheduled.
  // 安排callback完成了
  isHostCallbackScheduled = false;

  isPerformingWork = true;
  const previousDidTimeout = currentHostCallbackDidTimeout;
  currentHostCallbackDidTimeout = didUserCallbackTimeout;
  try 
    if (didUserCallbackTimeout) 
      // 当前帧过期且当前任务过期
      // Flush all the expired callbacks without yielding.
      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 = getCurrentTime();
        if (firstCallbackNode.expirationTime <= currentTime) 
          do 
            flushFirstCallback(); // 见4.2节
           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(); // 见4.2节
         while (firstCallbackNode !== null && !shouldYieldToHost());
      
    
   finally 
    isPerformingWork = false;
    currentHostCallbackDidTimeout = previousDidTimeout;
    // There's still work remaining. Request another callback.
    scheduleHostCallbackIfNeeded();
  

这部分主要基于当前帧以及当前任务过期时间来决定是否执行flushFirstCallback函数, 这个函数就是最终的执行任务函数.

4.2. flushFirstCallback

这个函数比较长, 主要功能为: 执行队首任务并把队首任务从链表移除,把第二个任务置为队首任务。执行任务可能产生新的任务,再把新任务插入到任务链表中. 见下方代码:

// packages\\scheduler\\src\\Scheduler.js

// 执行任务, 并更新链表
function flushFirstCallback() 
  const currentlyFlushingCallback = 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.
  // 在执行callback之前, 最好先把这个节点从列表中删掉
  var next = firstCallbackNode.next;
  if (firstCallbackNode === next) 
    // This is the last callback in the list.
    // 由于是双向链表, 这种情况就是现在只剩一个节点了
    // 全部设置为null
    firstCallbackNode = null;
    next = null;
   else 
    // 链表中删除firstCallbackNode
    var lastCallbackNode = firstCallbackNode.previous;
    firstCallbackNode = lastCallbackNode.next = next;
    next.previous = lastCallbackNode;
  

  // 从链表中彻底剥离, 把原对象引用置空
  currentlyFlushingCallback.next = currentlyFlushingCallback.previous = null;

  // Now it's safe to call the callback.
  // 获取属性
  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 ||
      // Immediate priority callbacks are always called as if they timed out
      priorityLevel === ImmediatePriority;
    continuationCallback = callback(didUserCallbackTimeout);
   catch (error) 
    throw error;
   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.
  // 如果返回的是函数, 则和刚刚完成的回调函数具有相同的过期时间和优先级
  if (typeof continuationCallback === 'function') 
    var continuationNode: CallbackNode = 
      callback: continuationCallback,
      priorityLevel,
      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.
    // 下面和scheduleCallback函数一个逻辑, 只有一点不同, `callback`在等于到期时间的回调之前插入到列表中而不是插入到之后
    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.
        // 没有找到相等或者低优先级的callback, 因此放到第一个
        nextAfterContinuation = firstCallbackNode;
       else if (nextAfterContinuation === firstCallbackNode) 
        // The new callback is the highest priority callback in the list.
        firstCallbackNode = continuationNode;
        scheduleHostCallbackIfNeeded();
      

      // 插入操作
      var previous = nextAfterContinuation.previous;
      previous.next = nextAfterContinuation.previous = continuationNode;
      continuationNode.next = nextAfterContinuation;
      continuationNode.previous = previous;
    
  


5. requestHostCallback

接下来我们最后再看下这个函数, 代码如下:

// packages\\scheduler\\src\\forks\\SchedulerHostConfig.default.js

  // absoluteTimeout => 链表node的过期时间(expirationTime)
  // callback => flushWork函数
  requestHostCallback = function(callback, absoluteTimeout) 
    scheduledHostCallback = callback;
    timeoutTime = absoluteTimeout;
    if (isFlushingHostCallback || absoluteTimeout < 0) 
      // Don't wait for the next frame. Continue working ASAP, in a new event.
      // 如果absoluteTimeout时间小于1, 则此次work为ImmediatePriority优先级
      // 应该立即执行
      port.postMessage(undefined);
     else if (!isAnimationFrameScheduled) 
      // If rAF didn't already schedule one, we need to schedule a frame.
      // 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;
      requestAnimationFrameWithTimeout(animationTick); // animationTick为一个函数, 见5.2节
    
  ;

这部分没多少代码

5.1. requestAnimationFrameWithTimeout

我们接下来看下requestAnimationFrameWithTimeout函数, 代码如下:

// packages\\scheduler\\src\\forks\\SchedulerHostConfig.default.js

// 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.

// requestAnimationFrame在切换tab之后不再运行,如果切换tab之后
// 我们还想让他在后台继续运行,应使用setTimeout作为兜底操作
const ANIMATION_FRAME_TIMEOUT = 100;
let rAFID;
let rAFTimeoutID;
const requestAnimationFrameWithTimeout = function(callback) 
  // schedule rAF and also a setTimeout
  rAFID = localRequestAnimationFrame(function(timestamp) 
    // timestamp 实际上就是performance.now()
    // cancel the setTimeout, 如果RAF好使就不使用Timeout
    // callback实际就是下面的animationTick函数, 见5.2节
    localClearTimeout(rAFTimeoutID);
    callback(timestamp); // callback就是animationTick函数, 见5.2小节
  );
  rAFTimeoutID = localSetTimeout(function() 
    // cancel the requestAnimationFrame
    localCancelAnimationFrame(rAFID);
    callback(getCurrentTime());
  , ANIMATION_FRAME_TIMEOUT);
;

5.2. animationTick

接着上面那个callback(timestamp);

React默认使用30fps运行, 在运行时每次都会比对前一次帧运行时间和下次帧运行时间, 来动态调整至合适数值.

  let frameDeadline = 0;
  let previousFrameTime = 33;
  let activeFrameTime = 33;
  
  const animationTick = function(rafTime) 
  // scheduledHostCallback 就是flushWork函数
    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.
      // 在当前的帧的开始安排下一个animation callback
      // 如果任务队列在当前帧不为空, 会继续执行下去, 如果队列为空了, 会立即退出
      requestAnimationFrameWithTimeout(animationTick);
     else 
      // No pending work. Exit.
      isAnimationFrameScheduled = false;
      return;
    

    // rafTime raf执行时的时间
    let nextFrameTime = rafTime - frameDeadline + activeFrameTime;
    if (
      nextFrameTime < activeFrameTime &&
      previousFrameTime < activeFrameTime &&
      !fpsLocked // 如果没有手动把fps数值死的话
    ) 
      if (nextFrameTime < 8) 
        // 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.
        // 不支持比120hz更好的帧率
        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.
      // 如果两次帧时间都比较短, 调整当前帧率, 取这两次中大的那个
      activeFrameTime =
        nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
     else 
      previousFrameTime = nextFrameTime;
    
    frameDeadline = rafTime + activeFrameTime;
    if (!isMessageEventScheduled) 
      isMessageEventScheduled = true;
      // postMessage触发任务链表队列, 见下节5.3.
      port.postMessage(undefined);
    
  ;

5.3. port.postMessage

这个函数就是调用flushWork, 并传入帧是否过期的地方, 看下方代码:

  // We use the postMessage trick to defer idle work until after the repaint.
  // 使用postMessage来将工作推迟到重绘之后
  const channel = new MessageChannel();
  // Channel Messaging API的Channel Messaging接口允许我们创建一个新的消息通道,并通过它的两个MessagePort 属性(port1, port2)发送数据。
  
  const port = channel.port2;
  channel.port1.onmessage = function(event) 
    isMessageEventScheduled = false;

    const prevScheduledCallback = scheduledHostCallback;
    const prevTimeoutTime = timeoutTime;
    scheduledHostCallback = null;
    timeoutTime = -1;

    const currentTime = getCurrentTime();

    let didTimeout = false以上是关于React源码分析=; scheduler分析的主要内容,如果未能解决你的问题,请参考以下文章

Spark源码分析之-scheduler模块

原创k8s源码分析-----kube-scheduler

小记--------spark资源调度机制源码分析-----Schedule

JStorm与Storm源码分析--Scheduler,调度器

kube-scheduler 磁盘调度源码分析

Scheduling restart of crashed service解决方案与源码分析