React源码04 - Fiber Scheduler (调度器

2020-08-09  本文已影响0人  晓风残月1994

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

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

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

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

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

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 包中:

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

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 技巧将空闲工作推迟到重新绘制之后。
具体太过复杂,就大概听个响吧,若要深究则深究:

// 仅供示意
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:

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

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:

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

**renderRoot **流程:

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

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

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


image
上一篇下一篇

猜你喜欢

热点阅读