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 setTimeoutrAFID = localRequestAnimationFrame(function(timestamp) {// cancel the setTimeoutlocalClearTimeout(rAFTimeoutID);callback(timestamp);});rAFTimeoutID = localSetTimeout(function() {// cancel the requestAnimationFramelocalCancelAnimationFrame(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) {// 先判断 keyif (event.source !== window || event.data !== messageKey) {return;}isMessageEventScheduled = false;// 赋值一份 callbackvar prevScheduledCallback = scheduledHostCallback;// 同样处理 timeoutvar 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 并调用 prevScheduledCallbackif (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;// 说明链表里只有一个节点,直接设置 nullif (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 就是传进来的 performAsyncWorkcontinuationCallback = 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 handlercurrentEventStartTime === -1 &&firstCallbackNode !== null &&firstCallbackNode.priorityLevel === ImmediatePriority) {isExecutingCallback = true;deadlineObject.didTimeout = true;try {do {flushFirstCallback();} while (// Keep flushing until there are no more immediate callbacksfirstCallbackNode !== 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.hqwc.cn/news/409680.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

QT上位机开发(MFC vs QT)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 在qt之前&#xff0c;上位机开发的主要方法就是mfc。后来出现了c#语言之后&#xff0c;上位机的开发就有一部分人转成了c#。这些开发都是在windows…

基于Xilinx K7-410T的高速DAC之AD9129开发笔记(二)

引言&#xff1a;上一篇文章我们简单介绍了AD9129的基础知识&#xff0c;包括芯片的重要特性&#xff0c;外部接口相关的信号特性等。本篇我们重点介绍下项目中FPGA与AD9129互联的原理图设计&#xff0c;包括LVDS IO接口设计、时钟电路以、供电设计以及PCB设计。 LVDS数据接口设…

Jenkins-Maven Git

整合Maven 安装GIT #更新yum sudo yum update #安装git yum install git 安装Maven插件,在插件管理中心&#xff1a; 配置仓库 配置密码认证 我们可以在这个目录下看到Jenkins 帮我们拉取了代码 /env/liyong/data/docker/jenkins_mount/workspace/maven-job 配置maven打包…

IOS-高德地图SDK接入-Swift

申请key 这个要前往高德开发平台注册成为个人开发者然后在控制台创建一个应用&#xff1a; 高德开发平台 注册步骤就不写了&#xff0c;写一下创建应用的步骤&#xff1a; 1、点击应用管理——>我的应用 2、点击右上角的创建新应用 3、输入内容&#xff1a; 4、点击添加ke…

模拟瑞幸小程序购物车

是根据渡一袁老师的大师课写的&#xff0c;如有什么地方存在问题&#xff0c;还请大家指出来哟ど⁰̷̴͈꒨⁰̷̴͈う♡&#xff5e; index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta http-e…

网页设计(八)HTML5基础与CSS3应用

一、当当网企业用户注册页面设计 当当网企业用户注册页面 改版后当当网企业用户注册页面 <!-- prj_8_1.html --> <!DOCTYPE html> <html><head><meta charset"UTF-8"><title>当当网企业用户注册页面设计</title><s…

ARM day1

一、概念 ARM可以工作的七种模式用户、系统、快中断、中断、管理、终止、未定义ARM核的寄存器个数 37个32位长的寄存器&#xff0c;当前处理器的模式决定着哪组寄存器可操作&#xff0c;且任何模式都可以存取&#xff1a; PC(program counter程序计数器) CPSR(current program…

MySQL-多表联合查询

&#x1f389;欢迎您来到我的MySQL基础复习专栏 ☆* o(≧▽≦)o *☆哈喽~我是小小恶斯法克&#x1f379; ✨博客主页&#xff1a;小小恶斯法克的博客 &#x1f388;该系列文章专栏&#xff1a;重拾MySQL &#x1f379;文章作者技术和水平很有限&#xff0c;如果文中出现错误&am…

领域驱动设计应用之WebAPI

领域驱动设计应用之WebAPI 此篇文章主要讲述领域驱动设计在WebApi中的应用&#xff0c;以及设计方式&#xff0c;这种设计的原理以及有点。 文章目录 领域驱动设计应用之WebAPI前言一、相对于传统设计模式的有点二、WebAPI对接中的使用案例业务拆分父类设计HttpResponse(返回)…

逻辑回归(解决分类问题)

定义&#xff1a;逻辑回归是一种用于解决分类问题的统计学习方法。它通过对数据进行建模&#xff0c;预测一个事件发生的概率。逻辑回归通常用于二元分类问题&#xff0c;即将数据分为两个类别。它基于线性回归模型&#xff0c;但使用了逻辑函数&#xff08;也称为S形函数&…

BDD(Behavior-Driven Development)行为驱动开发介绍

为什么需要BDD&#xff1f; “开发软件系统最困难的部分就是准确说明开发什么” (“The hardest single part of building a software system is deciding precisely what to build” — No Silver Bullet, Fred Brooks) 。 看一下下面的开发场景&#xff1a; 场景一&#xf…

新书速览|数据科学技术:文本分析和知识图谱

图解数据科学&#xff0c;文本分析、知识图谱详解&#xff0c;实战案例呈现&#xff0c;助你深入理解技术原理&#xff0c;行业应用启发&#xff0c;助力创新发展。 本书内容 数据科学的关键技术包括数据存储计算、数据治理、结构化数据分析、语音分析、视觉分析、文本分析和知…