HolyZheng / holyZheng-blog

草稿
36 stars 0 forks source link

React Scheduler 源码解析 #44

Open HolyZheng opened 4 years ago

HolyZheng commented 4 years ago

本文基于 React v16.8.6

在React中,我们大部分带有副作用的操作都会触发React的更新,而在Concurrent Mode模式中。React使用了一套全新的调度模式来进行应用的更新。时至今日,大家都知道React 的Concurrent Mode整个过程分为Render和Commit阶段。其中Render阶段是纯Javascript的运算,可中断。所以将Render阶段的运算分割到浏览器每一帧的空闲时间中去执行,从而减少JavaScript线程对页面UI渲染的阻塞,提高用户体验。但是说里面的一些细节,估计很多人都还比较懵懂,比如:

  1. 一次完整的调度过程是怎么样的?(Render阶段具体做了什么?)
  2. React内部是如何保证高优先级的调度任务先执行的?
  3. 时间片到期后,React如何在下一帧的空闲时间里恢复任务的继续执行?
  4. 任务执行过程中出现了更高优先级的任务,React是怎么处理的?带来了什么问题?

这篇文章就是尽量深入浅出的去展示笔者的总结,从一次完整的调度过程探索上面提到的种种问题。当然,人无完人,如果发现有什么不对的地方也欢迎指正。

调度前期准备

首先我们一些具有副作用的操作比如setState,会触发应用的调度更新。当我们执行这些操作的时候,React会为当前操作计算优先级,生成一个update对象保存到当前的FiberNode节点的updateQueue属性上。比如执行:

// 此案例忽略React合成事件带来的影响
this.setState(preState => ({
    age: preState + 1
}))

// 对应源码
enqueueSetState(inst, payload, callback) {
    // ...
    // 计算expirationTime(优先级) 
    const expirationTime = computeExpirationForFiber(currentTime, fiber);
    // 创建update对象
    const update = createUpdate(expirationTime);
    // ...
    // 将update对象添加到fibreNode的updateQueue末尾
    enqueueUpdate(fiber, update);
    // 发起调度任务
    scheduleWork(fiber, expirationTime);
  },

FiberUpdateQueue

然后发起调度 scheduleWork(fiber, expirationTime)。这时候React首先会从触发操作的fibreNode节点开始回溯到Root节点,并且更新沿路经过的祖先节点fibreNode中的childExpirationTime属性,该属性在调度过程中代表着当前节点所有后代节点中最大的优先级。然后从当前Root中所有还在等待执行的任务中选择优先级最大的来发起调度。

// packages/react-reconciler/src/ReactFiberScheduler.js
function scheduleWork(fiber, expirationTime) {
    // 更新祖先节点的 childExpirationTime 
    const root = scheduleWorkToRoot(fiber, expirationTime);
    if (root === null) {
      return;
    }
    // 从当前任务和正在等待的任务,以及正在暂停的任务中找出最大的 expirationTime(优先级)
    markPendingPriorityLevel(root, expirationTime);
     // TODO 这里的优先级查询和callbacklist的作用异同
    if (
      !isWorking ||
      isCommitting ||
      // ...unless this is a different root than the one we're rendering.
      nextRoot !== root
    ) {
      const rootExpirationTime = root.expirationTime;
      // 以当前最大的优先级来请求工作,也就是为优先级最大的“更新”请求工作。
      requestWork(root, rootExpirationTime);
    }
  }

FiberChildExpirationTime

在React应用中,我们可能不只一个Root节点。在React内部是通过链表来管理这些需要被调度的Root。在更新了祖先节点之后React就会将当前需要调度的Root添加到这个链表中,如果该Root已经存在就会尝试去更新链表上该Root的expirationTime:

// packages/react-reconciler/src/ReactFiberScheduler.js
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
  // 调度的root是通过链表来串联的。
  // 如果当前root不在链表中,则在链表末尾添加当前root
  // 如果已在链表中,查看此次优先级是否更大,优先级更大就会更新此root的expirationTime。
  addRootToSchedule(root, expirationTime);
  // ...
  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) {
    // 如果优先级是同步,马上执行同步工作
    performSyncWork();
  } else {
    // 根据 expirationTime 优先级去调度
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }
}

FiberRootsList

接着React会尝试为这次“操作”生成一个带有expirationTime(过期时间)的callbackNode。在React内部,会用一个链表callbackList来管理callbackNode。除了React自身向callbackList添加callbackNode外,我们还可以通过React暴露的unstable_scheduleCallback api 向callbackList中添加callbackNode,来制定callback的过期时间。

// 比如
unstable_scheduleCallback(() => {
    // do something....  
    // 比如 setState,会尽量在过期时间内执行这个setState,发起scheduleWork。
}, {timeout})

所以callbackList可能具有不只一个callbackNode,他们会根据优先级由大到小排序。不过其中有且只有一个callbackNode的callback为performAsyncWork。

FiberCallbackList

function scheduleCallbackWithExpirationTime(
  root: FiberRoot,
  expirationTime: ExpirationTime,
) {
  if (callbackExpirationTime !== NoWork) {
    // 如果先前已有callback被调度,查看timeout时间是否充足
    // 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.
        // timeout不够,将callback从list中移除
        cancelDeferredCallback(callbackID);
      }
    }
    // The request callback timer is already running. Don't start a new one.
  } else {
    startRequestCallbackTimer();
  }

  // ...
  // 不同平台对应不同函数,浏览器下是packages/scheduler/scr/scheduler.js下的 unstable_scheduleCallback方法
  callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
}

每当React准备创建一个performAsyncWork的callbackNode的时候,会查看callbackList中是否已经存在performAsyncWork的callbackNode,如果存在的话,就会对比它们的优先级。如果当前新建的优先级更高,就会将callbackList上的回调函数为performAsyncWork的callbackNode移除,然后将新建的callbackNode按优先级插入到callbackList中;否则取消新建callbackNode。通过unstable_scheduleCallback 添加的callbackNode也会按优先级进行插入。

// packages/scheduler/src/Scheduler.js
// 创建callbackNode和按优先级添加到callbackList中
function unstable_scheduleCallback(callback, deprecated_options) {
  // ...
  // 计算callbackNode 的 expirationTime(过期时间)

  // 创建一个 callbackNode 
  var newNode = {
    callback,
    priorityLevel: currentPriorityLevel,
    expirationTime,
    next: null,
    previous: null,
  };

  // 从大到小插入callbackNode到callbackList中,相同优先级的就按插入顺序来排 
  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 {
      // 循环找出第一个比 newNode 优先级低的节点
      if (node.expirationTime > expirationTime) {
        // The new callback expires before this one.
        next = node;
        break;
      }
      node = node.next;
    } while (node !== firstCallbackNode);

    if (next === null) {
      // 如果没有,那么newNode为优先级最小的,会在下面将其插入到列表最后
      next = firstCallbackNode;
    } else if (next === firstCallbackNode) {
      // 如果链表中第一个就比newNode小,那么会在下面将它插入到链表最前面
      firstCallbackNode = newNode;
      ensureHostCallbackIsScheduled();
    }

    // 插入操作
    var previous = next.previous;
    previous.next = next.previous = newNode;
    newNode.next = next;
    newNode.previous = previous;
  }

  return newNode;
}

React 会在每一帧的空闲时间里,遍历这个callbackList。依次执行第一个节点的回调(callback),直到当前时间片用完。被执行的callbackNode会从链表中移除。如果执行的callback不是performAsyncWork,那么就会尝试生成新的 callbackNode(performAsyncWork)来调整它在callbackList中的位置。如果执行的回调是performAsyncWork那么就会开始执行异步工作(遍历fiber tree等)。

// packages/scheduler/src/Scheduler.js
function flushWork(didTimeout) {

  try {
    if (didTimeout) {
        // ... 
    } else {
      if (firstCallbackNode !== null) {
        do {
          // 执行第一个callbackNode中的回调,并将此callbackNode从链表中移除
          flushFirstCallback();
          // 如果下一个callback不为空,且当前帧剩余空闲时间依旧充足,继续执行下一个callback。
          // 直到当前帧剩余空闲时间用尽。
        } while (firstCallbackNode !== null && !shouldYieldToHost());
      }
    }
  } finally {
    if (firstCallbackNode !== null) {
      // 如果还有 CallbackNode 下一帧继续遍历执行
      ensureHostCallbackIsScheduled();
    } 
    // ...
  }

当 flushFirstCallback 函数中就会执行回调函数performAsyncWork时,React就会对此Fiber Tree进行一轮异步调度

// 执行异步任务
function performAsyncWork() {
  try {
    if (!shouldYieldToRenderer()) {
    // ...
    // 执行工作,expirationtime 设为Nowork
    performWork(NoWork, true);
  } finally {
    didYield = false;
  }
}

performAsyncWork 调用的是performWork(NoWork, true)。(ps:对应的performSyncWork调用的是performWork(Sync, true))

执行异步工作

function performWork(minExpirationTime: ExpirationTime, isYieldy: boolean) {

  // 遍历root链表,找出优先级最大的root节点,并且把优先级为Nowork的节点移除
  findHighestPriorityRoot();

  // 异步渲染 isYieldy 为 true,标示可以让步,也就是可以中断
  if (isYieldy) {
    // ...
    while (
      nextFlushedRoot !== null &&
      nextFlushedExpirationTime !== NoWork &&
      minExpirationTime <= nextFlushedExpirationTime &&
      !(didYield && currentRendererTime > nextFlushedExpirationTime)
    ) {
      // nextFlushedRoot是highestPriorityRoot
      // nextFlushedExpirationTime是highestPriorityWork
      // 也就是从最具有最高优先级的root节点开始执行工作任务
      performWorkOnRoot(
        nextFlushedRoot,
        nextFlushedExpirationTime,
        currentRendererTime > nextFlushedExpirationTime,
      );
      // 重新找到最高优先级的root
      findHighestPriorityRoot();
      // 重新计算 CurrentRendererTime 
      recomputeCurrentRendererTime();
      currentSchedulerTime = currentRendererTime;
    }
  } else {
  // ...
  }
  // ...
}

当前的performAsyncWork中React会持续的从收集到的Roots链表中找出优先级最高的Root,然后对该Root执行调度工作。直到将所有Roots的工作都执行完毕,或者有更高的优先级的performAsyncWork需要执行才会结束。

FiberCallback_Root

function performWorkOnRoot(
  root: FiberRoot,
  expirationTime: ExpirationTime,
  isYieldy: boolean,
) {
  isRendering = true;
  // Check if this is async work or sync/expired work.
  if (!isYieldy) {
  // ... 
  } else {
    let finishedWork = root.finishedWork;
    if (finishedWork !== null) {
      // 如果render阶段结束了,就commit
      completeRoot(root, finishedWork, expirationTime);
    } else {
      root.finishedWork = null;
      // ...
      // 进入render阶段
      renderRoot(root, isYieldy);
      //  root.finishedWork 表示 render是否已完成
      finishedWork = root.finishedWork;
      if (finishedWork !== null) {
        // 当render阶段结束后,判断是否还有剩余时间,如果没有则在下一次空闲时间里commit。
        if (!shouldYieldToRenderer()) {
          // 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;
}

这里的逻辑很清晰,就是如果render阶段如果还没开始或还没有结束就进入render阶段,如果render阶段已经完成,那就可以进入commit阶段了。

进入Render 阶段

首先我们知道,React的Concurrent Mode模式中采用了一种类似双缓冲的方式以便在必要的时候去恢复一些数据。也就是对于我们正在调度的FiberTree,React会同时维护两份。一份叫做current tree,一份叫做 workInProgress tree。current tree代表UI的当前状态,workInProgress tree代表新的UI状态。而render阶段遍历调度的过程就是对workInProgress tree的构造和打tag的过程。

function renderRoot(root: FiberRoot, isYieldy: boolean): void {
  // ...
  nextRoot = root;
  nextRenderExpirationTime = expirationTime;
  // 对于一个新的调度任务,会从Root去构建WorkInProgress tree
  nextUnitOfWork = createWorkInProgress(
    nextRoot.current,
    null,
    nextRenderExpirationTime,
  );
  // ...
  try {
    // 遍历 FiberNode,对每个FiberNode调度
    workLoop(isYieldy);
  } catch (thrownValue) {
    // ...
  }
}

FiberBuildWorkInProgress

Render阶段就是对当前选定的Root节点(FIber Tree)进行深度优先遍历,同时根据前面提到的每个FiberNode节点的childExpirationTime来查看FiberNode的后代节点中是否还有需要在这次调度中执行的更新工作,如果没有就不会继续遍历FiberNode的后代节点。整个遍历的过程涉及到了多个方法。我们以ClassComponent为例子来了解一下整个遍历过程。

// 这里的loop是以fiberNode为节点进行遍历,每一个fiberNode代表一个工作单元,代表一次循环
function workLoop(isYieldy) {
  if (!isYieldy) {
  // ... 
  } else {
    // 每执行完一个工作单元就查看是否有更高的任务或者时间片是否用完
    while (nextUnitOfWork !== null && !shouldYieldToRenderer()) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  }
}

renderRoot 方法中,调用workLoop方法来对FiberTree进行遍历。nextUnitOfWork代表下一个进行调度的FiberNode。要了解这个过程首先你需要了解深度优先遍历,以深度优先遍历的思想来看下面的内容。继续来看看performUnitOfWork:

function performUnitOfWork(workInProgress: Fiber): Fiber | null {
  // 工作的执行是在
  const current = workInProgress.alternate;

  let next;
  // ... 
  // 返回第一个子节点 
  next = beginWork(current, workInProgress, nextRenderExpirationTime);
  workInProgress.memoizedProps = workInProgress.pendingProps;

  if (next === null) {
    // 当前FiberNode没有需要调度的子节点了,收尾这个单元的工作
    next = completeUnitOfWork(workInProgress);
  }
  // 返回下一个需要调度的工作单元(兄弟节点或子节点或回溯到了父节点) 
  return next;
}

深度优先遍历在遍历每个节点的时候是怎么样的逻辑呢?查看是否有子节点,有的话继续遍历子节点。没有的话首先看看有没有兄弟节点,都没有的话就会回溯到父节点。这里的逻辑也一样:

  1. 首先执行这个FiberNode的工作(beginWork),beginWork会返回子节点;
  2. 如果没有子节点,那么这个节点的工作就可以complete了(completeUnitOfWork);
  3. completeUnitOfWork就会返回兄弟节点,如果没有兄弟节点的话就会返回父节点作为下一个工作单元。

再来看看 beginWork、completeUnitOfWork这两个关键函数做了什么。首先不同类型的组件做要的具体工作是不一样的,所以我们看到beginWork方法里用了一个switch条件语句来区分每一类组件的具体工作,这里就按ClassComponent来看一下。

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {
  if (current !== null) {
    if (oldProps !== newProps || hasLegacyContextChanged()) {
      // 标示需要更新
      didReceiveUpdate = true;
    } esle if (updateExpirationTime < renderExpirationTime) {
      // 当前类节点在此轮调度中没有需要执行的任务,只做Context的更新
      switch (workInProgress.tag) {
        case ClassComponent: {
          const Component = workInProgress.type;
          if (isLegacyContextProvider(Component)) {
            pushLegacyContextProvider(workInProgress);
          }
          break;
        }
      }
      // 根据 childExpirationTime 判断是否需要继续往后代节点遍历
      return bailoutOnAlreadyFinishedWork(
        current,
        workInProgress,
        renderExpirationTime,
      );
    }
  }
  // ...
  switch (workInProgress.tag) {
    // class组件会进入此
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderExpirationTime,
      );
    }
    // ...
  }
}

React对FiberTree的深度优先遍历并不是盲目的进行遍历,当遍历到一些在此轮调度中不需要更新的节点时,会在 bailoutOnAlreadyFinishedWork 方法中根据上文中提到了childExpirationTime(后代节点中所有任务中的最大的优先级)来判断是否需要返回子代节点作为下一个工作单元,也就是是否需要继续往下遍历。如果当前节点需要进行更新操作的话,会执行对于的更新方法,ClassComponent 对应updateClassComponent 。updateClassComponent具体做了什么就不展开仔细看代码了,毕竟本文重点是Concurrent Mode模式本身,这里就简单提一下 updateClassComponent 做了的工作有:

  1. 点用UNSAFE_componentWillMount,UNSAFE_componentWillReceiveProp,NSAFE_componentWillUpdate等生命周期。
  2. 处理updateQueue上大于等于此次expirationTime的updates,生产最新的state
  3. 调用 getDerivedStateFromProps
  4. 根据组件render方法返回的内容创建/更新workInProgress Tree中对应的节点
  5. 返回第一个子节点作为 nextUnitOfWork(workLoop的下一个工作单元)

FiberEffectList

在处理updateQueue的时候,会筛选出优先级足够的update执行来更新state。剩下的update会成为一个新的updateQueue,其中最大的优先级作为该fiberNode的新优先级

export function processUpdateQueue<State>(
    workInProgress: Fiber,
    queue: UpdateQueue<State>,
    props: any,
    instance: any,
    renderExpirationTime: ExpirationTime,
  ): void {
    // ...

    // Iterate through the list of updates to compute the result.
    while (update !== null) {
      const updateExpirationTime = update.expirationTime;
      if (updateExpirationTime < renderExpirationTime) {
        // This update does not have sufficient priority. Skip it.
        if (newFirstUpdate === null) {
          // This is the first skipped update. It will be the first update in
          // the new list.
          newFirstUpdate = update;
          // Since this is the first update that was skipped, the current result
          // is the new base state.
          newBaseState = resultState;
        }
        // Since this update will remain in the list, update the remaining
        // expiration time.
        if (newExpirationTime < updateExpirationTime) {
          newExpirationTime = updateExpirationTime;
        }
      } else {
        // This update does have sufficient priority. Process it and compute
        // a new result.
        resultState = getStateFromUpdate(
          workInProgress,
          queue,
          update,
          resultState,
          props,
          instance,
        );
        // ...
      }
      // Continue to the next update.
      update = update.next;
    }
    // ...
    queue.baseState = newBaseState;
    queue.firstUpdate = newFirstUpdate;
    queue.firstCapturedUpdate = newFirstCapturedUpdate;
    workInProgress.expirationTime = newExpirationTime;
    workInProgress.memoizedState = resultState;
}

FiberUpdate_p

执行完beginWork之后,拿到nextUnitOfWork。如果nextUnitWork为空,就会执行completeUnitOfWork方法,因为当前节点不需要再往下遍历了,是时候去遍历它的兄弟节点或回溯到父节点了,所以可以complete这个工作单元。来看一下completeUnitOfWork:

function completeUnitOfWork(workInProgress: Fiber): Fiber | null {
    // 该方法尝试完成当前的工作单元,然后去完成下一个兄弟节点的任务,
    // 如果没有兄弟节点了,就返回去完成父节点的任务。
    while (true) {
      const returnFiber = workInProgress.return;
      const siblingFiber = workInProgress.sibling;

      if ((workInProgress.effectTag & Incomplete) === NoEffect) {

        // This fiber completed.
        // Remember we're completing this unit so we can find a boundary if it fails.
        nextUnitOfWork = workInProgress;
        // 完成该节点任务,只要不是SuspenseComponent,返回的就是null
        nextUnitOfWork = completeWork(
            current,
            workInProgress,
            nextRenderExpirationTime,
          );
        // 更新 ChildExpirationTime
        resetChildExpirationTime(workInProgress, nextRenderExpirationTime);

        if (
          returnFiber !== null &&
          (returnFiber.effectTag & Incomplete) === NoEffect
        ) {
          // 将当前节点的已有effect向上收集,也就是把先把从子节点收集到的effect向上收集
          // 到父节点。
          if (returnFiber.firstEffect === null) {
            returnFiber.firstEffect = workInProgress.firstEffect;
          }
          if (workInProgress.lastEffect !== null) {
            if (returnFiber.lastEffect !== null) {
              returnFiber.lastEffect.nextEffect = workInProgress.firstEffect;
            }
            returnFiber.lastEffect = workInProgress.lastEffect;
          }

          // 然后再将当前节点effect插入到effectlist中,后代节点effect排在effect前面,
          // 最终经过层层的向上收集,root节点的effectlist将会包含所有的effect。
          const effectTag = workInProgress.effectTag;
          if (effectTag > PerformedWork) {
            if (returnFiber.lastEffect !== null) {
              returnFiber.lastEffect.nextEffect = workInProgress;
            } else {
              returnFiber.firstEffect = workInProgress;
            }
            returnFiber.lastEffect = workInProgress;
          }
        }

        // 处理完当前节点的任务,先考虑处理兄弟节点,再考虑回溯到父节点处理
        if (siblingFiber !== null) {
          // If there is more work to do in this returnFiber, do that next.
          return siblingFiber;
        } else if (returnFiber !== null) {
          // If there's no more work in this returnFiber. Complete the returnFiber.
          workInProgress = returnFiber;
          continue;
        } else {
          // We've reached the root.
          return null;
        }
      } else {
        // ... 
      }
    }
    // ...
}

首先执行completeWork,然后正如前面所说的如果有兄弟节点就返回兄弟节点(siblingFiber)返回作为下一个工作单元;如果没有则返回父节点(returnFiber)作为下一个工作单元。同时如果当前节点有更新,就会将当前节点添加到EffectList中。最终EffectList中会包含所有具有更新的FiberNode,在commit阶段就会遍历这个EffectList,根据FiberNode上打的Tag,执行对应的操作。

completeWork类似beginWork,也是针对不同类型的组件执行对应的操作,ClassComponent的话就是对context的更新,这里就不详细看了。

FiberEffectList_2

到目前为止就是一轮 workLoop。React会在每一轮workLoop结束后检查时间片是否过期以及是否有更高优先级的任务插队。这就衍生出了两个问题:

  1. 当前帧时间片用完后,React是如何在下一帧的时间片里继续上一帧的工作的呢?
  2. 如果被更高优先级的调度任务插队,React是怎么做的?因此由引发了什么问题?

时间片用完,下一帧如何继续工作?

前文讲到,React会为每个调度任务创建一个callbackNode按优先级插入到callbackList中,然后依次执行和移除第一个callbackNode。同时workLoop中通过nextUnitWork来指向下一个工作单元。

function workLoop(isYieldy) {
  if (!isYieldy) {
  // ... 
  } else {
    // 每执行完一个工作单元就查看是否有更高的任务或者时间片是否用完
    while (nextUnitOfWork !== null && !shouldYieldToRenderer()) {
      nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  }
}

当时间片用完的时候,就会跳出while循环。但是React并不会重制nextUnitOfWork的值,而是让它继续指向当次调度任务的下一个工作单元。同时,会为当前未执行完的调度任务(performAsyncWork)重新创建一个callbackNode(performAsyncWork),并插入到callbackList中。

function performWork(minExpirationTime: ExpirationTime, isYieldy: boolean) {
    // ...
    if (isYieldy) {  
     // ...
    } else {
     // ...
    }
    // ...
    // 时间片到期后,如果当前调度任务还未完成,为当前的任务再创建一个callbackNode
    if (nextFlushedExpirationTime !== NoWork) {
      scheduleCallbackWithExpirationTime(
        ((nextFlushedRoot: any): FiberRoot),
        nextFlushedExpirationTime,
      );
    }
   // ...
  }

FiberCallbackNode_r

然后在下一帧执行新的performAsyncWork时,沿用nextUnitOfWork的值,从上次结束的地方继续执行调度工作。

function renderRoot(root: FiberRoot, isYieldy: boolean): void {
  // ...
  if (
    expirationTime !== nextRenderExpirationTime ||
    root !== nextRoot ||
    nextUnitOfWork === null // 不通过,nextUnitOfWork不会重制到Root节点
  ) {
       nextUnitOfWork = createWorkInProgress(
          nextRoot.current,
          null,
          nextRenderExpirationTime,
    );
   }
   // ... 
   workLoop(isYieldy);
}

较低任务执行过程中出现高优先级任务

高优先级任务中断低优先级任务有三种情况,第一种是在 !isWorking 状态,也就是既不在render阶段也不在commit阶段。第二种是在isRendering状态,也就是render阶段。第三种是在isCommitting,也就是commit阶段。这里主要涉及到了两个问题:

  1. 高优先级如何打断低优先级任务。
  2. 低优先级任务如何恢复执行。

处于 !isWorking 状态情况下

如果不在render阶段也不在commit阶段,React会重置nextUnitOfWork等过程变量,然后重新requestWork。生成新的callbackNode(performAsyncWork)。在当前帧结束时,由于nextUnitOfwork === null 所以会结束原本执行的较低优先级的performAsyncWork,在下一帧的空闲时间中执行新的高优先级的performAsyncWork。

function scheduleWork(fiber, expirationTime) {
    // 更新roots链表
    const root = scheduleWorkToRoot(fiber, expirationTime);
    if (root === null) {
      return;
    }
    if (
      !isWorking &&
      nextRenderExpirationTime !== NoWork &&
      expirationTime > nextRenderExpirationTime
    ) {
      // 新任务优先级更高,重置栈(重置nextUnitOfWork等各个任务工作过程量)
      resetStack();
    }

    if ( !isWorking || isCommitting || nextRoot !== root ) {
      // 重新请求工作
      requestWork(root, rootExpirationTime);
    }
 // ...

处于render阶段的情况下

当处于Render阶段的时候,出现了高优先级任务的话。首先高优先级任务会例行的更新Roots链表。然后在performWork中,React会找出Roots链表中最高优先级的Roots进行调度。并且在renderRoot函数中判断到当前执行的Root的expirationTime高于前一次的话,会回resetStack()重置nextUnitOfWork指向Root节点。进而开始执行高优先级的任务。

function scheduleWork(fiber, expirationTime) {
    // 更新roots链表
    const root = scheduleWorkToRoot(fiber, expirationTime);
    // ...
}
function performWork(minExpirationTime: ExpirationTime, isYieldy: boolean) {
  //...
  while (
      nextFlushedRoot !== null &&
      nextFlushedExpirationTime !== NoWork &&
      minExpirationTime <= nextFlushedExpirationTime &&
      !(didYield && currentRendererTime > nextFlushedExpirationTime)
    ) {
      // nextFlushedRoot是highestPriorityRoot
      // nextFlushedExpirationTime是highestPriorityWork
      // 也就是从最具有最高优先级的root节点开始执行工作任务
      performWorkOnRoot(
        nextFlushedRoot,
        nextFlushedExpirationTime,
        currentRendererTime > nextFlushedExpirationTime,
      );
      // 重新找到最高优先级的root
      findHighestPriorityRoot();
      // 重新计算 CurrentRendererTime 
      recomputeCurrentRendererTime();
      currentSchedulerTime = currentRendererTime;
    }
    // ...
   // 如果还有工作,调度新的callback
  if (nextFlushedExpirationTime !== NoWork) {
    scheduleCallbackWithExpirationTime(
      ((nextFlushedRoot: any): FiberRoot),
      nextFlushedExpirationTime,
    );
  }
  // ... 
}
function renderRoot(root: FiberRoot, isYieldy: boolean): void {
  // ...
  if (
    expirationTime !== nextRenderExpirationTime ||
    root !== nextRoot ||
    nextUnitOfWork === null
  ) {
    // 如果是一个新的任务的话就会重新初始化各个过程量的值
    // Reset the stack and start working from the root.
    resetStack();
    // 设置即将要执行的渲染工作的各个参数
    nextRoot = root;
    nextRenderExpirationTime = expirationTime;
    nextUnitOfWork = createWorkInProgress(
      nextRoot.current,
      null,
      nextRenderExpirationTime,
    );
    //...
  }
}

处于commit阶段的情况下

如果当前处于commit阶段,说明performAsyncWork的render阶段已经结束,nextUnitOfWork已经为空。所以不需要resetStack(),继续requestWork,创建一个新的callbackNode(performAsyncWork)插入到callbackList,等待它执行。具体逻辑可以看上面的scheduleWork。

被打断的低优先级任务如何重新恢复执行?

如上文所说,在workLoop中遍历FiberTree执行工作的时候,我们同时在构建workInProgress Tree。当我们低优先级任务执行到一半,也就是workInProgress Tree被构建到一半的时候,我们被高优先级插。nextUnitOfWork被置为Null,React就会从头去构建我们的workInProgress Tree。

nextUnitOfWork = createWorkInProgress(
      nextRoot.current,
      null,
      nextRenderExpirationTime,
);

因为我们执行update的操作是在workInprogress上进行的,即使在低优先级任务中有update被执行且移出updateQueue,这时都会从current中重新恢复回来。然后高优先级任务 进入commitRoot阶段时 ,会重新findNextExpirationTimeToWorkOn,fiberRoot的expirationTime就会恢复回之前低优先级任务的expirationTIme。

于是低优先级任务就会在下一轮performWorkOnRoot中重新由头开始执行。之前已经执行过的fiberNode会被二次执行。

问题

二次执行的fiberNode对应组件的部分生命周期(componentWillMount,componentWillReceiveProps,componentWillUpdate)会多次执行。React的做法是考虑到这些生命周期本身使用起来也有比较容易写出问题问题,所以当前逐步给这些生命周期添加“UNSAFE_”前缀。

componentWillMount → UNSAFE_componentWillMount
componentWillReceiveProps → UNSAFE_componentWillReceiveProps
componentWillUpdate → UNSAFE_componentWillUpdate

v16.9.0之后的版本直接使用不带“UNSAFE_”的这三个生命周期的话会得到警告,同时React团队保证新的方法名(如 UNSAFE_componentWillMount)在 React 16.9 和 React 17.x 中,仍可以继续使用。官网对应信息