react源码解析15.scheduler&lane(代码片段)

全栈潇晨 全栈潇晨     2022-11-30     473

关键词:

react源码解析15.scheduler&Lane

视频课程(高效学习):进入课程

课程目录:

1.开篇介绍和面试题

2.react的设计理念

3.react源码架构

4.源码目录结构和调试

5.jsx&核心api

6.legacy和concurrent模式入口函数

7.Fiber架构

8.render阶段

9.diff算法

10.commit阶段

11.生命周期

12.状态更新流程

13.hooks源码

14.手写hooks

15.scheduler&Lane

16.concurrent模式

17.context

18事件系统

19.手写迷你版react

20.总结&第一章的面试题解答

21.demo

当我们在类似下面的搜索框组件进行搜索时会发现,组件分为搜索部分和搜索结果展示列表,我们期望输入框能立刻响应,结果列表可以有等待的时间,如果结果列表数据量很大,在进行渲染的时候,我们又输入了一些文字,因为用户输入事件的优先级是很高的,所以就要停止结果列表的渲染,这就引出了不同任务之间的优先级和调度

react源码15.5

Scheduler

我们知道如果我们的应用占用较长的js执行时间,比如超过了设备一帧的时间,那么设备的绘制就会出不的现象。

Scheduler主要的功能是时间切片和调度优先级,react在对比差异的时候会占用一定的js执行时间,Scheduler内部借助MessageChannel实现了在浏览器绘制之前指定一个时间片,如果react在指定时间内没对比完,Scheduler就会强制交出执行权给浏览器

react源码15.3

时间切片

​ 在浏览器的一帧中js的执行时间如下

react源码15.1

​ requestIdleCallback是在浏览器重绘重排之后,如果还有空闲就可以执行的时机,所以为了不影响重绘重排,可以在浏览器在requestIdleCallback中执行耗性能的计算,但是由于requestIdleCallback存在兼容和触发时机不稳定的问题,scheduler中采用MessageChannel来实现requestIdleCallback,当前环境不支持MessageChannel就采用setTimeout。

​ 在之前的介绍中我们知道在performUnitOfWork之后会执行render阶段和commit阶段,如果在浏览器的一帧中,cup的计算还没完成,就会让出js执行权给浏览器,这个判断在workLoopConcurrent函数中,shouldYield就是用来判断剩余的时间有没有用尽。在源码中每个时间片时5ms,这个值会根据设备的fps调整。

function workLoopConcurrent() 
  while (workInProgress !== null && !shouldYield()) 
    performUnitOfWork(workInProgress);
  

function forceFrameRate(fps) //计算时间片
  if (fps < 0 || fps > 125) 
    console[\'error\'](
      \'forceFrameRate takes a positive int between 0 and 125, \' +
        \'forcing frame rates higher than 125 fps is not supported\',
    );
    return;
  
  if (fps > 0) 
    yieldInterval = Math.floor(1000 / fps);
   else 
    yieldInterval = 5;//时间片默认5ms
  

任务的暂停

在shouldYield函数中有一段,所以可以知道,如果当前时间大于任务开始的时间+yieldInterval,就打断了任务的进行。

//deadline = currentTime + yieldInterval,deadline是在performWorkUntilDeadline函数中计算出来的
if (currentTime >= deadline) 
  //...
	return true

调度优先级

​ 在Scheduler中有两个函数可以创建具有优先级的任务

  • runWithPriority:以一个优先级执行callback,如果是同步的任务,优先级就是ImmediateSchedulerPriority

    function unstable_runWithPriority(priorityLevel, eventHandler) 
      switch (priorityLevel) //5种优先级
        case ImmediatePriority:
        case UserBlockingPriority:
        case NormalPriority:
        case LowPriority:
        case IdlePriority:
          break;
        default:
          priorityLevel = NormalPriority;
      
    
      var previousPriorityLevel = currentPriorityLevel;//保存当前的优先级
      currentPriorityLevel = priorityLevel;//priorityLevel赋值给currentPriorityLevel
    
      try 
        return eventHandler();//回调函数
       finally 
        currentPriorityLevel = previousPriorityLevel;//还原之前的优先级
      
    
    
  • scheduleCallback:以一个优先级注册callback,在适当的时机执行,因为涉及过期时间的计算,所以scheduleCallback比runWithPriority的粒度更细。

    • 在scheduleCallback中优先级意味着过期时间,优先级越高priorityLevel就越小,过期时间离当前时间就越近,var expirationTime = startTime + timeout;例如IMMEDIATE_PRIORITY_TIMEOUT=-1,那var expirationTime = startTime + (-1);就小于当前时间了,所以要立即执行。

    • scheduleCallback调度的过程用到了小顶堆,所以我们可以在O(1)的复杂度找到优先级最高的task,不了解可以查阅资料,在源码中小顶堆存放着任务,每次peek都能取到离过期时间最近的task。

    • scheduleCallback中,未过期任务task存放在timerQueue中,过期任务存放在taskQueue中。

      ​ 新建newTask任务之后,判断newTask是否过期,没过期就加入timerQueue中,如果此时taskQueue中还没有过期任务,timerQueue中离过期时间最近的task正好是newTask,则设置个定时器,到了过期时间就加入taskQueue中。

      ​ 当timerQueue中有任务,就取出最早过期的任务执行。

function unstable_scheduleCallback(priorityLevel, callback, options) 
  var currentTime = getCurrentTime();

  var startTime;//开始时间
  if (typeof options === \'object\' && options !== null) 
    var delay = options.delay;
    if (typeof delay === \'number\' && delay > 0) 
      startTime = currentTime + delay;
     else 
      startTime = currentTime;
    
   else 
    startTime = currentTime;
  

  var timeout;
  switch (priorityLevel) 
    case ImmediatePriority://优先级越高timeout越小
      timeout = IMMEDIATE_PRIORITY_TIMEOUT;//-1
      break;
    case UserBlockingPriority:
      timeout = USER_BLOCKING_PRIORITY_TIMEOUT;//250
      break;
    case IdlePriority:
      timeout = IDLE_PRIORITY_TIMEOUT;
      break;
    case LowPriority:
      timeout = LOW_PRIORITY_TIMEOUT;
      break;
    case NormalPriority:
    default:
      timeout = NORMAL_PRIORITY_TIMEOUT;
      break;
  

  var expirationTime = startTime + timeout;//优先级越高 过期时间越小

  var newTask = //新建task
    id: taskIdCounter++,
    callback//回调函数
    priorityLevel,
    startTime,//开始时间
    expirationTime,//过期时间
    sortIndex: -1,
  ;
  if (enableProfiling) 
    newTask.isQueued = false;
  

  if (startTime > currentTime) //没有过期
    newTask.sortIndex = startTime;
    push(timerQueue, newTask);//加入timerQueue
    //taskQueue中还没有过期任务,timerQueue中离过期时间最近的task正好是newTask
    if (peek(taskQueue) === null && newTask === peek(timerQueue)) 
      if (isHostTimeoutScheduled) 
        cancelHostTimeout();
       else 
        isHostTimeoutScheduled = true;
      
      //定时器,到了过期时间就加入taskQueue中
      requestHostTimeout(handleTimeout, startTime - currentTime);
    
   else 
    newTask.sortIndex = expirationTime;
    push(taskQueue, newTask);//加入taskQueue
    if (enableProfiling) 
      markTaskStart(newTask, currentTime);
      newTask.isQueued = true;
    
    if (!isHostCallbackScheduled && !isPerformingWork) 
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);//执行过期的任务
    
  

  return newTask;

react源码15.2

任务暂停之后怎么继续

​ 在workLoop函数中有这样一段

const continuationCallback = callback(didUserCallbackTimeout);//callback就是调度的callback
currentTime = getCurrentTime();
if (typeof continuationCallback === \'function\') //判断callback执行之后的返回值类型
  currentTask.callback = continuationCallback;//如果是function类型就把又赋值给currentTask.callback
  markTaskYield(currentTask, currentTime);
 else 
  if (enableProfiling) 
    markTaskCompleted(currentTask, currentTime);
    currentTask.isQueued = false;
  
  if (currentTask === peek(taskQueue)) 
    pop(taskQueue);//如果是function类型就从taskQueue中删除
  

advanceTimers(currentTime);

​ 在performConcurrentWorkOnRoot函数的结尾有这样一个判断,如果callbackNode等于originalCallbackNode那就恢复任务的执行

if (root.callbackNode === originalCallbackNode) 
  // The task node scheduled for this root is the same one that\'s
  // currently executed. Need to return a continuation.
  return performConcurrentWorkOnRoot.bind(null, root);

Lane

​ Lane的和Scheduler是两套优先级机制,相比来说Lane的优先级粒度更细,Lane的意思是车道,类似赛车一样,在task获取优先级时,总是会优先抢内圈的赛道,Lane表示的优先级有以下几个特点。

  • 可以表示不同批次的优先级

    ​ 从代码中中可以看到,每个优先级都是个31位二进制数字,1表示该位置可以用,0代表这个位置不能用,从第一个优先级NoLanes到OffscreenLane优先级是降低的,优先级越低1的个数也就越多(赛车比赛外圈的车越多),也就是说含多个1的优先级就是同一个批次。

    export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
    export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;
    
    export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;
    export const SyncBatchedLane: Lane = /*                 */ 0b0000000000000000000000000000010;
    
    export const InputDiscreteHydrationLane: Lane = /*      */ 0b0000000000000000000000000000100;
    const InputDiscreteLanes: Lanes = /*                    */ 0b0000000000000000000000000011000;
    
    const InputContinuousHydrationLane: Lane = /*           */ 0b0000000000000000000000000100000;
    const InputContinuousLanes: Lanes = /*                  */ 0b0000000000000000000000011000000;
    
    export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000100000000;
    export const DefaultLanes: Lanes = /*                   */ 0b0000000000000000000111000000000;
    
    const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000001000000000000;
    const TransitionLanes: Lanes = /*                       */ 0b0000000001111111110000000000000;
    
    const RetryLanes: Lanes = /*                            */ 0b0000011110000000000000000000000;
    
    export const SomeRetryLane: Lanes = /*                  */ 0b0000010000000000000000000000000;
    
    export const SelectiveHydrationLane: Lane = /*          */ 0b0000100000000000000000000000000;
    
    const NonIdleLanes = /*                                 */ 0b0000111111111111111111111111111;
    
    export const IdleHydrationLane: Lane = /*               */ 0b0001000000000000000000000000000;
    const IdleLanes: Lanes = /*                             */ 0b0110000000000000000000000000000;
    
    export const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;
    
  • 优先级的计算的性能高

    ​ 例如,可以通过二进制按位与来判断a和b代表的lane是否存在交集

    export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) 
      return (a & b) !== NoLanes;
    
    

Lane模型中task是怎么获取优先级的(赛车的初始赛道)

​ 任务获取赛道的方式是从高优先级的lanes开始的,这个过程发生在findUpdateLane函数中,如果高优先级没有可用的lane了就下降到优先级低的lanes中寻找,其中pickArbitraryLane会调用getHighestPriorityLane获取一批lanes中优先级最高的那一位,也就是通过lanes & -lanes获取最右边的一位

export function findUpdateLane(
  lanePriority: LanePriority,
  wipLanes: Lanes,
): Lane 
  switch (lanePriority) 
    //...
    case DefaultLanePriority: 
      let lane = pickArbitraryLane(DefaultLanes & ~wipLanes);//找到下一个优先级最高的lane
      if (lane === NoLane) //上一个level的lane都占满了下降到TransitionLanes继续寻找可用的赛道
        lane = pickArbitraryLane(TransitionLanes & ~wipLanes);
        if (lane === NoLane) //TransitionLanes也满了
          lane = pickArbitraryLane(DefaultLanes);//从DefaultLanes开始找
        
      
      return lane;
    
  

Lane模型中高优先级是怎么插队的(赛车抢赛道)

​ 在Lane模型中如果一个低优先级的任务执行,并且还在调度的时候触发了一个高优先级的任务,则高优先级的任务打断低优先级任务,此时应该先取消低优先级的任务,因为此时低优先级的任务可能已经进行了一段时间,Fiber树已经构建了一部分,所以需要将Fiber树还原,这个过程发生在函数prepareFreshStack中,在这个函数中会初始化已经构建的Fiber树

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) 
  const existingCallbackNode = root.callbackNode;//之前已经调用过的setState的回调
  //...
	if (existingCallbackNode !== null) 
    const existingCallbackPriority = root.callbackPriority;
    //新的setState的回调和之前setState的回调优先级相等 则进入batchedUpdate的逻辑
    if (existingCallbackPriority === newCallbackPriority) 
      return;
    
    //两个回调优先级不一致,则被高优先级任务打断,先取消当前低优先级的任务
    cancelCallback(existingCallbackNode);
  
	//调度render阶段的起点
	newCallbackNode = scheduleCallback(
    schedulerPriorityLevel,
    performConcurrentWorkOnRoot.bind(null, root),
  );
	//...

function prepareFreshStack(root: FiberRoot, lanes: Lanes) 
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
	//...
  //workInProgressRoot等变量重新赋值和初始化
  workInProgressRoot = root;
  workInProgress = createWorkInProgress(root.current, null);
  workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
  workInProgressRootExitStatus = RootIncomplete;
  workInProgressRootFatalError = null;
  workInProgressRootSkippedLanes = NoLanes;
  workInProgressRootUpdatedLanes = NoLanes;
  workInProgressRootPingedLanes = NoLanes;
	//...

Lane模型中怎么解决饥饿问题(最后一名赛车最后也要到达终点啊)

​ 在调度优先级的过程中,会调用markStarvedLanesAsExpired遍历pendingLanes(未执行的任务包含的lane),如果没过期时间就计算一个过期时间,如果过期了就加入root.expiredLanes中,然后在下次调用getNextLane函数的时候会优先返回expiredLanes

export function markStarvedLanesAsExpired(
  root: FiberRoot,
  currentTime: number,
): void 

  const pendingLanes = root.pendingLanes;
  const suspendedLanes = root.suspendedLanes;
  const pingedLanes = root.pingedLanes;
  const expirationTimes = root.expirationTimes;

  let lanes = pendingLanes;
  while (lanes > 0) //遍历lanes
    const index = pickArbitraryLaneIndex(lanes);
    const lane = 1 << index;

    const expirationTime = expirationTimes[index];
    if (expirationTime === NoTimestamp) 

      if (
        (lane & suspendedLanes) === NoLanes ||
        (lane & pingedLanes) !== NoLanes
      ) 
        expirationTimes[index] = computeExpirationTime(lane, currentTime);//计算过期时间
      
     else if (expirationTime <= currentTime) //过期了
      root.expiredLanes |= lane;//在expiredLanes加入当前遍历到的lane
    

    lanes &= ~lane;
  

export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes 
 	//...
  if (expiredLanes !== NoLanes) 
    nextLanes = expiredLanes;
    nextLanePriority = return_highestLanePriority = SyncLanePriority;//优先返回过期的lane
   else 
  //...
    
  return nextLanes;


​ 下图更直观,随之时间的推移,低优先级的任务被插队,最后也会变成高优先级的任务

react源码15.4

react源码解析19.手写迷你版react(代码片段)

react源码解析19.手写迷你版react视频课程(高效学习):进入课程课程目录:1.开篇介绍和面试题2.react的设计理念3.react源码架构4.源码目录结构和调试5.jsx&核心api6.legacy和concurrent模式入口函数7.Fiber架构8.render阶段9.diff算法10.comm... 查看详情

react源码解析18事件系统(代码片段)

react源码解析18事件系统视频课程(高效学习):进入课程课程目录:1.开篇介绍和面试题2.react的设计理念3.react源码架构4.源码目录结构和调试5.jsx&核心api6.legacy和concurrent模式入口函数7.Fiber架构8.render阶段9.diff算法10.commit阶段1... 查看详情

react源码解析14.手写hooks(代码片段)

react源码解析14.手写hooks视频课程(高效学习):进入课程课程目录:1.开篇介绍和面试题2.react的设计理念3.react源码架构4.源码目录结构和调试5.jsx&核心api6.legacy和concurrent模式入口函数7.Fiber架构8.render阶段9.diff算法10.commit阶段1... 查看详情

【原创】react-源码解析-component&ref(3)

...们在声明ClassComponent的时候extends的就是React.Component,在看源码只是我个人觉得写个class应该实现了很多复杂的功能比如说setState和render方法等,但是看了源码之后颠覆了我之前的认知:Component在源码中Component方法只是简单的对props... 查看详情

【原创】react-源码解析-forward-ref&context(4)

...对原组件进行包装的新的组件。接下来我们去看看方法的源码:forwardRef源码很简单,改方法返回了一个Oject具有render属性,同时$$typeof为"react.forward_ref"的Symbol值。        这里可能存在对于type... 查看详情

源码看react事件机制(代码片段)

对React熟悉的同学都知道,React中的事件机制并不是原生的那一套,事件没有绑定在原生DOM上,发出的事件也是对原生事件的包装。那么这一切是怎么实现的呢?事件注册首先还是看我们熟悉的代码&lt;buttononClick=this.autoFocus&... 查看详情

react中间件相关的一些源码解析(代码片段)

...on>>dispatch这一步了。  下边看看中间件相关的一些源码,从createStore开始:一、createStoreconstcreateStore=(reducers,initialState,enhancer)=>   if (typeof initialState===‘function‘&& typeof&nbs 查看详情

如何将 html 解析为 React 组件?

】如何将html解析为React组件?【英文标题】:HowtoparsehtmltoReactcomponent?【发布时间】:2017-11-2210:17:50【问题描述】:这是我的情景:1.页面内容的应用请求CMS(内容管理系统)。2.CMS返回"&lt;div&gt;Hi,&lt;SpecialButtoncolor=&qu... 查看详情

react-router@4.0使用和源码解析

如果你已经是一个正在开发中的react应用,想要引入更好的管理路由功能。那么,react-router是你最好的选择~react-router版本现今已经到4.0.0了,而上一个稳定版本还是2.8.1。相信我,如果你的项目中已经在使用react-router之前的版本... 查看详情

jpom-agent启动流程&交互源码解析

文章目录地址版本源码解析-Agent启动流程&交互地址Gitee:https://gitee.com/dromara/Jpom官网:https://jpom.io/一款简而轻的低侵入式在线构建、自动部署、日常运维、项目监控软件。版本我们以2.8.12为例子,鉴赏学习一下源码解... 查看详情

string.trim()源码解析

publicStringtrim()intlen=value.length;intst=0;char[]val=value;/*avoidgetfieldopcode*/while((st<len)&&(val[st]<=''))st++;while((st<len)&&(val[le 查看详情

mybatis源码-sqlsession门面模式&selectlist源码解析(代码片段)

文章目录Pre工程概览pom.xmlmybatis-config.xmlUserMapper测试类selectList源码解析附SQLlog4j.propertiesapp.propertiesUserPre如果MyBatis的基础用法还不熟悉,31篇入门博客拿走不谢戳戳戳—>https://blog.csdn.net/yangshangwei/category_7205317 查看详情

reacthooks源码深度解析

...于理解的​​React​​开发体验。​​ReactHooks​​的核心源码主要包括两个部分:​​React​​​内部的​​Hook​​管理器和一系列预置的​​​Hook​​​函数。首先,让我们看一下​​React​​内部的​​Hook​​管理器。这... 查看详情

8.2构建客户端源码解析

准备工作:先启动两个provider:dubbo://10.211.55.5:20880/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider&dubbo=2.5.7&generic=false&interface=com.alibaba.dubbo.demo.DemoSe 查看详情

jquery源码解析

(function(global,factory){ "usestrict"; if(typeofmodule==="object"&&typeofmodule.exports==="object"){ module.exports=global.document? factory(global,true): function(w){ if(!w.document) 查看详情

react源码解析-debugger阶段-类组件mount阶段的beginwork(代码片段)

类组件的beginWork。Appfiber执行完递阶段之后,轮到DDfiber执行递解端。对于类组件,在beginWork的时候,会调用updateClassComponent方法。类组件的fiber如:alternate:nullchild:nullelementType:ƒDD()key:nulllanes:16mode:1pendin 查看详情

vue2.6.13源码解析initlifecycle&&initevents(代码片段)

文章目录前言一、initLifeCycle二、listeners2.1processAttrs2.2addHandler2.3genData2.4genHandlers2.5createComponent三、initEvents3.1updateComponentListeners3.1.1add3.1.2remove3.2updateListeners3.3createFnInvoker总结前言 查看详情

完全解析!bert&transformer阅读理解源码详解

...:你所不知道的Transformer!超详细的Bert文本分类源码解读|附源码中文情感分类单标签参考论文:https://arxiv.org/abs/1706.03762https://arxiv.org/abs/1810.04805在本文中,我将以run_squad.py以及SQuAD数据集为例介绍阅读理解的源码... 查看详情