1

[email protected]源码阅读(一)Scheduler

 3 years ago
source link: https://segmentfault.com/a/1190000040173121
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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会非常简单粗暴放弃掉之前完成到一半的更新,等待后面调度重头再开始

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK