关于javascript:React1686源码阅读一Scheduler

11次阅读

共计 3830 个字符,预计需要花费 10 分钟才能阅读完成。

开篇

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 会非常简单粗犷放弃掉之前实现到一半的更新,期待前面调度重头再开始

正文完
 0