美文网首页
React源码04 - Fiber Scheduler (调度器

React源码04 - Fiber Scheduler (调度器

作者: 晓风残月1994 | 来源:发表于2020-08-09 18:45 被阅读0次

    创建更新之后,找到 Root 然后进入调度,同步和异步操作完全不同,实现更新分片的性能优化。

    主流的浏览器刷新频率为 60Hz,即每(1000ms / 60Hz)16.6ms 浏览器刷新一次。JS可以操作 DOM,JS线程GUI渲染线程 是互斥的。所以 **JS脚本执行 **和 **浏览器布局、绘制 **不能同时执行。
    在每16.6ms时间内,需要完成如下工作:

    JS脚本执行 -----  样式布局 ----- 样式绘制
    

    既然以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器在每一帧 16.6ms 中执行完自己的 GUI 渲染线程后,还有剩余时间的话能通知我们执行 react 的异步更新任务,react 执行时会自己计时,如果时间到了,而 react 依然没有执行完,则会挂起自己,并把控制权还给浏览器,以便浏览器执行更高优先级的任务。然后 react 在下次浏览器空闲时恢复执行。而如果是同步任务,则不会中断,会一直占用浏览器直到页面渲染完毕。

    其实部分浏览器已经实现了这个API,这就是 requestIdleCallback(字面意思:请求空闲回调)。但是由于以下因素,React 放弃使用:

    • 浏览器兼容性
    • 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的 requestIdleCallback 触发的频率会变得很低。

    React 实现了功能更完备的 requestIdleCallback polyfill(使用window.requestAnimationFrame() 和 JavaScript 任务队列进行模拟),这就是 Scheduler,除了在空闲时触发回调的功能外,Scheduler 还提供了多种调度优先级供任务设置。

    当 Scheduler 将任务交给 Reconciler 后,Reconciler 会为变化的虚拟 DOM 打上代表增/删/更新的标记,类似这样:

    // 这种二进制存储数据:
    // 设置:集合 | 目标
    // 查询:集合 & 目标
    // 取消:集合 & ~目标
    
    export const Placement = /*             */ 0b0000000000010;
    export const Update = /*                */ 0b0000000000100;
    export const PlacementAndUpdate = /*    */ 0b0000000000110;
    export const Deletion = /*              */ 0b0000000001000;
    

    整个 Scheduler 与 Reconciler 的工作都在内存中进行。只有当所有组件都完成 Reconciler 的工作后,才会统一交给 Renderer。

    Renderer 根据 Reconciler 为虚拟 DOM 打的标记,同步执行对应的 DOM 操作。


    image

    isBatchingUpdates 变量在早前的调用栈中(我们为 onClick 绑定的事件处理函数会被 react 包裹多层),被标记为了 true ,然后 fn(a, b) 内部经过了3次 setState 系列操作,然后 finally 中 isBatchingUpdates 恢复为之前的 false,此时执行同步更新工作 performSyncWork

    image

    第2种:
    handleClick 中使用 setTimeoutthis.countNumber 包裹了一层 setTimeout(() => { this.countNumber()}, 0) ,同样要调用 handleClick 也是先经过 interactiveUpdates$1 上下文,也会执行 setTimeout ,然后 fn(a, b) 就执行完了,因为最终是浏览器来调用 setTimeout 的回调 然后执行里面的 this.countNumber ,而对于 interactiveUpdates$1 来说继续把自己的 performSyncWork 执行完,就算结束了。显然不管 performSyncWork 做了什么同步更新,我们的 setState 目前为止都还没得到执行。然后等到 setTimeout 的回调函数等到空闲被执行的时候,才会执行 setState ,此时没有了批量更新之上下文,所以每个 setState 都会单独执行一遍 requestWork 中的 performSyncWork 直到渲染结束,且不会被打断,3次 setState 就会整个更新渲染 3 遍(这样性能不好,所以一般不会这样写 react)。
    什么叫不会被打断的同步更新渲染?看一下 demo 中的输出,每次都同步打印出了最新的 button dom 的 innerText

    第3种:
    已经可以猜到,无非就是因为使用 setTimeout 而“错过了”第一次的批量更新上下文,那等到 setTimeout 的回调执行的时候,专门再创建一个批量更新上下文即可:

    image
    image

    **
    继续之前的源码,requestWork 的最后,如果不是同步的更新任务,那么就要参与 Scheduler 时间分片调度了:

      // TODO: Get rid of Sync and use current time?
      if (expirationTime === Sync) {
        performSyncWork();
      } else {
        scheduleCallbackWithExpirationTime(root, 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(); // 涉及到开发工具和polyfill,略过
      }
        
      // 如果是取消了老的调度任务,或者是尚未有调度任务,则接下来会安排调度
      callbackExpirationTime = expirationTime;
      // 计算出任务的timeout,也就是距离此刻还有多久过期
      const currentMs = now() - originalStartTimeMs; // originalStartTimeMs 代表react应用最初被加载的那一刻
      const expirationTimeMs = expirationTimeToMs(expirationTime);
      const timeout = expirationTimeMs - currentMs;
      // 类似于 setTimeout 返回的 ID,可以用来延期回调
      callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
    }
    

    6. unstable_scheduleCallback

    前面都还在 packages/react-reconciler/ReactFiberScheduler.js 中,下面就要跟着刚才的 **scheduleDeferredCallback **辗转进入到单独的 packages/scheduler 包中:

    • 根据不同优先级等级计算不同的 callbackNode 上的过期时间。
    • 存储以过期时间为优先级的环形链表,用时可借助首节点 firstCallbackNode 可对链表进行遍历读取。
    • firstCallbackNode 变了后要调用 ensureHostCallbackIsScheduled 重新遍历链表进行调度。
    image

    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;
    }
    

    7. ensureHostCallbackIsScheduled

    • 该方法名字就说明了目的是保证 callback 会被调度,故若已经有 callbackNode 在被调度,自会自动循环。
    • 从头结点,也就是最先过期的 callbackNode 开始请求调用,顺表如果有已存在的调用要取消。这就是之前说过的参与调用的任务有两种被打断的可能:1. 时间片到点了,2. 有更高优先级的任务参与了调度
    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 = 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) {
        // 没有超时,就常规安排,等待时间片
        isAnimationFrameScheduled = true;
        requestAnimationFrameWithTimeout(animationTick);
      }
    };
    
    // 取消之前安排的任务回调,就是重置一些变量
    cancelHostCallback = function() {
      scheduledHostCallback = null;
      isMessageEventScheduled = false;
      timeoutTime = -1;
    };
    

    为了模拟 requestIdleCallback API:
    传给 window.requestanimationframe 的回调函数会在浏览器下一次重绘之前执行,也就是执行该回调后浏览器下面会立即进入重绘。使用 window.postMessage 技巧将空闲工作推迟到重新绘制之后。
    具体太过复杂,就大概听个响吧,若要深究则深究:

    • animationTick
    • idleTick
    // 仅供示意
    requestAnimationFrameWithTimeout(animationTick);
    var animationTick = function(rafTime) {
      requestAnimationFrameWithTimeout(animationTick);
    }
    window.addEventListener('message', idleTick, false);
    window.postMessage(messageKey, '*');
    

    react 这里还能统计判断出平台刷新频率,来动态减少 react 自身运行所占用的时间片,支持的上限是 120hz 的刷新率,即每帧总共的时间不能低于 8ms。
    此间如果一帧的时间在执行 react js 之前就已经被浏览器用完,那么对于非过期任务,等待下次时间片;而对于过期任务,会强制执行。

    8. flushWork

    ensureHostCallbackIsScheduled 中的 requestHostCallback(flushWork, expirationTime) 参与时间片调度:
    flushWork:

    • 即使当前时间片已超时,也要把 callbackNode 链表中所有已经过期的任务先强制执行掉
    • 若当前帧还有时间片,则常规处理任务
    function flushWork(didTimeout) {
      isExecutingCallback = true;
      deadlineObject.didTimeout = didTimeout;
      try {
        // 把callbackNode链表中所有已经过期的任务先强制执行掉
        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) {
              do {
                flushFirstCallback();
              } while (
                firstCallbackNode !== null &&
                firstCallbackNode.expirationTime <= currentTime
              );
              continue;
            }
            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
            );
          }
        }
      } 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();
      }
    }
    

    flushFirstCallback 负责处理链表节点,然后执行 flushedNode.callback

    9. performWork

    • 是否有 deadline 的区分
    • 循环渲染 Root 的条件
    • 超过时间片的处理

    performSyncWork 不会传 deadline。
    没有deadline时,会循环执行 root 上的同步任务,或者任务过期了,也会立马执行任务。

    performAsyncWork:

    function performAsyncWork(dl) {
      if (dl.didTimeout) { // 是否过期
        if (firstScheduledRoot !== null) {
          recomputeCurrentRendererTime();
          let root: FiberRoot = firstScheduledRoot;
          do {
            didExpireAtExpirationTime(root, currentRendererTime);
            // The root schedule is circular, so this is never null.
            root = (root.nextScheduledRoot: any);
          } while (root !== firstScheduledRoot);
        }
      }
      performWork(NoWork, dl);
    }
    

    performSyncWork:

    function performSyncWork() {
      performWork(Sync, null);
    }
    

    performWork:

    function performWork(minExpirationTime: ExpirationTime, dl: Deadline | null) {
      deadline = dl;
    
      // Keep working on roots until there's no more work, or until we reach
      // the deadline.
      findHighestPriorityRoot();
    
      if (deadline !== null) {
        recomputeCurrentRendererTime();
        currentSchedulerTime = currentRendererTime;
    
        if (enableUserTimingAPI) {
          const didExpire = nextFlushedExpirationTime < currentRendererTime;
          const timeout = expirationTimeToMs(nextFlushedExpirationTime);
          stopRequestCallbackTimer(didExpire, timeout);
        }
    
        while (
          nextFlushedRoot !== null &&
          nextFlushedExpirationTime !== NoWork &&
          (minExpirationTime === NoWork ||
            minExpirationTime >= nextFlushedExpirationTime) &&
          (!deadlineDidExpire || currentRendererTime >= nextFlushedExpirationTime)
        ) {
          performWorkOnRoot(
            nextFlushedRoot,
            nextFlushedExpirationTime,
            currentRendererTime >= nextFlushedExpirationTime,
          );
          findHighestPriorityRoot();
          recomputeCurrentRendererTime();
          currentSchedulerTime = currentRendererTime;
        }
      } else {
        while (
          nextFlushedRoot !== null &&
          nextFlushedExpirationTime !== NoWork &&
          (minExpirationTime === NoWork ||
            minExpirationTime >= nextFlushedExpirationTime)
        ) {
          performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, true);
          findHighestPriorityRoot();
        }
      }
    
      // We're done flushing work. Either we ran out of time in this callback,
      // or there's no more work left with sufficient priority.
    
      // If we're inside a callback, set this to false since we just completed it.
      if (deadline !== null) {
        callbackExpirationTime = NoWork;
        callbackID = null;
      }
      // If there's work left over, schedule a new callback.
      if (nextFlushedExpirationTime !== NoWork) {
        scheduleCallbackWithExpirationTime(
          ((nextFlushedRoot: any): FiberRoot),
          nextFlushedExpirationTime,
        );
      }
    
      // Clean-up.
      deadline = null;
      deadlineDidExpire = false;
    
      finishRendering();
    }
    

    performWorkOnRoot:

    • isRendering 标记现在开始渲染了
    • 判断 finishedWork:是:调用 completeRoot 进入下一章的 commit 阶段;否:调用 renderRoot 遍历 Fiber 树。
    function performWorkOnRoot(
      root: FiberRoot,
      expirationTime: ExpirationTime,
      isExpired: boolean,
    ) {
        
      isRendering = true;
    
      // Check if this is async work or sync/expired work.
      if (deadline === null || isExpired) {
        // Flush work without yielding.
        // TODO: Non-yieldy work does not necessarily imply expired work. A renderer
        // may want to perform some work without yielding, but also without
        // requiring the root to complete (by triggering placeholders).
    
        let finishedWork = root.finishedWork;
        if (finishedWork !== null) {
          // This root is already complete. We can commit it.
          completeRoot(root, finishedWork, expirationTime);
        } else {
          root.finishedWork = null;
          // If this root previously suspended, clear its existing timeout, since
          // we're about to try rendering again.
          const timeoutHandle = root.timeoutHandle;
          if (timeoutHandle !== noTimeout) {
            root.timeoutHandle = noTimeout;
            // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
            cancelTimeout(timeoutHandle);
          }
          const isYieldy = false;
          renderRoot(root, isYieldy, isExpired);
          finishedWork = root.finishedWork;
          if (finishedWork !== null) {
            // We've completed the root. Commit it.
            completeRoot(root, finishedWork, expirationTime);
          }
        }
      } else {
        // Flush async work.
        let finishedWork = root.finishedWork;
        if (finishedWork !== null) {
          // This root is already complete. We can commit it.
          completeRoot(root, finishedWork, expirationTime);
        } else {
          root.finishedWork = null;
          // If this root previously suspended, clear its existing timeout, since
          // we're about to try rendering again.
          const timeoutHandle = root.timeoutHandle;
          if (timeoutHandle !== noTimeout) {
            root.timeoutHandle = noTimeout;
            // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
            cancelTimeout(timeoutHandle);
          }
          const isYieldy = true;
          renderRoot(root, isYieldy, isExpired);
          finishedWork = root.finishedWork;
          if (finishedWork !== null) {
            // We've completed the root. Check the deadline one more time
            // before committing.
            if (!shouldYield()) {
              // Still time left. Commit the root.
              completeRoot(root, finishedWork, expirationTime);
            } else {
              // There's no time left. Mark this root as complete. We'll come
              // back and commit it later.
              root.finishedWork = finishedWork;
            }
          }
        }
      }
    
      isRendering = false;
    }
    

    10. renderRoot

    • 调用 workLoop 进行循环单元更新
    • 捕获错误并进行处理
    • 走完流程之后善后

    **renderRoot **流程:

    • 遍历 Fiber 树的每个节点。

    根据 Fiber 上的 updateQueue 是否有内容,决定是否要更新那个 Fiber 节点,并且计算出新的 state,
    对于异步任务,更新每个 Fiber 节点时都要判断时间片是否过期,如果一个 Fiber 更新时出错,则其子节点就不用再更新了。最终整个 Fiber 树遍历完之后,根据捕获到的问题不同,再进行相应处理。

    • createWorkInProgress:renderRoot 中,调用 createWorkInProgress 创建 “workInProgress” 树,在其上进行更新操作。在 renderRoot 开始之后,所有的操作都在 “workInProgress” 树上进行,而非直接操作 “current” 树。(双buff机制)
    • workLoop:开始更新一颗 Fiber 树上的每个节点, isYieldy 指示是否可以中断,对于 sync 任务和已经超时的任务都是不可中断的,于是 while 循环更新即可;对于可中断的,则每次 while 循环条件中还要判断是否时间片到点需先退出。
    function workLoop(isYieldy) {
      if (!isYieldy) {
        // Flush work without yielding
        while (nextUnitOfWork !== null) {
          nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
        }
      } else {
        // Flush asynchronous work until the deadline runs out of time.
        while (nextUnitOfWork !== null && !shouldYield()) {
          nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
        }
      }
    }
    
    • performUnitOfWork:更新子树:
    function performUnitOfWork(workInProgress: Fiber): Fiber | null {
      const current = workInProgress.alternate;
      // See if beginning this work spawns more work.
      startWorkTimer(workInProgress);
      let next;
        if (enableProfilerTimer) {
        if (workInProgress.mode & ProfileMode) {
          startProfilerTimer(workInProgress);
        }
    
        next = beginWork(current, workInProgress, nextRenderExpirationTime);
        workInProgress.memoizedProps = workInProgress.pendingProps;
    
        if (workInProgress.mode & ProfileMode) {
          // Record the render duration assuming we didn't bailout (or error).
          stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
        }
      } else {
        next = beginWork(current, workInProgress, nextRenderExpirationTime);
        workInProgress.memoizedProps = workInProgress.pendingProps;
      }
      if (next === null) {
        // If this doesn't spawn new work, complete the current work.
        next = completeUnitOfWork(workInProgress);
      }
      ReactCurrentOwner.current = null;
      return next;
    }
    
    • beginWork:开始具体的节点更新,下一章再说。

    Root 节点具体怎么遍历更新,以及不同类型组件的更新,将在下一篇探讨。


    image

    相关文章

      网友评论

          本文标题:React源码04 - Fiber Scheduler (调度器

          本文链接:https://www.haomeiwen.com/subject/srxarktx.html