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

news2024/11/28 23:08:21

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 只开放了一部分代码,还有一部分代码暂时没有用到

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1392240.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【报错】NVIDIA 驱动版本不兼容 — NVIDIA driver on your system is too old

【报错】NVIDIA 驱动版本不兼容 — NVIDIA driver on your system is too old 报错信息查看torch版本查看nvidia驱动版本 报错信息 CUDA initialization: The NVIDIA driver on your system is too old (found version 11040). Please update your GPU driver by downloading …

投资半导体行业可靠吗?九方智投洪帮主助投资者深度剖析市场

近期,CES2024展会火热袭来,作为行业内知名活动,展会的官方数据显示本次参展企业达到了4000家。作为CES2024的特色主题,人工智能有望成为会议的重大亮点。 人工智能涉及到的领域包含方方面面,其中AI软件和硬件解决方案的发布将促进半导体行业需求,算力芯片和边缘侧AI的机会值得关…

纯前端实现加减运算验证码

纯前端实现加减运算验证码 实现效果 //页面展示 <template><view class"form-input-item" style"padding:8rpx 22rpx;"><input class"form-input" placeholder"请输入验证码" type"text" maxlength"6…

【计算机硬件】3、输入输出技术、总线结构

文章目录 输入输出技术内存与接口地址的编址方法1、 内存与接口地址独立编址方法2、内存与接口地址统一编址方法 计算机和外设间的数据交互方式1、程序控制(查询)方式2、程序中断方式3、DMA方式&#xff08;直接主存存取&#xff09; 总线结构 输入输出技术 内存与接口地址的编…

智能组网和云桥通sd-wan企业组网技术方案的区别对比

智能组网和云桥通sd-wan企业组网是两种不同的网络技术方案&#xff0c;它们在设计理念、应用场景和功能特点上存在以下这些区别&#xff1a; 1. 定义与设计理念&#xff1a; 智能组网&#xff1a; 智能组网是一种综合利用人工智能、自动化和网络管理技术的网络组建和管理方案。…

链表练习 Leetcode82.删除排序链表中的重复元素 II

题目传送门&#xff1a;Leetcode82 给定一个已排序的链表的头 head &#xff0c; 删除原始链表中所有重复数字的节点&#xff0c;只留下不同的数字 。返回 已排序的链表 。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,3,4,4,5] 输出&#xff1a;[1,2,5]示例 2&#xff1…

抖音直播间爆品如何打造培训教程课件

【干货资料持续更新&#xff0c;以防走丢】 抖音直播间爆品如何打造培训教程课件 部分资料预览 资料部分是网络整理&#xff0c;仅供学习参考。 抖音运营资料合集&#xff08;完整资料包含以下内容&#xff09; 目录 通过数据确定爆品的短视频自然流选方法 在抖音这样的短…

SC20-EVB ubuntu14.04 Andriod 5.1 SDK编译下载

1.ubuntu14.04安装环境配置 vi /etc/profile to add export JAVA_HOME/usr/lib/jvm/java-7-openjdk-amd64 export JRE_HOME J A V A H O M E / j r e e x p o r t C L A S S P A T H . : {JAVA_HOME}/jre export CLASSPATH.: JAVAH​OME/jreexportCLASSPATH.:{JAVA_HOME}/lib…

亚信安慧AntDB-S流式数据库实现企业数智化转型

AntDB-S流式数据库是一项针对实时数仓等场景的创新技术&#xff0c;旨在取代传统的流式处理引擎&#xff0c;从而简化开发和测试过程。该流式数据库具备强大的ACID特性和高可用性&#xff0c;为企业降低了流式业务开发和维护的成本。AntDB-S不仅将数据存储、计算和管理集成为一…

SQL语句详解四-DQL(数据查询语言-多表查询一)

文章目录 表和表的关系一对一关系一对多、多对一关系多对多关系 表和表的关系 概述&#xff1a;数据库中表的关系有三种&#xff0c;一对一关系、一对多的关系、多对多的关系。 一对一关系 例如&#xff1a;一个人只能有一个身份证号&#xff0c;一个身份证号只属于一个人 示…

集合框架面试

1.常见的集合有哪些 主要分为3种List、Map、Set 2.ArrayList和LinkedList有什么区别 数据结构不同&#xff1a;ArrayList是基于数组实现的&#xff0c;LinkedList是双向链表实现使用场景不同&#xff1a;ArrayList更利于查找&#xff0c;LinkedList利于增删是否支持随机访问…

92.乐理基础-记号篇-演奏记号(三)刮奏、琶音

内容参考于&#xff1a;三分钟音乐社 上一个内容&#xff1a;91.乐理基础-记号篇-演奏记号&#xff08;二&#xff09;保持音、滑音-CSDN博客 下图红框里是之前的内容&#xff1a; 刮奏&#xff1a;它是滑音操作层面上的说法&#xff0c;可以把滑音理解成它是一种效果&#x…

kibana查看和展示es数据

本文来说下使用kibana查看和展示es数据 文章目录 数据准备查询所有文档示例kibana查看和展示es数据 数据准备 可以使用es的命令或者java程序来往&#xff0c;es进行新增数据 查询所有文档示例 在 apifox 中&#xff0c;向 ES 服务器发 GET请求 &#xff1a;http://localhost:92…

Live800:满足客户的情感需求:提升客户服务的新维度

在当前的商业环境中&#xff0c;提供优质的产品或服务已不再足够。消费者现在更关注他们与品牌的关系&#xff0c;以及品牌如何满足他们的情感需求。这就要求企业在提供客户服务时&#xff0c;不仅要解决客户的实际问题&#xff0c;也要关注和满足客户的情感需求。今天将探讨如…

深度强化学习Task1:马尔可夫过程、DQN算法回顾

本篇博客是本人参加Datawhale组队学习第一次任务的笔记 【教程地址】https://github.com/datawhalechina/joyrl-book 【强化学习库JoyRL】https://github.com/datawhalechina/joyrl/tree/main 【JoyRL开发周报】 https://datawhale.feishu.cn/docx/OM8fdsNl0o5omoxB5nXcyzsInGe…

基于Web的航空航天数字博物馆推荐系统

介绍 项目背景&#xff1a; 航空航天数字博物馆推荐系统是一个基于Web开发的应用&#xff0c;旨在为用户提供一个全面的航空航天领域的数字博物馆体验。通过展品展示、分类筛选和个性化推荐等功能&#xff0c;用户可以更好地了解航空航天知识和文化&#xff0c;并丰富参观体验…

笔试面试题——继承和多态

&#x1f4d8;北尘_&#xff1a;个人主页 &#x1f30e;个人专栏:《Linux操作系统》《经典算法试题 》《C》 《数据结构与算法》 ☀️走在路上&#xff0c;不忘来时的初心 文章目录 一、什么是多态&#xff1f;二、什么是重载、重写(覆盖)、重定义(隐藏)&#xff1f;三、 inli…

Open CASCADE学习|显示模型

目录 1、编写代码 Viewer.h Viewer.cpp ViewerInteractor.h ViewerInteractor.cpp helloworld.cpp 2、配置 3、编译运行 1、编写代码 Viewer.h #pragma once ​ #ifdef _WIN32 #include <Windows.h> #endif ​ // Local includes #include "ViewerInteract…

奥伦德光电耦合器5G通信领域及其相关领域推荐

光电耦合器是以光为媒介传输电信号的一种电-光-电转换器件。由于该器件使用寿命长、工作温度范围宽&#xff0c;所以在过程控制、工业通信、家用电器、医疗设备、通信设备、计算机以及精密仪器等方面有着广泛应用在当前工艺技术持续发展与提升的过程中&#xff0c;其工作速度、…

测试的基本概念

1、什么是需求&#xff1f; 在企业中主要分为两类&#xff1a;用户需求和软件需求 用户需求&#xff1a;甲方的需求&#xff0c;或者终端用户使用产品时必须要完成的任务&#xff08;比较简略&#xff09;。 软件需求&#xff1a;或者叫功能需求&#xff0c;该需求会详细描述开…