React16源码: React中的异步调度scheduler模块的源码实现

React Scheduler


1 ) 概述

  • react当中的异步调度,称为 React Scheduler
  • 发布成单独的一个 npm 包就叫做 scheduler
  • 这个包它做了什么?
    • A. 首先它维护时间片
    • B. 然后模拟 requestIdleCallback 这个API
      • 因为现在浏览器的支持不是特别的多
      • 所以在浏览当中只是去模拟了一个这个API,而不是直接使用这个API
      • 因为需要考虑到浏览器兼容性
      • 这个API的作用
        • 调用这个API传入一个回调之后,这个API会等到浏览器把它的一些主要任务执行完了
        • 当它有空闲的时间的时候,再回来调用这个回调
      • 相对于 requestAnimationFrame 来说,它的优先级会低很多
      • 它是等浏览器器要做的事情做完了之后,再回来调这个回调
      • requestAnimationFrame 是浏览器要渲染当前帧的时候,调用这个回调
    • C. 调度列表和进行一个超时的判断
  • 关于时间片
    • 不管是在浏览器还是在App当中,要给用户很流畅的一个感觉的时候
    • 至少要保证在一秒钟之内要渲染30帧以上
    • 现在的一些高刷新率的浏览器,可能会要求在60帧以上,甚至还有更高的,比如,120帧
    • 这个帧数就是我们1秒钟,页面要重新渲染刷新多少次
    • 它并不是说我一秒钟之内刷新30次,满足就行了。
    • 比如前面的半秒钟只刷新了一次,后面的半秒钟刷新了二十九次,这个也是不行的
    • 这个给用户的感觉,就是前面这半秒钟会特别的卡就一动不动,然后后面又变得流畅
    • 所以,它的要求还需要是平均的每33毫秒要刷新1帧,要保持这个频率
    • 浏览器必须自己去渲染这些动画,要每1帧里面有固定的时间去渲染这个动画
    • 在这里举个例子,比如说整个应用所有的js的操作,都是通过 react 来实现的
    • 而浏览器有一个一直在更新的动画, 浏览器渲染这个动画如果要11毫秒
    • 那么给每一帧的, 就是把一秒钟分成了30帧之后,每一帧是33毫秒
    • 这个33毫秒里面的11毫秒是必须要留给浏览器去渲染这个动画的, 才能让这个动画看起来是流畅的
    • 而在这个时候留给react去渲染它的应用更新的时候,每一帧里面就只有22毫秒
    • 如果react它在这一帧里面的一个更新,它需要渲染的时间很长,比如说35毫秒
    • 那这个时候,我们一帧的时间就全部给react渲染给占掉了
    • 因为 js 引擎是单线程的, 如果react在一直在执行,浏览器它就没有机会去获得运行权
    • 就没有机会去刷新它的一个动画, 这时候,不仅把一帧的时间占完了
    • 这样还不够,还要去下一帧里面借用一点时间,那么这个时间用完之后
    • 浏览器要去更新动画,如果这一帧里面我们就用掉了13毫秒,剩下的时间就只剩下20毫秒
    • 那么这20毫秒,又可能要运行一部分react的更新,然后再去浏览器的一个渲染
    • 这就会导致整个动画变得卡顿起来了
    • 这就是 React Scheduler 它的一个目的, 为了保证react它去执行更新的这个时间
    • 不超过在浏览器的每一帧里面特定的时间,它希望留给浏览器去刷新动画,或者是响应用户输入的反馈的时候
    • 每一帧里面有足够的时间

2 )时间片源码

  • 时间片源码在 packages/scheduler 这个包里面,是一个单独的模块,单独发布到 npm 上

  • 在 ReactFiberScheduler.js 里面,哪个地方用到它呢?

    • requestWork 函数里面,如果 expirationTime 异步的,就会调用 scheduleCallbackWithExpirationTime
      function scheduleCallbackWithExpirationTime(
        root: FiberRoot,
        expirationTime: ExpirationTime,
      ) {
        if (callbackExpirationTime !== NoWork) {
          // A callback is already scheduled. Check its expiration time (timeout).
          if (expirationTime > callbackExpirationTime) {
            // Existing callback has sufficient timeout. Exit.
            return;
          } else {
            if (callbackID !== null) {
              // Existing callback has insufficient timeout. Cancel and schedule a
              // new one.
              cancelDeferredCallback(callbackID);
            }
          }
          // The request callback timer is already running. Don't start a new one.
        } else {
          startRequestCallbackTimer();
        }
      
        callbackExpirationTime = expirationTime;
        const currentMs = now() - originalStartTimeMs;
        const expirationTimeMs = expirationTimeToMs(expirationTime);
        const timeout = expirationTimeMs - currentMs;
        callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
      }
      
      • 全局变量 callbackExpirationTime 对应的是 上一次调用 React Scheduler 去申请了一个callback
      • 这个callback 也会有一个 expirationTime, 因为是异步调度,所以会有一个 expirationTime 传进来
      • 如果这个 callbackExpirationTime !== NoWork 代表之前有一个callback在执行了
      • 这边就会判断当前的 expirationTime 是否比之前回调中的那个要大
      • 如果大,说明当前的这个的优先级要低,这个时候就直接return了不执行
      • 因为它优先级更低,我们肯定要执行优先级更高的那个,调用 cancelDeferredCallback 把之前的 cancel 掉
      • startRequestCallbackTimer 这个函数跳过,不涉及主流程,涉及DEV Tool 相关
      • 接着更新一系列的变量
        • 更新 callbackExpirationTime
        • 计算出 timeout
      • 最后调用 scheduleDeferredCallback 这个方法来自于 ReactFiberHostConfig.js
        • 如果直接查找 这个文件,发现基本上没有什么内容, 是因为 React对于打包工具的配置,进行了文件名的映射
        • 它实际映射的是 eact-reconciler/src/forks/ReactFiberHostConfig.dom.js
          export * from 'react-dom/src/client/ReactDOMHostConfig';
          
        • 发现里面就一行代码,找到对应的 ReactDOMHostConfig.js 文件,搜索 scheduleDeferredCallback 方法
          export {
            unstable_scheduleCallback as scheduleDeferredCallback,
          } from 'scheduler';
          
          • 可追溯到 这个方法来自于 scheduler 包
          • 这个方法涉及比较多,先跳过
        • callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
      • 它最后返回 一个 callbackID, 这个id用于后期 cancel 的标识,cancelDeferredCallback(callbackID);
        • 这里之前也说了,如果新的任务优先级更高,需要把老的取消,再调用新的callback
      • 而里面的参数 performAsyncWork
        • requestWork 中,当 expirationTime === Sync 时,调用的也是 performSyncWork 这个是同步的
        • 而如果是异步,则调用 scheduleCallbackWithExpirationTime 函数,最终调用的是这里的 performAsyncWork
        • 所以,这两个是对应的,同步和异步
  • 进入 scheduleDeferredCallback 函数的源码 packages/scheduler/src/Scheduler.js 找到 unstable_scheduleCallback

    function unstable_scheduleCallback(callback, deprecated_options) {
      var startTime =
        currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
    
      var expirationTime;
      if (
        typeof deprecated_options === 'object' &&
        deprecated_options !== null &&
        typeof deprecated_options.timeout === 'number'
      ) {
        // FIXME: Remove this branch once we lift expiration times out of React.
        expirationTime = startTime + deprecated_options.timeout;
      } else {
        switch (currentPriorityLevel) {
          case ImmediatePriority:
            expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
            break;
          case UserBlockingPriority:
            expirationTime = startTime + USER_BLOCKING_PRIORITY;
            break;
          case IdlePriority:
            expirationTime = startTime + IDLE_PRIORITY;
            break;
          case NormalPriority:
          default:
            expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
        }
      }
    
      var newNode = {
        callback,
        priorityLevel: currentPriorityLevel,
        expirationTime,
        next: null,
        previous: null,
      };
    
      // Insert the new callback into the list, ordered first by expiration, then
      // by insertion. So the new callback is inserted any other callback with
      // equal expiration.
      if (firstCallbackNode === null) {
        // This is the first callback in the list.
        firstCallbackNode = newNode.next = newNode.previous = newNode;
        ensureHostCallbackIsScheduled();
      } else {
        var next = null;
        var node = firstCallbackNode;
        do {
          if (node.expirationTime > expirationTime) {
            // The new callback expires before this one.
            next = node;
            break;
          }
          node = node.next;
        } while (node !== firstCallbackNode);
    
        if (next === null) {
          // No callback with a later expiration was found, which means the new
          // callback has the latest expiration in the list.
          next = firstCallbackNode;
        } else if (next === firstCallbackNode) {
          // The new callback has the earliest expiration in the entire list.
          firstCallbackNode = newNode;
          ensureHostCallbackIsScheduled();
        }
    
        var previous = next.previous;
        previous.next = next.previous = newNode;
        newNode.next = next;
        newNode.previous = previous;
      }
    
      return newNode;
    }
    
    • 首先看 参数 callback, deprecated_options
      • callback 是传进来的 performAsyncWork
      • deprecated_options 是即将被废弃的 optinos,这个即将被废弃
    • 接着处理 var startTime = currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
      • getCurrentTime 是重新计算一个 xx.now()
        if (hasNativePerformanceNow) {
          var Performance = performance;
          getCurrentTime = function() {
            return Performance.now();
          };
        } else {
          getCurrentTime = function() {
            return localDate.now();
          };
        }
        
      • 这里,浏览器平台是这个 localDate.now();
    • 下面有个判断if (typeof deprecated_options === 'object' && deprecated_options !== null && typeof deprecated_options.timeout === 'number')
      • 接着判断 deprecated_options 这个参数,存在则计算出 expirationTime
        // FIXME: Remove this branch once we lift expiration times out of React.
        expirationTime = startTime + deprecated_options.timeout;
        
        • 当把 expirationTime 相关的逻辑提取出来之后,这个 if判断就被删除了,后面只有 else 里面的东西了
        • 所以说,这个 deprecated_options 即将被废弃
    • 如果走到 else 里面,进行switch case currentPriorityLevel
      • 可以看下各个常量的值
        var maxSigned31BitInt = 1073741823;
        
        // Times out immediately
        var IMMEDIATE_PRIORITY_TIMEOUT = -1;
        // Eventually times out
        var USER_BLOCKING_PRIORITY = 250;
        var NORMAL_PRIORITY_TIMEOUT = 5000;
        // Never times out
        var IDLE_PRIORITY = maxSigned31BitInt;
        
      • 也就是说,将来很可能会把 expirationTime 相关逻辑移入 scheduler 包中
      • 之前在 packages/react-reconciler/src/ReactFiberReconciler.js 中
      • 不过,在目前的逻辑中 else 里面的东西,用不到
    • 接下去,创建 newNode 的对象
      var newNode = {
        callback,
        priorityLevel: currentPriorityLevel,
        expirationTime,
        next: null,
        previous: null,
      };
      
      • next 和 previous 是用来存储链表的数据结构的
    • 接下来 if (firstCallbackNode === null)
      • firstCallbackNode 是 scheduler 中维护的一个单项列表的头部
      • 如果匹配判断,说明传递进来的 callback 是第一个
        • 进行赋值处理 firstCallbackNode = newNode.next = newNode.previous = newNode;
        • 并调用 ensureHostCallbackIsScheduled();
    • 不匹配的时候
      • 有一个或多个callback, 则进行循环
      • 在循环中判断,node.expirationTime > expirationTime
        • 如果匹配,next = node; 并跳出循环
        • 这是 scheduler 对于传进来的所有callback, 按照 expirationTime 的大小,也就是优先级的高低进行排序
        • 它会把优先级更高的任务,排到最前面
      • 如果 next 是 null
        • 这个节点要插在callbackList里面的最后一个
      • 如果 next 是 firstCallbackNode,即第一个
        • 因为当前节点要插在这个单项列表最前面,优先级最高
        • 马上 firstCallbackNode 变化了,即更新了 firstCallbackNode = newNode;
        • 调用 ensureHostCallbackIsScheduled();
          • 这个函数在上面两处调用了,但是没有在 if (next === null) 中调用
            • 因为 这个条件下,firstCallbackNode 仍然处于第一位
            • 后续要调用的话,第一个被调用的还是 firstCallbackNode
            • 所以,顺序不会变,所以不需要重新调用 ensureHostCallbackIsScheduled();
          • 注意,调用上述方法会进入一个循环,循环的调用List里面的东西
          • 当 firstCallbackNode 变化了,才会去调用,因为头部变了
  • 下面为这个方法链表的处理示例

  • 接着,进入 ensureHostCallbackIsScheduled 这个方法让队列进入调度的过程
    function ensureHostCallbackIsScheduled() {
      if (isExecutingCallback) {
        // Don't schedule work yet; wait until the next time we yield.
        return;
      }
      // Schedule the host callback using the earliest expiration in the list.
      var expirationTime = firstCallbackNode.expirationTime;
      if (!isHostCallbackScheduled) {
        isHostCallbackScheduled = true;
      } else {
        // Cancel the existing host callback.
        cancelHostCallback();
      }
      requestHostCallback(flushWork, expirationTime);
    }
    
    • isExecutingCallback 变量表示已经调用callback, 直接 return
      • 代表着 已经有一个callbackNode 被调用了
      • 也就是我们传入的 performAsyncWork 正在被调用了
      • 进入被调用的过程,自动进入一个循环的过程
      • 就不需要再重新启动一次调度
    • 获取变量 var expirationTime = firstCallbackNode.expirationTime;
    • 如果没有被调度,标识正在被调度 isHostCallbackScheduled = true;
    • 否则,取消之前的回调 cancelHostCallback();
    • 最后 requestHostCallback, 进入这个方法,有很多种场景分别定义,但是找到我们需要的场景,搜索该方法名
      • 排除 mock, 非浏览器环境的判断
      • 并进入直接到 else 中
        // 这里跳过很多代码
        // ...
        // 主要在这里
        requestHostCallback = function(callback, absoluteTimeout) {
          scheduledHostCallback = callback;
          timeoutTime = absoluteTimeout;
          if (isFlushingHostCallback || absoluteTimeout < 0) {
            // Don't wait for the next frame. Continue working ASAP, in a new event.
            window.postMessage(messageKey, '*');
          } else if (!isAnimationFrameScheduled) {
            // If rAF didn't already schedule one, we need to schedule a frame.
            // TODO: If this rAF doesn't materialize because the browser throttles, we
            // might want to still have setTimeout trigger rIC as a backup to ensure
            // that we keep performing work.
            isAnimationFrameScheduled = true;
            requestAnimationFrameWithTimeout(animationTick);
          }
        };
        
        cancelHostCallback = function() {
          scheduledHostCallback = null;
          isMessageEventScheduled = false;
          timeoutTime = -1;
        };
        
    • 上述 requestHostCallback 是我们需要关注的点
      • cheduledHostCallback = callback; 读取 callback
      • timeoutTime = absoluteTimeout; 是我们传进来的 expirationTime
      • 接着判断 if (isFlushingHostCallback || absoluteTimeout < 0)
        • 这两种情况,不需要等待下一帧去做这个事情
        • 而是以最快的速度进入这个方法的调用 window.postMessage(messageKey, '*');
        • absoluteTimeout < 0 说明已经超时了
      • 不符合上述条件,按照正常的调度流程去走
        • 判断 isAnimationFrameScheduled 这个变量的状态
        • 如果它没有设置为 true, 则还没有进入调度循环的过程
        • 这时候就把它设置为 true, 并执行 requestAnimationFrameWithTimeout(animationTick);
        • 进入 requestAnimationFrameWithTimeout
          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()); // 这里 getCurrentTime 是一个模拟 timestamp 的参数
            }, ANIMATION_FRAME_TIMEOUT);
          };
          
          • 这里的 localRequestAnimationFrame 相当于 window.requestAnimationFrame
            • 它内部做了两件事,清理 timeout, 执行callback
            • 这个 callback 就是我们传进来的 animationTick 这个方法
            • rAFTimeoutID 是下面的 timeout 定时器
            • 这个定时器的作用是: 如果 localRequestAnimationFrame 一直没有调用,超时了,这边设置的时间是 100ms
            • 超时后,取消 localRequestAnimationFrame 的调用,并且直接调用 callback(getCurrentTime());
            • 也就是下一帧的时间必须在 100ms之内被调用
            • 这个方法的作用就是,防止 localRequestAnimationFrame 太长时间没有被调用
            • 里面有相互取消的操作,这里面有一个竞争关系,谁先触发,谁先调用
        • 同样,参数这里 animationTick 也是个方法
          var animationTick = function(rafTime) {
            // 这里会匹配到
            if (scheduledHostCallback !== null) {
              // Eagerly schedule the next animation callback at the beginning of the
              // frame. If the scheduler queue is not empty at the end of the frame, it
              // will continue flushing inside that callback. If the queue *is* empty,
              // then it will exit immediately. Posting the callback at the start of the
              // frame ensures it's fired within the earliest possible frame. If we
              // waited until the end of the frame to post the callback, we risk the
              // browser skipping a frame and not firing the callback until the frame
              // after that.
              // 因为 firstCallbackNode 是一个队列,里面会有很多 callback
              // 当前 animationTick 只执行一个 callback
              // 如果后续还有,也会在下一帧中去执行
              // 不期望等待callback执行完成后,再去请求下一帧,可能会跳过很多的时间
              // 所以在这里立马执行
              requestAnimationFrameWithTimeout(animationTick); // 立即进行调用,请求下一帧
            } else {
              // No pending work. Exit.
              // 如果下次进来,scheduledHostCallback 是没有的,则跳出
              isAnimationFrameScheduled = false;
              return;
            }
            // rafTime 是 animationTick 被调用的时间
            // frameDeadline 默认是 0
            // activeFrameTime 是 33,这个就是保持浏览器30帧的执行时间
            // 这里就是计算,这个方法到下一帧可以执行的时间
            var nextFrameTime = rafTime - frameDeadline + activeFrameTime;
            if (
              nextFrameTime < activeFrameTime &&
              previousFrameTime < activeFrameTime
            ) {
              if (nextFrameTime < 8) {
                // Defensive coding. We don't support higher frame rates than 120hz.
                // If the calculated frame time gets lower than 8, it is probably a bug.
                nextFrameTime = 8;
              }
              // If one frame goes long, then the next one can be short to catch up.
              // If two frames are short in a row, then that's an indication that we
              // actually have a higher frame rate than what we're currently optimizing.
              // We adjust our heuristic dynamically accordingly. For example, if we're
              // running on 120hz display or 90hz VR display.
              // Take the max of the two in case one of them was an anomaly due to
              // missed frame deadlines.
              activeFrameTime =
                nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;
            } else {
              previousFrameTime = nextFrameTime;
            }
            frameDeadline = rafTime + activeFrameTime;
            if (!isMessageEventScheduled) {
              isMessageEventScheduled = true;
              window.postMessage(messageKey, '*');
            }
          };
          
          • 第一次计算的 nextFrameTime 其实是没有用的,因为算出来的时间会比较大
          • 第二次进来,这时候 frameDeadline 就不是 0 了
          • requestAnimationFrameWithTimeout 又是连续调用的
          • 因为我们进入这个方法就会立马调用这个方法
          • 下个方法调用就是下一帧了,因为 requestAnimationFrame 是一帧一帧来调用的
          • 下一帧时间进来,又重新计算出来了一个 nextFrameTime
          • 这个时候,rafTime 是小于 frameDeadline 的,因为 frameDeadline 加上了一个完整帧的时间 33
          • 对于调用 requestAnimationFrame 的时候,是下一帧动画刚开始渲染的时候,肯定没有到 33 毫秒的时候
          • 这时候 nextFrameTime 是小于 33,说明机器的刷新频率高于30帧
          • if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime)
          • 这个判断的意义在于,如果连续两帧的调用都计算出来,发现小于 33 ms (目前的帧时间)
          • 那么就把帧时间 activeFrameTime 变小,因为使用 frameDeadline 的时候,activeFrameTime
          • 是非常重要的,frameDeadline = rafTime + activeFrameTime;
          • 说明在接下去的 33ms之内都是可以运行react更新的代码
          • 实际浏览器的刷新时间都要小于33ms, 比如 10ms, 这时候,占用33ms去渲染react应用
          • 就会导致浏览器刷新动画的时间,非常不够,就导致动画变得比较卡顿
          • 这个是考虑不同平台刷新频率的问题,不如 VR平台对刷新要求比较高
          • 如果 nextFrameTime < 8 这时候 nextFrameTime = 8
          • 这说明react目前不支持每帧小于8ms的场景
          • 通过以上前后几次帧时间的判断,来判断平台的刷新频率来更新 activeFrameTime
          • 来减少 react 运行时间的目的
          • 但是 frameDeadline = rafTime + activeFrameTime; 这里计算出的 frameDeadline 要大于33的
            • 因为 activeFrameTime 是完整的一帧时间 33
            • 而每帧留给 react 更新的时间要小于 33
            • 一帧之内要处理 react的渲染 和 浏览器的更新,那么 react渲染一定要小于33
              这里算出的 frameDeadline 是 当前时间 + 33
          • 这是为什么呢?
            • 这里用了js中任务队列的概念,像是 setTimeout, window.postMessage
            • 都是把一个任务推到了一个队列里面, 然后再继续执行当前 js 的任务
            • 对于浏览器来说,animationTick 是在 requestAnimationFrameWithTimeout 的callback中调用
            • animationTick 方法执行完之后,立马进入浏览器动画刷新的流程
            • 下面调用的 window.postMessage 要等到浏览器动画或用户反馈执行完了之后,才会执行 postMessage 的功能
            • 这意味着需要等到浏览器刷新完成后,才会接收到 postMessage 的意图
            • 这时候浏览器刷新动画的时间已经过了,相当于 rafTime + activeFrameTime 的时间已经流失掉一部分了(浏览器刷新需时)
            • 剩下的时间给 react 执行更新的
          • 这就是 react scheduler 中模拟 requestIdleCallback 的方法,通过 requestAnimationFrame 调用完 callback 之后
          • 立马进入浏览器的动画更新的设定,在下面的判断中给任务队列插入一个任务
          • if (!isMessageEventScheduled)
            • isMessageEventScheduled = true;
            • window.postMessage(messageKey, '*');
          • 在浏览器执行完之后,调用任务队列, 这个时间总共加起来是 33ms
          • 当发送完 postMessage 到了哪里?可看到
            window.addEventListener('message', idleTick, false)
            
          • 进入 idleTick
            var idleTick = function(event) {
              // 先判断 key
              if (event.source !== window || event.data !== messageKey) {
                return;
              }
            
              isMessageEventScheduled = false;
              // 赋值一份 callback
              var prevScheduledCallback = scheduledHostCallback;
              // 同样处理 timeout
              var prevTimeoutTime = timeoutTime;
              // 重置下面两个
              scheduledHostCallback = null;
              timeoutTime = -1;
              // 获取当前时间
              var currentTime = getCurrentTime();
            
              var didTimeout = false;
              // 这个条件如果 <= 0 说明浏览器动画或用户反馈超过 33ms, 意思是,把这一帧的时间已经用完了
              // 对于 react 来说,它已经没有时间执行它的更新了
              if (frameDeadline - currentTime <= 0) {
                // There's no time left in this idle period. Check if the callback has
                // a timeout and whether it's been exceeded.
                // 进入上述条件,它需要继续判断 timeout 是否已经过期,或者小于当前时间(说明任务也已经过期了)
                // 如果任务已经过期,这个任务就需要强行被更新
                // 可以在任务没有过期的时候,判断帧时间如果没有了,即: frameDeadline - currentTime <= 0
                // 先跳过,等下一帧来更新,但是在任务已经过期的时候,就需要强制执行了,于是就设置了下面的 didTimeout = true;
                if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
                  // Exceeded the timeout. Invoke the callback even though there's no
                  // time left.
                  didTimeout = true;
                } else {
                  // No timeout.
                  // 没有过期,并且 isAnimationFrameScheduled === false 去调用 requestAnimationFrameWithTimeout 这个方法
                  if (!isAnimationFrameScheduled) {
                    // Schedule another animation callback so we retry later.
                    // 恢复
                    isAnimationFrameScheduled = true;
                    requestAnimationFrameWithTimeout(animationTick);
                  }
                  // Exit without invoking the callback.
                  scheduledHostCallback = prevScheduledCallback;
                  timeoutTime = prevTimeoutTime;
                  return;
                }
              }
              // 接来下如果 存在 prevScheduledCallback 则设置 isFlushingHostCallback 并调用 prevScheduledCallback
              if (prevScheduledCallback !== null) {
                isFlushingHostCallback = true;
                try {
                  prevScheduledCallback(didTimeout);
                } finally {
                  isFlushingHostCallback = false;
                }
              }
            };
            
            • 这里最后的 prevScheduledCallback 向上溯源,找到 ensureHostCallbackIsScheduled
            function ensureHostCallbackIsScheduled() {
              if (isExecutingCallback) {
                // Don't schedule work yet; wait until the next time we yield.
                return;
              }
              // Schedule the host callback using the earliest expiration in the list.
              var expirationTime = firstCallbackNode.expirationTime;
              if (!isHostCallbackScheduled) {
                isHostCallbackScheduled = true;
              } else {
                // Cancel the existing host callback.
                cancelHostCallback();
              }
              requestHostCallback(flushWork, expirationTime);
            }
            
            • 可以看到这里 requestHostCallback(flushWork, expirationTime); 传入了 flushWork 方法
              • 输出任务需要调用 flushWork 方法
            • 现在来看下 flushWork 方法, 这是 react scheduler 调度到 要执行 callback 的流程
            • 在执行callback的时候,调用了 flushWork 这个方法
              // didTimeout 参数是 firstCallbackNode 的 expirationTime 是否已超时
              function flushWork(didTimeout) {
                // 真正调用 callback 设置为 true
                // 对于 ensureHostCallbackIsScheduled 方法来说,如果为 true, 则直接 return 了
                isExecutingCallback = true;
                deadlineObject.didTimeout = didTimeout; // deadlineObject 是上层设置的一个通用的对象
                try {
                  if (didTimeout) {
                    // Flush all the expired callbacks without yielding.
                    while (firstCallbackNode !== null) {
                      // Read the current time. Flush all the callbacks that expire at or
                      // earlier than that time. Then read the current time again and repeat.
                      // This optimizes for as few performance.now calls as possible.
                      var currentTime = getCurrentTime();
                      if (firstCallbackNode.expirationTime <= currentTime) {
                        // 执行 callbackNode 的链表直到遇到第一个不过期的为止,把已过期的任务都强制输出
                        do {
                          flushFirstCallback();
                        } while (
                          firstCallbackNode !== null &&
                          firstCallbackNode.expirationTime <= currentTime
                        ); // 这里 firstCallbackNode 是 next, firstCallbackNode.expirationTime <= currentTime 这表示下一个节点的任务还是过期的任务
                        continue; // 这里continue 跳出后即 break 跳出 外层while循环
                      }
                      break;
                    }
                  } else {
                    // 这里表示没有任务是过期的
                    // Keep flushing callbacks until we run out of time in the frame.
                    if (firstCallbackNode !== null) {
                      do {
                        flushFirstCallback();
                      } while (
                        firstCallbackNode !== null &&
                        getFrameDeadline() - getCurrentTime() > 0
                      ); // getFrameDeadline() - getCurrentTime() > 0 表示 有空,闲暇 会执行 flushFirstCallback
                    }
                  }
                } finally {
                  isExecutingCallback = false;
                  // 最后,如果还有,再次进入调度
                  if (firstCallbackNode !== null) {
                    // There's still work remaining. Request another callback.
                    ensureHostCallbackIsScheduled();
                  } else {
                    isHostCallbackScheduled = false;
                  }
                  // Before exiting, flush all the immediate work that was scheduled.
                  flushImmediateWork();
                }
              }
              
              • 关于这里的 deadlineObject
              var deadlineObject = {
                timeRemaining,
                didTimeout: false,
              };
              
              • 而这里的 timeRemaining 是一个方法
              var timeRemaining;
              if (hasNativePerformanceNow) {
                timeRemaining = function() {
                  // 这个判断不成立,跳过
                  if (
                    firstCallbackNode !== null &&
                    firstCallbackNode.expirationTime < currentExpirationTime
                  ) {
                    // A higher priority callback was scheduled. Yield so we can switch to
                    // working on that.
                    return 0;
                  }
                  // We assume that if we have a performance timer that the rAF callback
                  // gets a performance timer value. Not sure if this is always true.
                  // getFrameDeadline() 方法就是 frameDeadline = rafTime + activeFrameTime;
                  // 也就是说,确定,这一帧的渲染时间,是否已经超过
                  var remaining = getFrameDeadline() - performance.now();
                  return remaining > 0 ? remaining : 0;
                };
              } else {
                timeRemaining = function() {
                  // Fallback to Date.now()
                  if (
                    firstCallbackNode !== null &&
                    firstCallbackNode.expirationTime < currentExpirationTime
                  ) {
                    return 0;
                  }
                  var remaining = getFrameDeadline() - Date.now();
                  return remaining > 0 ? remaining : 0;
                };
              }
              
              • 这里根据 hasNativePerformanceNow 来进行一个区分,这两个基本差不多,选择其一
              • 所以,timeRemaining 用于计算还剩多少时间,比如在 ReactFiberSchedler.js中的 shouldYield
                // When working on async work, the reconciler asks the renderer if it should
                // yield execution. For DOM, we implement this with requestIdleCallback.
                function shouldYield() {
                  if (deadlineDidExpire) {
                    return true;
                  }
                  if (
                    deadline === null ||
                    deadline.timeRemaining() > timeHeuristicForUnitOfWork
                  ) {
                    // Disregard deadline.didTimeout. Only expired work should be flushed
                    // during a timeout. This path is only hit for non-expired work.
                    return false;
                  }
                  deadlineDidExpire = true;
                  return true;
                }
                
                • 这里 deadline.timeRemaining() > timeHeuristicForUnitOfWork, 这里 timeHeuristicForUnitOfWork 是 1
                • 用剩下时间是否 > 1 来判断是否已经过期了,如果 > 1 说明还有时间执行 react的更新,这里 return false
                • 如果剩下时间 < 1 了,代表这一帧的渲染时间已经超时,设置全局变量 deadlineDidExpire = true; 并且 return true
                • 这个 shouldYield 方法就是判断这个任务要跳出还是继续执行下去
              • 接着是一个 try finally 里面
                • if (didTimeout) 这时候已经过期了,进入while循环 while (firstCallbackNode !== null)
                • 里面 有个if 判断是肯定匹配的,if (firstCallbackNode.expirationTime <= currentTime)
                  • 执行 do while
                  • flushFirstCallback 是真正调用callback的方法
                    function flushFirstCallback() {
                      var flushedNode = firstCallbackNode;
                    
                      // Remove the node from the list before calling the callback. That way the
                      // list is in a consistent state even if the callback throws.
                      var next = firstCallbackNode.next;
                      // 说明链表里只有一个节点,直接设置 null
                      if (firstCallbackNode === next) {
                        // This is the last callback in the list.
                        firstCallbackNode = null;
                        next = null;
                      } else {
                        // 如果不是,多个节点,则构建链表(环形链表) 
                        var lastCallbackNode = firstCallbackNode.previous;
                        firstCallbackNode = lastCallbackNode.next = next; // firstCallbackNode 变成了 next 的节点 对应上面调用方法的 do while firstCallbackNode.exirationTime <= currentTime 
                        next.previous = lastCallbackNode;
                      }
                      // 把之前的指向清空,如果指针还留着,可能会导致问题
                    
                      flushedNode.next = flushedNode.previous = null;
                    
                      // Now it's safe to call the callback.
                      var callback = flushedNode.callback;
                      var expirationTime = flushedNode.expirationTime;
                      var priorityLevel = flushedNode.priorityLevel;
                      var previousPriorityLevel = currentPriorityLevel;
                      var previousExpirationTime = currentExpirationTime;
                      currentPriorityLevel = priorityLevel;
                      currentExpirationTime = expirationTime;
                      var continuationCallback;
                      try {
                        // 这里 callback 就是传进来的 performAsyncWork
                        continuationCallback = callback(deadlineObject); // 这里应该是 undefined
                      } finally {
                        currentPriorityLevel = previousPriorityLevel;
                        currentExpirationTime = previousExpirationTime;
                      }
                    
                      // A callback may return a continuation. The continuation should be scheduled
                      // with the same priority and expiration as the just-finished callback.
                      // 因为 performAsyncWork 这个 callback 没有返回值 所以这个目前来说不成立,也许后续会有用
                      if (typeof continuationCallback === 'function') {
                        var continuationNode: CallbackNode = {
                          callback: continuationCallback,
                          priorityLevel,
                          expirationTime,
                          next: null,
                          previous: null,
                        };
                    
                        // Insert the new callback into the list, sorted by its expiration. This is
                        // almost the same as the code in `scheduleCallback`, except the callback
                        // is inserted into the list *before* callbacks of equal expiration instead
                        // of after.
                        if (firstCallbackNode === null) {
                          // This is the first callback in the list.
                          firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
                        } else {
                          var nextAfterContinuation = null;
                          var node = firstCallbackNode;
                          do {
                            if (node.expirationTime >= expirationTime) {
                              // This callback expires at or after the continuation. We will insert
                              // the continuation *before* this callback.
                              nextAfterContinuation = node;
                              break;
                            }
                            node = node.next;
                          } while (node !== firstCallbackNode);
                    
                          if (nextAfterContinuation === null) {
                            // No equal or lower priority callback was found, which means the new
                            // callback is the lowest priority callback in the list.
                            nextAfterContinuation = firstCallbackNode;
                          } else if (nextAfterContinuation === firstCallbackNode) {
                            // The new callback is the highest priority callback in the list.
                            firstCallbackNode = continuationNode;
                            ensureHostCallbackIsScheduled();
                          }
                    
                          var previous = nextAfterContinuation.previous;
                          previous.next = nextAfterContinuation.previous = continuationNode;
                          continuationNode.next = nextAfterContinuation;
                          continuationNode.previous = previous;
                        }
                      }
                    }
                    
                • 在finally 中执行了 ensureHostCallbackIsScheduled
                  • 在这个方法中,有个判断是 isHostCallbackScheduled 仅且仅有 在这个判断中,isHostCallbackScheduled 才会被设置为 true
                  • 所以,在有 firstCallbackNode 时,调用 ensureHostCallbackIsScheduled() 时,isHostCallbackScheduled 是 true 的
                  • 它为 true 时,在 ensureHostCallbackIsScheduled() 中,会执行 cancelHostCallback()
                    cancelHostCallback = function() {
                      scheduledHostCallback = null;
                      isMessageEventScheduled = false;
                      timeoutTime = -1;
                    };
                    
                  • 也就是说把之前调度的变量都重置了,不能让老的callback再执行一遍,以此可能导致产生错误
                  • 在 finally 中的 else 环节 isHostCallbackScheduled 被设置成 false
                • 最后执行了 flushImmediateWork 这个API 以后可能会用到
                  function flushImmediateWork() {
                    // 这里 ImmediatePriority 是一个固定的值 但是 firstCallbackNode.priorityLevel
                    // 这里 firstCallbackNode.priorityLevel 是 固定的 3 这个if 不会被执行
                    // 所以,这个api 暂未开放
                    if (
                      // Confirm we've exited the outer most event handler
                      currentEventStartTime === -1 &&
                      firstCallbackNode !== null &&
                      firstCallbackNode.priorityLevel === ImmediatePriority
                    ) {
                      isExecutingCallback = true;
                      deadlineObject.didTimeout = true;
                      try {
                        do {
                          flushFirstCallback();
                        } while (
                          // Keep flushing until there are no more immediate callbacks
                          firstCallbackNode !== null &&
                          firstCallbackNode.priorityLevel === ImmediatePriority
                        );
                      } finally {
                        isExecutingCallback = false;
                        if (firstCallbackNode !== null) {
                          // There's still work remaining. Request another callback.
                          ensureHostCallbackIsScheduled();
                        } else {
                          isHostCallbackScheduled = false;
                        }
                      }
                    }
                  }
                  

总结

  • 以上是 react scheduler 模拟 requestIdleCallback 时间片的操作和调度
  • 能够控制把更多的优先权交给浏览器,让它去做动画或用户输入反馈的更新
  • 在有空闲的时间,回过头来执行 react 的异步更新操作
  • 在这里面会有各种各样的计时来控制帧时间的判断
  • 如果发现浏览器的刷新频率更高,则调低帧时间,以及判断任务是否有过期
  • 如果过期了,需要强制输出
  • 从这个React版本来看,react scheduler 只开放了一部分代码,还有一部分代码暂时没有用到
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值