React@16.8.6源码阅读Scheduler

Posted tain335

tags:

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

开篇

React也使用了好一段时间,最近才有空把源码阅读一下(真是惭愧),因为项目用的React版本比较老,所以找的版本也就对应项目使用的版本刚好是16.8.6,不过React源码分析的文章已经很多了,而且有很多也质量很高,所以这里仅仅当做自己的笔记作为记录吧。

问题

在还没接触React的时候就已经了解到React16的一些新特性:协程,分片等,那个时候刚好接触到go语言对React的协程一直有几个疑问,会不会跟go语言的协程一样有调度器,React的调度单位是怎样的,Fiber是怎样抢占,中断和恢复执行的?

Scheduler

React的调度器其实代码量并不多,仅仅只有700多行,然后核心功能就是以下两点:

  1. 利用requestAnimationFrame来动态计算每帧的时间
  2. 利用MessageChannel来创建宏任务,来执行调度的任务

Scheduler目的是为了让任务都在每一帧的Idle阶段来执行,利用的是每帧空闲时间,而不阻塞浏览器的布局和绘制;
那么为什么不在requestAnimationFrame阶段来执行尼?我们都知道raf会在浏览器布局和绘制之前执行,但React是根本不知道浏览器接着后面布局和绘制需要消耗多少时间,所以在raf阶段处理是很难估计该预留多少时间自己去执行,然后让回给浏览器。

那么为什么不使用requestIdleCallback来控制在每帧的Idle阶段来执行尼?一开始React确实是这么干,但是后面因为requestIdleCallback的一些问题,而且新的api也有兼容性问题。

那么现在的新办法是如何处理的尼,首先用requestAnimationFrame先触发一个anmiationTick,这里有两个作用:第一可以预估每帧大概的时间;第二等anmiationTick触发时再用postMessage触发一个宏任务,这样这个宏任务就会在浏览器的布局和绘制之后执行,等同于在idle阶段执行了,当然这个宏任务里面还需要判断当前帧时间是否没有了(但是如果任务已经超时了不管还有没有时间剩下也是会执行的),这个判断就利用第一点获取的帧时间来进行的,如果没有剩余时间了就再触发一次animationTick,重复一次整个过程。

而每个调度的任务都会带有一个优先级priorityLevel,这个优先级是指:

var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;

这个优先级很重要,涉及到当前任务和这个任务执行时派生的任务的超时时间计算。

另外一些杂七杂八的点:

  1. requestAnimationTime有个缺点就是页面被隐藏的时候,有可能不执行,所以React采用了一个处理办法:

    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(getCurrentTime());
      }, ANIMATION_FRAME_TIMEOUT);
    };

    利用setTimeout来兜底,这样就万无一失了。

  2. 而判断任务是否要过期,就要不停使用peformance.now/Date.now来获取当前时间,而获取当前时间一般也是一个系统调用,频繁调用也是一种消耗;所以会利用timeoutTime来记录最近调度的一个任务的超时时间,执行的时候如果判断已经过期,则认为调度的任务列表里面存在过期任务,先把所有的过期任务清理完,所以整个过程只需要获取一次当前时间就可以了,减少获取当前时间的消耗。

与React结合

React的Scheduler算是一个独立的包,完全没有包含React其他内容,所以也很难回答我开头的疑问,究竟它的调度单位是什么,Fiber是怎样抢占和恢复的。
直接来到ReactFiberScheduler.js,scheduleWork方法:

function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
  const root = scheduleWorkToRoot(fiber, expirationTime); // 1
  if (
    !isWorking &&
    nextRenderExpirationTime !== NoWork &&
    expirationTime > nextRenderExpirationTime
  ) {
    resetStack(); // 2
  }
  markPendingPriorityLevel(root, expirationTime); // 3
  if (
    !isWorking ||
    isCommitting ||
    nextRoot !== root
  ) { // 4
    const rootExpirationTime = root.expirationTime;
    requestWork(root, rootExpirationTime);
  }
}

分四步来分析这个方法:

  1. 首先更新current树和workInProgress(如果存在)树fiber节点的childExpirationTime,然后返回root;这里简单说明一下,current和workInProgress树,current树就代表着当前显示在用户面前的fiber节点树,workInProgress树就是正在做更新的fiber节点树,毕竟现在的React是diff过程已经可以异步的,如果只有一棵树,那就很有可能出现更新到一半就显示给用户了,综合起来这种也算是游戏中常用的双缓冲技术的应用;而childExpirationTime代表的是子级节点中最高的优先级,可以用在后面更新的时候快速判断子级节点需不需要更新,因为每次调度更新的时候,都是从ReactFiberRoot往下遍历,所以这个属性就很重要了,可以提高效率。
  2. 如果不在更新过程中,出现了一种优先级更高的更新任务,也就是抢占,这个时候会重置执行栈,之前更新到一半的工作结果都会被抛弃,等下次调度重新开始。
  3. 标记ReactFiberRoot的优先级,在我一开始的源码阅读中,我一开始简单认为expirationTime就是超时时间,实际上还包含优先级的意思,而且源码中更多时候代表的是优先级,越往前调度的任务优先级越高,越往后就越低,高于当前的帧的deadline,都表示这些任务是过期任务,过期任务哪怕当前帧时间不够都会全部调度执行。而ReactFiberRoot上会有好几个字段跟优先级相关:

    earliestPendingTime
    latestPendingTime
    
    earliestSuspendedTime
    latestSuspendedTime
    
    latestPingedTime
    
    nextExpirationTimeToWorkOn
    expirationTime

    开头那5兄弟一开始真的让我感觉有点懵逼,一开始完全不知道为什么需要5个字段来标记优先级,在我认知里面每个节点仅仅需要一个expirationTime标记自身的优先级和childExpirationTime标记子级最高的优先级就足够了;但是后面多阅读几遍代码就发现它的意图,在这些优先级里面也是有分类的:Pending > Pinged > Suspended;React总是会先把Pending优先级任务清理完才会清理后面的任务,而Pending优先级代表的是还没有执行过的任务。
    而nextExpirationTimeToWorkOn和expirationTime一般情况下它们是相等,但是还有其他情况是不一样(就是处理Suspended类型优先级的时候),nextExpirationTimeToWorkOn代表的是准备处理的优先级,大于或者等于这个优先级的fiber节点都会得到处理;expirationTime当然代表的是root整体的优先级,会用来跟其他root来比较,看谁应该更优先处理。
    不过总的来说应该是React为了支持Suspend这个特性引入的复杂度,当然复杂度还不只这里,如果把Suspend相关的代码去掉,整体会很清爽,Suspend这个特性是否有这么大的价值,在后面的章节再具体分析一下。

  4. 如果不在更新过程中,或者这个Root跟当前调度的Root不一样,把这个Root也加入到调度队列里面,如果优先级比当前调度的Root更高,就会请求一次新的调度。

个人总结

  1. React利用Scheduler寻找一个合适的执行时机
  2. 在这个合适的时机里面ReactFiberRoot就是它的调度单位
  3. 如果被更高优先级的任务打断,React会非常简单粗暴放弃掉之前完成到一半的更新,等待后面调度重头再开始

以上是关于React@16.8.6源码阅读Scheduler的主要内容,如果未能解决你的问题,请参考以下文章

apm飞控多线程源码阅读笔记

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

Spark源码分析之-scheduler模块

react-router5.x 的配置及其页面跳转方法和js跳转方法

react源码解析15.scheduler&Lane

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