React源码阅读(二)- Scheduler

  • 第一篇文章React源码入门篇(一)我们主要从React.createRoot()ReactDom.reader中的renderupdateContainer函数入手, 在updateContainer函数中最后调用了scheduleUpdateOnFiber, 走入了我们的第二部分

这篇文章就从scheduleUpdateOnFiber开始探索Scheduler

代码大部分来源于react/blob/v18.2.0/packages/scheduler/src/forks/Scheduler.js

文章的脉络直接依照文章一总结的执行栈顺序

  • scheduleUpdateOnFiber()
  • ensureRootIsScheduled()
  • unstable_scheduleCallback()
  • requestHostCallback()
  • schedulePerformWorkUntilDeadline()
  • performWorkUntilDeadline()
  • flushWork()
  • workLoop()
  • performConcurrentWorkOnRoot()

一、scheduleUpdateOnFiber

这个函数还是相当多内容的, 但是我们直接抓重点看就好了, 这里有两个比较重要的函数, 一个是markRootUpdated, 一个是ensureRootIsScheduled

js 复制代码
function scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
  checkForNestedUpdates();
  // 标记FiberRootNode的pendingLanes
  markRootUpdated(root, lane, eventTime);
  .....
  // 
  ensureRootIsScheduled(root, eventTime);
  ....
}
  • markRootUpdated函数, 其中最主要的逻辑就是将前面获取到的优先级lane挂载到FiberRootNodependingLanes属性上, 表示当前有需要更新的任务。然后再计算出其优先级对应的赛道下标。更新起始时间。
js 复制代码
function markRootUpdated(root, updateLane, eventTime) {
   // 进行按位与并赋值
  root.pendingLanes |= updateLane; 
  // 如果当前优先级非空闲优先级的话则重置Suspence相关优先级
  if (updateLane !== IdleLane) {
    root.suspendedLanes = NoLanes;
    root.pingedLanes = NoLanes;
  }
  // 拿到事件起始时间
  var eventTimes = root.eventTimes;
  // 采用31 - clz32(lanes)的方式, 当前传入lanes16时,拿到赛道下标index为4
  var index = laneToIndex(updateLane);
  // 记录evnetTimes
  eventTimes[index] = eventTime;
}

二、ensureRootIsScheduled

这个函数很重要, 多次被调用。 主要是分成三个部分

  • 第一部分通过markStarvedLanesAsExpired处理饥饿问题
  • 第二部分通过getNextLanes确定下一个更新任务的优先级
  • 第三部分通过scheduleCallback, cancelCallback这两个函数根据拿到的优先级调度任务(这部分需要引入后面的内容才能更具体的描述, 后面会总结)
js 复制代码
function ensureRootIsScheduled(root, currentTime) {
  // 取出FiberRootNode的callbackNode
  var existingCallbackNode = root.callbackNode; 
  // 遍历待处理的更新任务,检查是否已经达到了它们的过期时间或者计算他们的过期时间
  // 如果是,就将这些任务标记为过期,在后面的逻辑再对针对他们进行处理
  markStarvedLanesAsExpired(root, currentTime); 
  // 确定下一个更新任务的优先级
  var nextLanes = getNextLanes(root, root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes);
  // 如果取出来为空, 则表示没有任务需要运行了
  if (nextLanes === NoLanes) {
    // 如果当前有任务再进行的话, 则取消该task
    if (existingCallbackNode !== null) {
      cancelCallback$1(existingCallbackNode);
    }
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
  } 
  // 下一个任务优先级
  var newCallbackPriority = getHighestPriorityLane(nextLanes);
  // 当前优先级
  var existingCallbackPriority = root.callbackPriority;
  ....
  // 如果前后优先级一致的话, 则直接重用当前任务, 不发起新的调度任务, 故直接return
  if (existingCallbackPriority === newCallbackPriority && 
  !( ReactCurrentActQueue$1.current !== null && existingCallbackNode !== fakeActCallbackNode)) {
    return;
  }
  // 当不一致的时候, 取消之前的callback任务
  if (existingCallbackNode != null) {
    cancelCallback$1(existingCallbackNode);
  } 
  // 创建新的callback task
  var newCallbackNode;
  // 如果新的回调优先级是同步优先级(SyncLane),则特殊处理
  if (newCallbackPriority === SyncLane) {
     // 如果根节点的标记是LegacyRoot,则将执行同步工作的函数调度到同步回调队列中
    if (root.tag === LegacyRoot) {
      ....
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
      ....
  } else { 
    var schedulerPriorityLevel;
    // 对优先级进行Switch Case的转换, 将lane优先级转换为事件优先级
    // 然后再转换为Scheduler的优先级
    switch (lanesToEventPriority(nextLanes)) {
      case DiscreteEventPriority:
        schedulerPriorityLevel = ImmediatePriority;
        break;

      case ContinuousEventPriority:
        schedulerPriorityLevel = UserBlockingPriority;
        break;

      case DefaultEventPriority:
        schedulerPriorityLevel = NormalPriority;
        break;

      case IdleEventPriority:
        schedulerPriorityLevel = IdlePriority;
        break;

      default:
        schedulerPriorityLevel = NormalPriority;
        break;
    }
    
    // 开启新的调度任务
    newCallbackNode = scheduleCallback$1(
        schedulerPriorityLevel,
        performConcurrentWorkOnRoot.bind(null, root)
    );
  }
  // 更新root节点上的信息
  root.callbackPriority = newCallbackPriority;
  // 这里其实就是scheduleCallback内部创建的newTask
  root.callbackNode = newCallbackNode;
} 

markStarvedLanesAsExpired

ensureRootIsScheduled其中的markStarvedLanesAsExpired也算比较重要的一环。 主要逻辑分成两步

  • 在没有设置expirationTime的情况下进行根据currentTime和优先级lanes计算出过期时间
  • 在有设置的情况下判断是否过期的, 过期的话保存到expiredLanes

所以他的任务就是检查并标记那些因为长时间未完成而被认为是饥饿状态的更新任务,以确保它们能够得到及时处理

js 复制代码
function markStarvedLanesAsExpired(root, currentTime) {
 // 先拿到各种更新任务优先级
 var pendingLanes = root.pendingLanes;
 var suspendedLanes = root.suspendedLanes;
 var pingedLanes = root.pingedLanes;
 var expirationTimes = root.expirationTimes; 
 var lanes = pendingLanes;

 while (lanes > 0) {
   // 算出expirationTime对应的赛道下标index
   var index = pickArbitraryLaneIndex(lanes);
   var lane = 1 << index;
   // 拿到FiberRootNode上的expirationTimes对应下标的过期时间
   // 默认为-1, 则NoTimestamp
  var expirationTime = expirationTimes[index];
  // 没有设置expirationTime的情况
  if (expirationTime === NoTimestamp) {
    // 如果当前不是挂起优先级和重启优先级,则计算出其过期时间
    if ((lane & suspendedLanes) === NoLanes || (lane & pingedLanes) !== NoLanes) {
      // 根据当前时间和优先级计算出其过期时间
      expirationTimes[index] = computeExpirationTime(lane, currentTime);
      }
      // 如果已经过期
    } else if (expirationTime <= currentTime) {
      // 根据按位或赋值在root.expiredLanes, 
      root.expiredLanes |= lane;
    }
    // 文章一介绍过,拿到下一个优先级, 继续处理
    lanes &= ~lane;
  }
}
  • 其中计算过期的时间代码如下, 可以看到, 其实就是主要分成三种
    • 对于连续的优先级,在currentTime的基础上再加250毫秒
    • 默认优先级或过渡相关优先级则加5000毫秒
    • 其他不设置过期时间
js 复制代码
function computeExpirationTime(lane: Lane, currentTime: number) {
   switch (lane) {
     case SyncHydrationLane:
     case SyncLane:
     case InputContinuousHydrationLane:
     case InputContinuousLane:
       return currentTime + 250;
     case DefaultHydrationLane:
     case DefaultLane:
     case TransitionHydrationLane:
     case TransitionLane1:
     case TransitionLane2:
     case TransitionLane3:
     case TransitionLane4:
     case TransitionLane5:
     case TransitionLane6:
     case TransitionLane7:
     case TransitionLane8:
     case TransitionLane9:
     case TransitionLane10:
     case TransitionLane11:
     case TransitionLane12:
     case TransitionLane13:
     case TransitionLane14:
     case TransitionLane15:
       return currentTime + 5000;
     case RetryLane1:
     case RetryLane2:
     case RetryLane3:
     case RetryLane4:
       return NoTimestamp;
     case SelectiveHydrationLane:
     case IdleHydrationLane:
     case IdleLane:
     case OffscreenLane:
     case DeferredLane:
       return NoTimestamp;
     default:
       return NoTimestamp;
   }
 }

getNextLanes

  • 获取下一任务: getNextLanes通过lanes找到下一个调度的更新任务, 主要看是否处于空闲任务, 然后找到最高优先级, 接着跟当前优先级进行比较,如果当前正在处理任务且新的任务优先级不高于当前任务优先级,则返回当前任务优先级, 否则的话返回新任务优先级

    js 复制代码
    function getNextLanes(root, wipLanes) {
      // 取出待更新任务优先级
      var pendingLanes = root.pendingLanes;
      // 如果等于NoLanes的话则说明当前没有任务在等待更新了
      if (pendingLanes === NoLanes) {
        return NoLanes;
      }
      // 初始化
      var nextLanes = NoLanes;
      var suspendedLanes = root.suspendedLanes;
      var pingedLanes = root.pingedLanes; 
      // 获取非空闲任务的待处理任务优先级
      var nonIdlePendingLanes = pendingLanes & NonIdleLanes;
      // 如果存在非空闲任务
      if (nonIdlePendingLanes !== NoLanes) {
       // 获取非空闲未被阻塞的任务优先级
        var nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
        if (nonIdleUnblockedLanes !== NoLanes) {
           // 获取非空闲未被阻塞的任务优先级中最高的优先级
          nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
        } else {
         // 这里一样的道理
          var nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
          if (nonIdlePingedLanes !== NoLanes) {
            nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
          }
        }
      } else {
        // 只剩下空闲任务等待执行, 也是去找到最高位的
        var unblockedLanes = pendingLanes & ~suspendedLanes;
        if (unblockedLanes !== NoLanes) {
          nextLanes = getHighestPriorityLanes(unblockedLanes);
        } else {
          if (pingedLanes !== NoLanes) {
            nextLanes = getHighestPriorityLanes(pingedLanes);
          }
        }
      }
      // 如果上面两个分支都没有走到的话就直接reuturn出去了
      if (nextLanes === NoLanes) {
        return NoLanes;
      } 
    
      // 如果当前正在处理任务且新的任务优先级不高于当前任务优先级,则继续处理当前任务。
      if (
      wipLanes !== NoLanes && 
      wipLanes !== nextLanes && 
      (wipLanes & suspendedLanes) === NoLanes
      ) {
        var nextLane = getHighestPriorityLane(nextLanes);
        var wipLane = getHighestPriorityLane(wipLanes);
        if (
        nextLane >= wipLane || 
        nextLane === DefaultLane &&
        (wipLane & TransitionLanes) !== NoLanes
        ) {
          // 不打断, 保持吃当前优先级
          return wipLanes;
        }
      }
     //  如果下一个任务优先级包含连续输入任务,则将默认任务添加到下一个任务优先级中。
      if ((nextLanes & InputContinuousLane) !== NoLanes) {
        nextLanes |= pendingLanes & DefaultLane;
      } 
      ....
      return nextLanes;
    }

cancelCallback

主要逻辑就是将taskcallback置为空

js 复制代码
function unstable_cancelCallback(task) {
  if (enableProfiling) {
    if (task.isQueued) {
      const currentTime = getCurrentTime();
      markTaskCanceled(task, currentTime);
      task.isQueued = false;
    }
  }
  task.callback = null;
}

scheduleCallback

调度任务: 这里分成同步和并发, 通过newCallbackPriority === SyncLanes进行区分

  • 同步的话调用的是scheduleSyncCallback, 会相对简单一些, 因为不考虑中断重启之类的。 故直接就是维护一个syncQueue队列, 有则入队列。然后再按顺序拿出来执行即可
js 复制代码
  export function scheduleSyncCallback(callback: SchedulerCallback) { 
     if (syncQueue === null) { 
     syncQueue = [callback]; 
     } else {
     syncQueue.push(callback); 
     } 
 }
  • 并发的话调用的是unstable_scheduleCallback

三、unstable_scheduleCallback

并发的话调用的是unstable_scheduleCallback, 相对于同步逻辑会比较多, 因为要考虑调度情况。 这里其实就是生成了一个task对象,然后根据startTimecurrentTime的对比, 判断要推入timeQueue还是taskQueue, 推入timeQueue的话调用requestHostTimeout,否则调用requestHostCallback

js 复制代码
 function unstable_scheduleCallback(priorityLevel, callback, options) {
   // ...前面是处理newTask的各个属性值, 这里就直接忽略了
   var newTask = {
     id: taskIdCounter++, 
     callback: callback,  // 我们传入的performConcurrentWorkOnRoot函数
     priorityLevel: priorityLevel, // 优先级
     startTime: startTime, // 开始时间
     expirationTime: expirationTime, // 过期时间
     sortIndex: -1
   };
    // 还没到时间,startTime还没到, 推入timeQueue
   if (startTime > currentTime) {
     newTask.sortIndex = startTime;
     push(timerQueue, newTask);  // 将新任务推入timeQueue延时队列
     // taskQueue[0]为null且timeQueue[0]为当前task
     if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
       if (isHostTimeoutScheduled) {
         // 有新的任务推进来的话则取消之前的定时,反正会开新的
         cancelHostTimeout();
       } else {
         isHostTimeoutScheduled = true;
       }
       // 开启新的定时进行处理
       requestHostTimeout(handleTimeout, startTime - currentTime);
     }
   } else {
    // 到点了, 推入taskQueue
     newTask.sortIndex = expirationTime;
     push(taskQueue, newTask); // 推入taskQueue
      // 如果如果没有任务在调度,并且当前不在执行工作状态
     if (!isHostCallbackScheduled && !isPerformingWork) { 
       isHostCallbackScheduled = true;
       requestHostCallback(flushWork); // 开启host回调
     }
   }
   return newTask; // 将创建好的task返回出去
 }

push

  • 其中push()函数也值得了解一下,他不是单纯的推入操作, 他push进去之后又调用了siftUp()实现堆上浮的操作,类似于找到最小堆, 其中对比调用的是compare(), 先对比了sortIndex, 我们从上面可以看到他分情况赋值,推入timeQueue赋值的是startTime, 推入taskQueue推入的是expirationTime, 如果一致的话则退级通过id进行对比, 那当我们通过peek(taskQueue)或者peek(timeQueue)拿取的时候总能拿到最高优先级的

    js 复制代码
    export function push(heap: Heap, node: Node): void {
      const index = heap.length;
      heap.push(node);
      siftUp(heap, node, index);
    }
    function siftUp(heap, node, i) {
      let index = i;
      while (index > 0) {
        const parentIndex = (index - 1) >>> 1;
        const parent = heap[parentIndex];
        if (compare(parent, node) > 0) {
          // The parent is larger. Swap positions.
          heap[parentIndex] = node;
          heap[index] = parent;
          index = parentIndex;
        } else {
          return;
        }
      }
    }
    function compare(a, b) {
      // Compare sort index first, then task id.
      const diff = a.sortIndex - b.sortIndex;
      return diff !== 0 ? diff : a.id - b.id;
    }

requestHostTimeout

可以看到他就是开启了一个定时, 延时的时间为startTime - currentTime,回调的函数为handleTimeout 再看handleTimeout, 其实就是因为我们的定时是s-c, 故时间到了一般意味着该任务可以从timeQueue移到taskQueue了, 故一开始我们判断了是否可以移入。 然后判断taskQueue是否有任务, 有的话则开启调度。 没有的话再继续判断timeQueue是否有任务。 有的话则继续开启定时

js 复制代码
function handleTimeout(currentTime) {
  isHostTimeoutScheduled = false;
  advanceTimers(currentTime); // 先判断timeQueue中是否有任务可以移入taskQueue

  if (!isHostCallbackScheduled) {
    // taskQueue存在任务
    if (peek(taskQueue) !== null) {
      isHostCallbackScheduled = true;
      requestHostCallback(flushWork);  // 开启调度
    } else {
      const firstTimer = peek(timerQueue);  // 拿到timeQueue中的第一个任务
      if (firstTimer !== null) {
       // 继续开启定时
        requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
      }
    }
  }
}

advanceTimer

  • 其中的advanceTimer函数, 作用其实就是找一找timerQueue有没有能够转移进来taskQueue的, 通过判断Queue中的startTimecurrentTime进行对比
js 复制代码
function advanceTimers(currentTime) {
  let timer = peek(timerQueue);
  while (timer !== null) {
    if (timer.callback === null) {// 无效的say拜拜
      pop(timerQueue);
    } else if (timer.startTime <= currentTime) {
       // 到点了, 从timerQueue移出, 转入taskQueue
      pop(timerQueue);
      // 这里要重置sortIndex, 原因我们上面也讲过了
      // 对应taskQueue来说sortIndex存放的就是expirationTime, 而timeQueue是startTime
      timer.sortIndex = timer.expirationTime; 
      push(taskQueue, timer);
       ..
    } else {
      return; // 排在最前面的都还没有到点, 后面的自然也没到, 直接return走人
    }
    timer = peek(timerQueue); // 挨个找出来判断
  }
}

requestHostCallback

  • 接着我们可以看到unstable_scheduleCallback中最后是调用了requestHostCallback去开启host回调, 其中又去调用了schedulePerformWorkUntilDeadline();

    js 复制代码
    function requestHostCallback(callback) {
      scheduledHostCallback = callback; 
      if (!isMessageLoopRunning) {
        isMessageLoopRunning = true;
        schedulePerformWorkUntilDeadline();
      }
    }

四、schedulePerformWorkUntilDeadline

我们通过打断点可以看到schedulePerformWorkUntilDeadline最终内部就是做了一个postMessage的操作, 且上下文new了一个MessageChannel

MessageChannel

我们可以先学一波什么是MessageChannel

  • Channel Messaging API 的 MessageChannel 接口允许我们创建一个新的消息通道,并通过它的两个MessagePort属性发送数据
  • Channel Messgae属于宏任务
  • 来段代码小试一下
js 复制代码
        const { port1 , port2 } = new MessageChannel();
        port1.postMessage('123')
        port2.onmessage = (msg) => {
            console.log('收到:' + msg.data);
        }
        Promise.resolve().then(() => { console.log('微任务啦')});
        console.log('同步任务啦');

所以截图中的代码就是先实例化了一个MessageChannel, 然后port1开启onmessage监听, 回调函数是performWorkUntilDeadline, 然后我们在schedulePerformWorkUntilDeadline中去使用port2通过postMessage的方式发送消息, 目的是为了触发performWorkUntilDeadline在后面的宏任务执行

我们录制一下performance也可以看到在执行完schedulePerformWorkUntilDeadline之后就交出主线程,浏览器做渲染的操作, 然后再执行宏任务触发了performWorkUntilDeadline

五、performWorkUntilDeadline

  • 可以看到他的核心逻辑就是调用了scheduledHostCallback, 无论成功还是失败再去再判断haseMoreWork去判断要调用schedulePerformWorkUntilDeadline还是结束
js 复制代码
const performWorkUntilDeadline = () => {
   // 所以只要前面调用了requestHostCallback的话这里就不会为null
  if (scheduledHostCallback !== null) {
    const currentTime = getCurrentTime();
    startTime = currentTime;
    const hasTimeRemaining = true;
    let hasMoreWork = true;
    try {
      hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    } finally {
        // 这里做判断
      if (hasMoreWork) {
        schedulePerformWorkUntilDeadline();
      } else {
        isMessageLoopRunning = false;
        scheduledHostCallback = null;
      }
    }
  } else {
    isMessageLoopRunning = false;
  }
  needsPaint = false;
};
  • 你会发现, 只要hasMoreWorktrue的时候, 他们之间就形成了循环
  • 那这里的scheduledHostCallback是什么呢, 我们可以回看requestHostCallback函数, 内部有逻辑就是将传入的flushWork赋值给了scheduledHostCallback

六、flushWork

flushWork的逻辑, 可以看到就是一些状态的重置, 然后核心逻辑又走向了workLoop

js 复制代码
function flushWork(hasTimeRemaining, initialTime) {
  // 将isHostCallbackScheduled赋值为false
  // 我们曾在scheduleCallback的时候判断了它后将其赋值为true才开启了requestHostCallback
  // 这里重置状态
  isHostCallbackScheduled = false;
  // ...
  isPerformingWork = true;  // 修改状态为执行中
  const previousPriorityLevel = currentPriorityLevel; // 设置优先级
  try {
    ....
    return workLoop(hasTimeRemaining, initialTime);
  } finally {
    currentTask = null; // 重置状态
    currentPriorityLevel = previousPriorityLevel;
    isPerformingWork = false; //执行完毕
    ...
  }
}

七、workLoop

  • 看到函数的命名中带有Loop就知道十有八九又来个循环, 果不其然, 内部又使用了while, 那是什么在循环呢, 还记得我们在scheduleCallback生成了newtask, 并且将task存入taskQueuetimeQueue, 那么现在就是考虑将他拿出来执行的时候啦
js 复制代码
function workLoop(hasTimeRemaining, initialTime) {
  let currentTime = initialTime;
  // 在找taskQueue之前先再找一遍timeQueue看看有没有老铁到点了能转移进来的
  advanceTimers(currentTime);
  // 拿到当前要执行的任务
  currentTask = peek(taskQueue);
  while (
    currentTask !== null &&  //当前有任务
    !(enableSchedulerDebugging && isSchedulerPaused) 
  ) {
    // 1. task任务还没过期
    // 2. hasTimeRemaining为false或者shouldYieldToHost()为true
    if (
      currentTask.expirationTime > currentTime &&
      (!hasTimeRemaining || shouldYieldToHost())
    ) {
      // 这种情况下就是属于task还没有过期, 但是没有剩余的可执行时间了,故跳出
      break;
    }
    // 拿出回调, 这个回调其实就是之前我们存放进去的performConcurrentWorkOnRoot
    const callback = currentTask.callback;
    if (typeof callback === 'function') {
      currentTask.callback = null; // 置空
      currentPriorityLevel = currentTask.priorityLevel; // 拿到优先级
      // 是否过期
      const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
      ....
      // 执行回调
      const continuationCallback = callback(didUserCallbackTimeout);
      currentTime = getCurrentTime();
      // 如果还有任务的话则类型为function, 那么此时会重新给currentTask.callback赋值
      if (typeof continuationCallback === 'function') {
        currentTask.callback = continuationCallback;
        ...
      } else {
        ...
        // 没有接下去的任务的话该可以将该任务从taskQueue取出了
        if (currentTask === peek(taskQueue)) {
          pop(taskQueue);
        }
      }
      // 处理完一项任务后重新找一遍timeQueue看看有没有能移入taskQueue的
      advanceTimers(currentTime);
    } else {
      // callback为空的情况则是因为通过cancelCallback取消了该任务, 故不用处理, 直接移出taskQueue即可
      pop(taskQueue);
    }
    currentTask = peek(taskQueue);
  }
 
  // 跳出循环有两种可能
  // 一种是没有currenTask了,taskQueue中的任务都执行完了
  // 一种就是还有task但是执行时间没有了break出来的
  if (currentTask !== null) {
    return true;  //这种就是还有任务, return true表示后续还有任务待执行
  } else {
    ....
    return false; //return false表示后面没有任务了
  }
}
  • workLoop拿到的taskcallback指的就是performConcurrentWorkOnRoot

八、 performConcurrentWorkOnRoot

从函数的命名也可以看出, 这个函数就是真正的去执行task的过程, 他先再调用了一次getNextLanes,确保此时拿到的lanes确实是最高优先级的, 然后再通过shouldTimeSlice判断进行饥饿处理和进入render节点,拿到exitStatus后,判断exitStatus进去commit阶段

js 复制代码
function performConcurrentWorkOnRoot(root, didTimeout) {
   ...
   const originalCallbackNode = root.callbackNode;
  // getNextLanes我们上面接触过了, 就是获取下一个任务的优先级
  // 那么在这里再调用一次的原因我理解是在执行任务前可能又有更高优先级的任务
  // 那么可以执行的任务是更高优先级的任务
  let lanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );
  // 没有等待的任务了
  if (lanes === NoLanes) {
    return null;
  }
  // shouldTimeSlice根据lane的优先级,决定是采用并发模式还是同步模式渲染,需要同时满足
  // 1. 不包含被阻塞的优先级
  // 2. 不包含过期的优先级
  // 3. 是否过期
  const shouldTimeSlice =
    !includesBlockingLane(root, lanes) &&
    // 这个就是通过root.expiredLanes去判断,我们在markStarvedLanesAsExpired处理过
    !includesExpiredLane(root, lanes) &&  
    (disableSchedulerTimeoutInWorkLoop || !didTimeout);
  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);
  // 接下去的代码就是通过调用renderRootxx()返回的exitStatus的不同情况做不同的处理了
  // 这里先省略后面有解释
  ....
  ensureRootIsScheduled(root, now());
  // 检查根节点的回调是否发生了变化---在文章三会详细讲这里
  if (root.callbackNode === originalCallbackNode) {
      // 如果没有变化,则返回一个继续执行的回调函数
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  // 否则返回null
  return null;
}
  • 其中的exitStatus有几种情况

    • RootInProgress = 0: 表示渲染正在进行中, 无需进一步处理
    • RootFatalErrored = 1: 表示渲染过程出现致命错误, 抛出错误并终止程序
    js 复制代码
        if (exitStatus === RootFatalErrored) {
          const fatalError = workInProgressRootFatalError;
          prepareFreshStack(root, NoLanes);
          markRootSuspended(root, lanes);
          ensureRootIsScheduled(root, now());
          throw fatalError; // 直接抛出错误了
        }
    • RootErrored = 2:表示渲染过程中出现错误, 尝试再次渲染, 第二次还是失败的话则放弃
    js 复制代码
        if (exitStatus === RootErrored) {
          const errorRetryLanes = getLanesToRetrySynchronouslyOnError(root);
          if (errorRetryLanes !== NoLanes) {
            lanes = errorRetryLanes;
            exitStatus = recoverFromConcurrentError(root, errorRetryLanes);
          }
        }
    • RootDidNotComplete = 6: 表示在未完成树的情况下展开, 进行标记, 这种情况只会发生在并发渲染模式, 让出主线程让浏览器执行更高优先级的任务。
    js 复制代码
        if (exitStatus === RootDidNotComplete) {
          markRootSuspended(root, lanes);
        }
    • RootSuspended = 3: 挂起了
    • RootSuspendedWithDelay = 4: 这个没了解到
    • RootCompleted = 5: 正常渲染完成
    js 复制代码
    {
          // The render completed.
          ...
          // render的流程结束后就要走向commit的流程
    
          // We now have a consistent tree. The next step is either to commit it,
          // or, if something suspended, wait to commit it after a timeout.
          root.finishedWork = finishedWork;
          root.finishedLanes = lanes;
          finishConcurrentRender(root, exitStatus, lanes);
        }
  • renderRootConcurrentrenderRootSync其实都是render的过程了,我们放在下一章继续讲解render相关流程

  • finishConcurrentRender作为commit的入口我们放在第四章继续讲解commit相关流程

附上面函数流程图的总结

简单图示

上面的有点乱, 再来一张纯净版的

  • 这个就很明显了
  • 红色的表示执行栈中的主函数
  • 绿色表示函数存在的一些主逻辑
  • 蓝色表示返回值(这里主要为了hasMoreWork)
  • 可以看到基本就是大循环里面嵌套小循环

九、调度总结

学习完上面的代码已经对调度有了一定的了解。 接下去从三个维度去复习总结调度过程(可以回看之前的代码逻辑, 都有涉及,此部分是个将上面的代码逻辑串联起来)

高优先级触发取消相关

  • 取消的原因: 有更高优先级的事件插入
  • 取消的逻辑:主要逻辑我们在ensureRootIsScheduled中学习过, 使用的函数是cancelCallback, 核心逻辑是将taskcallback置为空。
  • 取消效果如何实现: 在workLoop中将taskQueuetask拿出来处理时, 如果taskcallback为空的话,直接移出,不处理

打断相关

  • 打断原因: 释放线程给浏览器
  • 打断逻辑: 在workLoop中我们通过判断了三个条件。 分别为currentTask.expirationTime > currentTime判断任务是否过期。 hasTimeRemaining怕电脑是否有剩余时间。 shouldYieldToHost判断是否还有时间片。 判断如果需要让出线程的话则break,此时不再处理taskQueue的任务, 但是因为当前是存在任务的, 故schedulePerformWorkUntilDeadline又会创建新的宏任务去处理任务。 故就成功做到了打断,后面又能顺利重启

饥饿相关

  • 饥饿处理的原因: 因为我们存在可以打断低优先级然后执行高优先级任务的逻辑。 故可能引发一种情况: 一直有高优先级的任务存在导致低优先级的任务迟迟无法被执行
  • 饥饿处理的逻辑: 我们在前面的逻辑中两次提到饥饿处理
    • 第一是在ensureRootIsScheduled中调用了markStarvedLanesAsExpired, 根据pendinglanes计算出了当前存在过期现象的赛道, 并记录到expiredLanes
    • 第二是在performConcurrentWorkOnRoot通过expiredLanes作为条件之一进行了判断,若存在expiredLanes则调用renderRootSync, 否则调用renderRootConcurrent. 其中的差异我们下一章会涉及
相关推荐
GISer_Jing8 分钟前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
落霞的思绪1 小时前
CSS复习
前端·css
咖啡の猫3 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲5 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5816 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路6 小时前
GeoTools 读取影像元数据
前端
ssshooter7 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
Jerry7 小时前
Jetpack Compose 中的状态
前端
dae bal8 小时前
关于RSA和AES加密
前端·vue.js
柳杉8 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化