关于javascript:React1686源码阅读二渲染

44次阅读

共计 23361 个字符,预计需要花费 59 分钟才能阅读完成。

开始

又是很久很久没写新货色了,刚好最近工夫都比拟闲暇,终于能有工夫整顿之前的浏览源码的笔记;人不知; 鬼不觉 React 都公布了 18 的版本了,还好 React 整体架构还是没太大的变动,16 版本应该还有肯定的参考性。

先带着问题

在我还没浏览源码前或者在我浏览源码的时候记录好几个问题,基本上就是带着这些问题去一步步深刻理解源码的实现的:

  1. Virtual Dom 是如何转化 Fiber 构造的?
  2. workInProgress 和 current 为什么要有两个 fiber 树?
  3. fiber 是怎么暂停和复原执行的?
  4. fiber 更新在什么状况下会被暂停或者抢占?
  5. 同一 fiber 节点树被反复标记,低优先级被高优先级笼罩会怎么?
  6. 为什 componentWillMount 会反复触发?

逐渐剖析

间接从代码开始

ReactDOM.redner(<div></div>, '#root');

下一步:

    funciton legacyRenderSubtreeIntoContainer(...) {let root: Root = (container._reactRootContainer: any);
        if(!root) {
          // 开始初始渲染,创立 ReactRoot
          root = container._reactRootContainer = legacyCreateRootFromDOMContainer(container,forceHydrate,);

          unbatchedUpdates(() => {if (parentComponent != null) {...} else {
              // 调起 ReactRoot 的渲染办法
              root.render(children, callback);
            }
          });
        } else {...}
    }

ReactRoot 创立:

    function ReactRoot(
  container: DOMContainer,
  isConcurrent: boolean,
  hydrate: boolean,
) {const root = createContainer(container, isConcurrent, hydrate);
  this._internalRoot = root;
}

这里 createContainer,理论创立的是 createFiberRoot:

export function createFiberRoot(
  containerInfo: any,
  isConcurrent: boolean,
  hydrate: boolean,
): FiberRoot {const uninitializedFiber = createHostRootFiber(isConcurrent);

      let root;
    root = ({
      current: uninitializedFiber, // HostRoot 类型的 fiber 节点
      containerInfo: containerInfo, // dom 节点
      pendingChildren: null,

      pingCache: null,
      
      earliestPendingTime: NoWork, // 各种期待更新的优先级
      latestPendingTime: NoWork,
      earliestSuspendedTime: NoWork,
      latestSuspendedTime: NoWork,
      latestPingedTime: NoWork,
        
      didError: false, // 更新出错标记位

      pendingCommitExpirationTime: NoWork, 
      finishedWork: null, // 曾经实现 diff 的 wip fiber 树,期待最初 commit
      timeoutHandle: noTimeout,
      context: null,
      pendingContext: null,
      hydrate,
      nextExpirationTimeToWorkOn: NoWork, // 下次更新的优先级
      expirationTime: NoWork,
      firstBatch: null,
      nextScheduledRoot: null, // 下个调度的 Root,FiberRoot 会串起一个单向链表,调度器会不停遍历查看是否须要更新
    }: BaseFiberRootProperties);
  }

  uninitializedFiber.stateNode = root;
  return ((root: any): FiberRoot);
}

这里的 FiberRoot 才是理论的调度单位,这里有很多字段都是跟调度更新相干的,就大略正文一下。
这里 React 曾经创立了一个 ReactRoot->FiberRoot 的关系,再回到 legacyRenderSubtreeIntoContainer 办法,开始间接调起 ReactRoot.render 办法,能够想像为什么不在 FiberRoot 上实现相似 render 的办法,而在 ReactRoot 上做,大略是为了让整个 Fiber 树都是一个非常简单的对象构造组成不便当前应用其余语言间接实现相似的构造吧。
在顺便看一下 FiberNode 构造:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  // Instance
  this.tag = tag; // 类型标签
  this.key = key;
  this.elementType = // 虚构 dom 的类型,个别为字符串或者函数,class; 或者是 REACT_SUSPENSE_TYPE,REACT_FRAGMENT_TYPE 等内置类型
  this.type = null; // resolve 后的最终类型个别跟 elementType 是一样的
  this.stateNode = null; // 组件实例或者 Dom 节点

  // Fiber
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null; // 更新队列,记录了更新的申请
  this.memoizedState = null; // 对应 hook 状态链表
  this.contextDependencies = null;

  this.mode = mode;

  // Effects
  this.effectTag = NoEffect;
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  this.expirationTime = NoWork;
  this.childExpirationTime = NoWork;

  this.alternate = null;
}

接着来到 ReactFiberReconciler 的 updateContainer:

function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  const current = container.current;
  const currentTime = requestCurrentTime();
  const expirationTime = computeExpirationForFiber(currentTime, current);
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
  );
}

这里 requestCurrentTime 办法是很特地的,并不是单纯的是调用相似 Data.now 或者 Performance.now 办法获取工夫。

function requestCurrentTime() {if (isRendering) {
    // 要是渲染中,间接返回之前曾经计算调度好的工夫
    return currentSchedulerTime;
  }
  findHighestPriorityRoot();
  if (
    nextFlushedExpirationTime === NoWork ||
    nextFlushedExpirationTime === Never
  ) {
    // 要是没有其余期待中的工作才计算并更新 currentRendererTime
    recomputeCurrentRendererTime();
    currentSchedulerTime = currentRendererTime;
    return currentSchedulerTime;
  }
  // 要是有工作在期待中,则间接返回之前曾经计算调度好的工夫
  return currentSchedulerTime;
}

因为这个计算的工夫会影响最初工作的优先级的计算,所以 React 外面的优先级是蕴含两种含意的:工作的优先级和超时的工夫,这里 requestCurrentTime 指标就是尽可能把其余同一阶段(如在渲染过程中触发的,或者一个事件处理器外面触发的)触发的更新都计算为同一个优先级,而后在一次调度更新中解决。

再持续看 recomputeCurrentRendererTime 是如何计算这个工夫的:

function recomputeCurrentRendererTime() {const currentTimeMs = now() - originalStartTimeMs;
  currentRendererTime = msToExpirationTime(currentTimeMs);
}

const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1;

export function msToExpirationTime(ms: number): ExpirationTime {return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}

首先获取以后页面加载的时间差,而后每 10ms 内作为一个级别,再与 MAGIC_NUMBER_OFFSET 相减。
这样能够总结两点:

  1. 个别也是往后触发的工作工作,它的优先级是会越低的
  2. 每 10ms 内距离触发的工作都视为同一个优先级的工作

持续回到 updateContainer,接着调用 computeExpirationForFiber,再依据调度的上下文来确定理论的优先级:

function computeExpirationForFiber(currentTime: ExpirationTime, fiber: Fiber) {const priorityLevel = getCurrentPriorityLevel();

  let expirationTime;
  if ((fiber.mode & ConcurrentMode) === NoContext) {expirationTime = Sync;} else if (isWorking && !isCommitting) {expirationTime = nextRenderExpirationTime;} else {switch (priorityLevel) {
      case ImmediatePriority:
        expirationTime = Sync;
        break;
      case UserBlockingPriority:
        expirationTime = computeInteractiveExpiration(currentTime);
        break;
      case NormalPriority:
        // This is a normal, concurrent update
        expirationTime = computeAsyncExpiration(currentTime);
        break;
      case LowPriority:
      case IdlePriority:
        expirationTime = Never;
        break;
      default:
        invariant(
          false,
          'Unknown priority level. This error is likely caused by a bug in' +
            'React. Please file an issue.',
        );
    }

    if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {expirationTime -= 1;}
  }

  if (
    priorityLevel === UserBlockingPriority &&
    (lowestPriorityPendingInteractiveExpirationTime === NoWork ||
      expirationTime < lowestPriorityPendingInteractiveExpirationTime)
  ) {lowestPriorityPendingInteractiveExpirationTime = expirationTime;}

  return expirationTime;
}

这里的上下文次要包含:是否是并发模式以及以后的 priorityLevel 是什么。

  1. 如果不是并发模式,更新的优先级永远都是 Sync
  2. 如果是并发模式下,正在 render 的过程中,那优先级等同于以后 render 的工作优先级 nextRenderExpirationTime
  3. getCurrentPriorityLevel 获取以后 priorityLevel,这里就有
    ImmediatePriority:对应就是 Sync = MAX_SIGNED_31_BIT_INT
    UserBlockingPriority: 通过 computeInteractiveExpiration 计算
    NormalPriority:通过 computeAsyncExpiration 计算
    LowPriority,IdlePriority:对应就是 Never = 1,优先级最低

别离再看 computeInteractiveExpiration 和 computeAsyncExpiration 的计算

export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;

export function computeInteractiveExpiration(currentTime: ExpirationTime) {
  return computeExpirationBucket(
    currentTime,
    HIGH_PRIORITY_EXPIRATION,
    HIGH_PRIORITY_BATCH_SIZE,
  );
}

export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;

export function computeAsyncExpiration(currentTime: ExpirationTime,): ExpirationTime {
  return computeExpirationBucket(
    currentTime,
    LOW_PRIORITY_EXPIRATION,
    LOW_PRIORITY_BATCH_SIZE,
  );
}

function computeExpirationBucket(
  currentTime,
  expirationInMs,
  expirationInMs,
): ExpirationTime {
  return (
    MAGIC_NUMBER_OFFSET -
    ceiling(
      MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )
  );
}

感觉 computeExpirationBucket 这个办法计算有点绕,然而把之前 currentTime 的计算合并在一起就会清晰一点:

 MAGIC_NUMBER_OFFSET -
    ceiling(MAGIC_NUMBER_OFFSET - (MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0)) + expirationInMs / UNIT_SIZE,
      bucketSizeMs / UNIT_SIZE,
    )

合并等于:MAGIC_NUMBER_OFFSET - ceiling((ms  + expirationInMs) / UNIT_SIZE, bucketSizeMs / UNIT_SIZE)

也就是说以 computeInteractiveExpiration 为例,都是工作触发的工夫加 150ms 作为优先级和超时的工夫,而且每 100ms 作为一个级别距离,
而 computeAsyncExpiration 则是延后 5000ms,250ms 作为距离。

最初来到 updateContainerAtExpirationTime 办法,这里间接调起 scheduleRootUpdate 办法

function scheduleRootUpdate(
  current: Fiber,
  element: ReactNodeList,
  expirationTime: ExpirationTime,
  callback: ?Function,
) {const update = createUpdate(expirationTime);
  update.payload = {element};
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    warningWithoutStack(
      typeof callback === 'function',
      'render(...): Expected the last optional `callback` argument to be a' +
        'function. Instead received: %s.',
      callback,
    );
    update.callback = callback;
  }

  flushPassiveEffects();
  enqueueUpdate(current, update);
  scheduleWork(current, expirationTime);

  return expirationTime;
}

通过之前一堆筹备工作,这里才算是进入调度更新:

  1. 创立一个 update 对象,记录这次触发的更新信息
  2. flushPassiveEffects,触发 useEffect 这些 hook,放在当前 hook 剖析的文章再细说
  3. enqueueUpdate,把 update 对象退出到 fiber 的 updateQueue 上,这里要是存在 wip fiber 节点同样是须要退出到 wip fiber 节点的 updateQueue,因为整个更新过程有可能会被抢占而中断,也有可能顺利完成更新 wip fiber tree 会替换为 current fiber tree;所以两边队列都须要退出避免失落更新

而后就是 scheduleWork 调度工作:

function scheduleWork(fiber: Fiber, expirationTime: ExpirationTime) {
  // 1
  const root = scheduleWorkToRoot(fiber, expirationTime);
  if (root === null) {return;}
  // 2
  if (
    !isWorking &&
    nextRenderExpirationTime !== NoWork &&
    expirationTime > nextRenderExpirationTime
  ) {
    interruptedBy = fiber;
    resetStack();}
  // 3
  markPendingPriorityLevel(root, expirationTime);
  if (
    !isWorking ||
    isCommitting ||
    nextRoot !== root
  ) {
    const rootExpirationTime = root.expirationTime;
    requestWork(root, rootExpirationTime);
  }
}

持续分步剖析:

  1. scheduleWorkToRoot,次要更新 fiber 的 childExpirationTime 属性,该属性标记的是子 fiber 外面最高的优先级
  2. 如果不是在 diff 或者 commit 过程中,而且要是将要更新的优先级比以后解决的优先级更高,就意味着要中断了,所以要 resetStack
  3. markPendingPriorityLevel 次要更新 root 的两个属性 earliestPendingTime 和 latestPendingTime,这两个属性就是标记期待更新的工作的优先级范畴,因为期待更新的优先级其实也是分为几种类型的:pending,pinged,suspended,这些类型从高到低程序解决,所以优先解决 pending 的。
  4. findNextExpirationTimeToWorkOn 开始寻找优先级最高的工作去解决并更新 root 的 nextExpirationTimeToWorkOn 和 expirationTime,记录以后要解决的优先级
  5. 如果不是 diff 或者 commit 过程中,则开始 requestWork,否则间接返回等着下次调度执行就好

接着看 requestWork:

function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {addRootToSchedule(root, expirationTime);
  if (isRendering) {return;}

  if (expirationTime === Sync) {performSyncWork();
  } else {scheduleCallbackWithExpirationTime(root, expirationTime);
  }
}
  1. addRootToSchedule 退出到 root 单向调度链表
  2. 如果是优先级是 Sync 立马调起 performSyncWork,否则 scheduleCallbackWithExpirationTime 期待调度更新

这里就离开两条线路,一条是传统模式下的同步更新,另外一条就是 fiber 架构最终目标并发模式下的异步更新。

先看同步更新:

function performWork(minExpirationTime: ExpirationTime, isYieldy: boolean) {
    // 寻找最高优先级的 Root 筹备执行
    findHighestPriorityRoot();
    if (isYieldy) {// 异步更新逻辑} else {
        // 同步更新逻辑
        // 1. 存在期待解决的工作
        // 2. 期待解决的工作的优先级要大于等于这次调度的优先级
         while (
          nextFlushedRoot !== null &&
          nextFlushedExpirationTime !== NoWork &&
          minExpirationTime <= nextFlushedExpirationTime
        ) {performWorkOnRoot(nextFlushedRoot, nextFlushedExpirationTime, false);
          findHighestPriorityRoot();}
    }
    // 执行清理工作
    finishRendering();}

performWork 的逻辑也非常简单,不停循环寻找存在最高优先级的工作工作的 Root 调起 performWorkOnRoot 执行:

function performWorkOnRoot(
  root: FiberRoot,
  expirationTime: ExpirationTime,
  isYieldy: boolean,
) {
  isRendering = true;
  if (!isYieldy) {
    // 同步更新逻辑
    let finishedWork = root.finishedWork;
    if (finishedWork !== null) {completeRoot(root, finishedWork, expirationTime);
    } else {
      root.finishedWork = null;
      const timeoutHandle = root.timeoutHandle;
      if (timeoutHandle !== noTimeout) {cancelTimeout(timeoutHandle);
      }
      renderRoot(root, isYieldy);
      finishedWork = root.finishedWork;
      if (finishedWork !== null) {completeRoot(root, finishedWork, expirationTime);
      }
    }
  } else {// 异步更新逻辑}
}

到了 performWorkOnRoot,能够显著看到次要分为两个阶段来解决工作的:renderRoot 和 completeRoot。
先看 renderRoot:

function renderRoot(root: FiberRoot, isYieldy: boolean): void {
  // 1. 触发 useEffect hook
  flushPassiveEffects();
  isWorking = true;
  const previousDispatcher = ReactCurrentDispatcher.current;
  // 2. 设置 hook dispatcher
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  const expirationTime = root.nextExpirationTimeToWorkOn;
  // 3. 如果调度优先级跟以后的解决的优先级不一样或者解决的 root 不一样,又或者是一个全新的调度,就开始重置执行栈,创立 wip fiber 节点
  if (
    expirationTime !== nextRenderExpirationTime ||
    root !== nextRoot ||
    nextUnitOfWork === null
  ) {resetStack();
    nextRoot = root;
    nextRenderExpirationTime = expirationTime;
    nextUnitOfWork = createWorkInProgress(
      nextRoot.current,
      null,
      nextRenderExpirationTime,
    );
    root.pendingCommitExpirationTime = NoWork;
  }
  do {
    try {
      // 4. 开始解决工作工作主循环
      workLoop(isYieldy);
    } catch (thrownValue) {resetContextDependences();
      resetHooks();
      if (nextUnitOfWork === null) {
        didFatal = true;
        onUncaughtError(thrownValue);
      } else {
        const sourceFiber: Fiber = nextUnitOfWork;
        let returnFiber = sourceFiber.return;
        if (returnFiber === null) {
          didFatal = true;
          onUncaughtError(thrownValue);
        } else {
          throwException(
            root,
            returnFiber,
            sourceFiber,
            thrownValue,
            nextRenderExpirationTime,
          );
          nextUnitOfWork = completeUnitOfWork(sourceFiber);
          continue;
        }
      }
    }
    break;
  } while (true);
  ...
  onComplete(root, rootWorkInProgress, expirationTime);
}

再进入看 workLoop,performUnitOfWork 不停解决遍历 fiber 节点解决

function workLoop(isYieldy) {if (!isYieldy) {
    // Flush work without yielding
    while (nextUnitOfWork !== null) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  } else {
    // Flush asynchronous work until there's a higher priority event
    while (nextUnitOfWork !== null && !shouldYieldToRenderer()) {nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    }
  }
}

持续 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) {next = completeUnitOfWork(workInProgress);
    }
      ReactCurrentOwner.current = null;
      return next;
}

当初先总结一下 FiberRoot 和 Fiber 各个阶段:

所以 FiberRoot 和 Fiber 也各自都会有 render,complete, commit 阶段,不同的阶段都会有不一样的工作目标。
当初到了 beginWork 阶段:

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderExpirationTime: ExpirationTime,
): Fiber | null {if (current !== null) {
      const oldProps = current.memoizedProps;
      const newProps = workInProgress.pendingProps;
      // 如果 pros 有变动或者依赖的 context 有扭转
      if (oldProps !== newProps || hasLegacyContextChanged()) {didReceiveUpdate = true;} else if (updateExpirationTime < renderExpirationTime) {
        didReceiveUpdate = false;
        // 如果 fiber 的优先级没有以后解决的优先级更高的更新则跳过,间接进入 bailoutOnAlreadyFinishedWork,bailoutOnAlreadyFinishedWork 外面会判断 childExpirationTime 有没有解决的更新,如果没有则返回 null,而后遍历 sibling,要是存在则持续深度遍历
        ...
      }
    } else {didReceiveUpdate = false;}
    workInProgress.expirationTime = NoWork;
    switch(workInProgress.tag) {
        ....
          case HostRoot:
              return updateHostRoot(current, workInProgress, renderExpirationTime);
         case FunctionComponent: {
          const Component = workInProgress.type;
          const unresolvedProps = workInProgress.pendingProps;
          const resolvedProps =
            workInProgress.elementType === Component
              ? unresolvedProps
              : resolveDefaultProps(Component, unresolvedProps);
          return updateFunctionComponent(
            current,
            workInProgress,
            Component,
            resolvedProps,
            renderExpirationTime,
          );
    }
    }
}

beginWork,第一步先判断是否有更新,而后第二步接着依据不同组件类型调用更新的办法,因为根节点是 HostRoot 类型的,先看 updateHostRoot 办法,前面再回头跟进
updateFunctionComponent;

function updateHostRoot(current, workInProgress, renderExpirationTime) {pushHostRootContext(workInProgress);
  const updateQueue = workInProgress.updateQueue;
  const nextProps = workInProgress.pendingProps;
  const prevState = workInProgress.memoizedState;
  const prevChildren = prevState !== null ? prevState.element : null;
  // 解决 updteQueue,把高于以后优先级的 update 对象移出队列并解决,解决完后依据须要移入到 effectList 队列中期待后续解决
  processUpdateQueue(
    workInProgress,
    updateQueue,
    nextProps,
    null,
    renderExpirationTime,
  );
  const nextState = workInProgress.memoizedState;
  const nextChildren = nextState.element;
  if (nextChildren === prevChildren) {resetHydrationState();
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderExpirationTime,
    );
  }
  const root: FiberRoot = workInProgress.stateNode;
  if ((current === null || current.child === null) &&
    root.hydrate &&
    enterHydrationState(workInProgress)
  ) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderExpirationTime,
    );
  } else {
    reconcileChildren(
      current,
      workInProgress,
      nextChildren,
      renderExpirationTime,
    );
    resetHydrationState();}
  return workInProgress.child;
}

updateHostRoot 办法的第一步就是解决 updateQueue,把高于以后优先级的 update 对象移出队列并解决,解决完后依据须要移入到 effectList 队列 (effectList 队列都是 commit 阶段的蕴含这次更新的所有副作用:例如各种生命周期办法,hook 回调,dom 操作等) 中期待后续解决,而后再依据是否是初始挂载而调用 mountChildFibers 或者 reconcileChildren;而理论 mountChildFibers 和 reconcileChildren 都是一样的逻辑,惟一不一样的是 mountChildFibers 不会记录增删改这些副作用,因为这个 fiber 自身 effectTag 曾经标记为 Placement,后续 commit 阶段会不停遍历子节点插入到 dom 上,而 mountChildFibers 最大的指标就是把 jsx element 转换为 fiber 构造。

回头再看 updateFunctionComponent:

export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderExpirationTime,
) {
    // 获取 context,前面 context 剖析再细说
    const unmaskedContext = getUnmaskedContext(workInProgress, Component, true);
    const context = getMaskedContext(workInProgress, unmaskedContext);

    let nextChildren;
    prepareToReadContext(workInProgress, renderExpirationTime);
    // 执行函数组件
    nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderExpirationTime,
    );
    if (current !== null && !didReceiveUpdate) {bailoutHooks(current, workInProgress, renderExpirationTime);
    return bailoutOnAlreadyFinishedWork(
      current,
      workInProgress,
      renderExpirationTime,
    );
  }
  workInProgress.effectTag |= PerformedWork;
  // diff 算法
  reconcileChildren(
    current,
    workInProgress,
    nextChildren,
    renderExpirationTime,
  );
  return workInProgress.child;
}

进入 renderWithHooks:

export function renderWithHooks(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  props: any,
  refOrContext: any,
  nextRenderExpirationTime: ExpirationTime,
): any {
    renderExpirationTime = nextRenderExpirationTime;
    currentlyRenderingFiber = workInProgress;
    nextCurrentHook = current !== null ? current.memoizedState : null;
    // 切换 hook 上下文
    ReactCurrentDispatcher.current =
      nextCurrentHook === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
    let children = Component(props, refOrContext);

    if (didScheduleRenderPhaseUpdate) {
        do {
          didScheduleRenderPhaseUpdate = false;
          numberOfReRenders += 1;
          nextCurrentHook = current !== null ? current.memoizedState : null;
          nextWorkInProgressHook = firstWorkInProgressHook;

          currentHook = null;
          workInProgressHook = null;
          componentUpdateQueue = null;

          ReactCurrentDispatcher.current = __DEV__
            ? HooksDispatcherOnUpdateInDEV
            : HooksDispatcherOnUpdate;

          children = Component(props, refOrContext);
        } while (didScheduleRenderPhaseUpdate);

        renderPhaseUpdates = null;
        numberOfReRenders = 0;
      }
     const renderedWork: Fiber = (currentlyRenderingFiber: any);
     // 更新节点以后的信息
     renderedWork.memoizedState = firstWorkInProgressHook;
     renderedWork.expirationTime = remainingExpirationTime;
     renderedWork.updateQueue = (componentUpdateQueue: any);
     renderedWork.effectTag |= sideEffectTag;
}

renderWithHook 会首先切换 hook 的上下文(分为 mount 和 update 两种上下文),而后间接调用函数组件,如果函数组件在调用的时候又触发了更新,就会再一次反复调用;最初更新节点信息,次要是查看 hook 上还有没有其余期待的更新,如果有就把该优先级更新到节点上,期待下一轮更新。

当 renderWithHook 失去新的 jsx elements,就会进入 reconcileChildren,与 current fiber 树一起进行 diff 而后更新到 wip fiber 树上,间接介绍一下 React 的 diff 算法吧:

function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    expirationTime: ExpirationTime,
  ): Fiber | null {
    let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;

    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {nextOldFiber = oldFiber.sibling;}
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        expirationTime,
      );
      if (newFiber === null) {if (oldFiber === null) {oldFiber = nextOldFiber;}
        break;
      }
      if (shouldTrackSideEffects) {if (oldFiber && newFiber.alternate === null) {deleteChild(returnFiber, oldFiber);
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {resultingFirstChild = newFiber;} else {previousNewFiber.sibling = newFiber;}
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    if (newIdx === newChildren.length) {deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }

    if (oldFiber === null) {for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(
          returnFiber,
          newChildren[newIdx],
          expirationTime,
        );
        if (!newFiber) {continue;}
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {resultingFirstChild = newFiber;} else {previousNewFiber.sibling = newFiber;}
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
}

比照 vue2,最大的不同点大略就是不是应用前后游标遍历的算法,间接是从结尾开始遍历比照,这次要就是因为 fiber 构造,兄弟节点是是采纳单链表的模式而不是数组(为什么是单链表而不能是数组?因为 diff 是能够中途打断的,而且遍历算法是深度优先遍历,如果采纳数组无奈或者很难存每一层级遍历的以后地位,其实 fiber 的 return 就是充当这个遍历地位记录),当然是不能前后游标遍历的算法了;其余的例如没有 key 则依照程序或如果有 key 则依据 key map 找到对应元素就更新,遍历完结,旧列表还剩元素就是革除,新列表还剩元素就插入,都是大体没有变动的。
当 diff 算法完结后,fiber 上的 effectTag 会标记这次更新后所须要做的操作,而后在 completeUnitOfWork 上:

function completeUnitOfWork(workInProgress: Fiber): Fiber | null {while (true) {
        const current = workInProgress.alternate;
        const returnFiber = workInProgress.return;
        const siblingFiber = workInProgress.sibling;
        if ((workInProgress.effectTag & Incomplete) === NoEffect) {
            nextUnitOfWork = workInProgress;
            // 调用 completeWork,弹出 context
            nextUnitOfWork = completeWork(
              current,
              workInProgress,
              nextRenderExpirationTime,
            );
            // 重置 childExpirationTime
            resetChildExpirationTime(workInProgress, nextRenderExpirationTime);
            if (nextUnitOfWork !== null) {return nextUnitOfWork;}
            // 把须要减少删除更新的 fiber 节点增加道父节点的 effectList 上
             if (
          returnFiber !== null &&
          (returnFiber.effectTag & Incomplete) === NoEffect
        ) {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;
            }
            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) {return siblingFiber;} else if (returnFiber !== null) {
          workInProgress = returnFiber;
          continue;
        } else {return null;}
    } else {
        // 节点没有实现,可能有谬误抛出
         const next = unwindWork(workInProgress, nextRenderExpirationTime);
        if (next !== null) {
            next.effectTag &= HostEffectMask;
            return next;
        }
        if (returnFiber !== null) {
        returnFiber.firstEffect = returnFiber.lastEffect = null;
        returnFiber.effectTag |= Incomplete;
      }
    }
}

次要有三个工作:

  1. 清理协程栈,筹备执行下一个 fiber
  2. 重置 fiber 的 childExpirationTime 标记
  3. 把须要减少删除更新的 fiber 节点增加到父节点的 effectList 上,前面只有遍历根节点的 effcetList 就能够实现最初的 commit,就不须要再深度遍历

到此 renderRoot 的工作根本实现,进入 completeRoot 阶段:

function completeRoot(
  root: FiberRoot,
  finishedWork: Fiber,
  expirationTime: ExpirationTime,
): void {
    root.finishedWork = null;
    runWithPriority(ImmediatePriority, () => {commitRoot(root, finishedWork);
    });
}

completeRoot 次要还是间接调度执行 commitRoot 办法,commitRoot 曾经是最初一步了,然而也是最简单,最多步骤,代码太长就不贴了间接剖析步骤:

  1. markCommittedPriorityLevels, 传入的参数 expirationTime 是 root.expirationTime 或者 root.childExpirationTime
    如果 expirationTime 是 NoWork 证实所有 work 都曾经实现
    如果 expirationTime 都低于 latestPingedTime,证实之前 latestPingedTime 优先级曾经实现,设置为 NoWork
    如果 expirationTime 都低于 latestPendingTime,证实之前 latestPendingTime 优先级曾经实现,设置为 NoWork
    如此类推
    findNextExpirationTimeToWorkOn 从新设置 root.expirationTime 和 root.nextExpirationTimeToWorkOn

// 2,3,5 都是不停在遍历 effect 链表

  1. commitBeforeMutationLifecycles
    如果是 FunctionComponent, ForwardRef, SimpleMemoComponent
    (1) commitHookEffectList,遍历查看是否存在 UnmountSnapshot 的 effectTag
    如果是 ClassComponent
    (2) 调用 ClassComponent 的 getSnapshotBeforeUpdate 办法

    1. commitAllHostEffects 这个阶段开始对实在 dom 节点造成影响,对 dom 树增删改解决
      执行 useLayoutEffect 的清理函数, 执行 dom 的属性更新
    2. 把 wip 切换为 current // 当初曾经更新完了 dom
    3. commitAllLifeCycles
      如果是 FunctionComponent, ForwardRef, SimpleMemoComponent
      (1) 执行 useLayoutEffect hook //dom 曾经是更新了,这个时候还在 js 代码的线程,还没渲染,所以这个时候批改 dom,用户是感知不到的,而 useEffect 是 dom 更新完,曾经渲染到界面后才触发
      如果是 ClassComponent
      (2) 执行 ClassComponent 的 componentDidMount/componentDidUpdate 办法
      commitUpdateQueue
    4. flushPassiveEffects 办法会调起 passiveEffectCallback,而 passiveEffectCallback 其实就是 commitPassiveEffects
      所以 flushPassiveEffects 调用点会有几个中央;

      1. scheduleRootUpdate 办法 // 下一次 schedule 更新的时候
      2. renderRoot 办法 // 执行下一个 Root 的更新的时候,会把前一个的 passiveEffects 清空
      3. dispatchAction 办法 // setState 办法触发,不在以后渲染中 会触发 flushPassiveEffects,在以后渲染中则不会
      4. commitRoot 办法 // 设置定一个调度,一个宏工作中更新
        passiveEffectCallbackHandle = scheduler.unstable_runWithPriority(scheduler.unstable_NormalPriority, function () {
        return schedulePassiveEffects(callback);
        });
        // 所以 passiveEffect 不肯定是延后调用的,然而如果有多个 Root 期待更新,前一个 Root 会先清空 PassiveEffect,再接着进入更新流程
        // 这里 Root 的意思是带有 HostRoot 标记的组件,也就是 reactDom.render 的根节点
        // 至多在一个组件树外面是延后调用的
        // flushPassiveEffects 保障在下一轮更新前调用,然而执行机会可能是同步的也可能是异步的
    5. 最初再更新 root.expirationTime
      // commit 实现之后是不会把 root 移除出调度队列的,会在下次调度,findHighestPriorityRoot 的时候移除

到此 commitRoot 实现,一次残缺的渲染完结,前面再会循环查找还有没有期待解决的 Root。

问题解答

理解了整个渲染流程之后,能够开始答复一开始的问题了

Virtual Dom 是如何转化 Fiber 构造的?

Virtual Dom 是在 reconcile 的过程中转换到 fiber 构造的

workInProgress 和 current 为什么要有两个 fiber 树

如果只有一棵树的话,因为渲染过程是能够中断的,渲染过程中又可能会触发新的优先级的渲染,而这些优先级都标记在 fiber 树上,还有 fiber 节点上的 effectList 保留的都是渲染过程中产生后续解决的 fiber 节点;如果被中断,fiber 树上就会有脏的状态,难以清理。

同一 fiber 节点树被反复标记,低优先级被高优先级笼罩会怎么?

并不会又问题,因为 hook 是有 updateQueue 来记录每个更新,这个更新对象还有 expirationTime 记录这个更新的优先级,
每次执行的更新的时候,会遍历 updateQueue,会挑出高于这个渲染优先级的 update 来进行更新,低于这次渲染优先级的会跳过
最初更新的时候 fiber 的优先级会设置成残余的 remainingExpirationTime

为什 componentWillMount 会反复触发

因为在 beginWork 阶段调起的,在异步执行环境,work 有可能屡次中断从新执行,所以会屡次执行

fiber 更新在什么状况下会被暂停或者抢占

  1. 以后的工夫片用完
  2. 完结 diff 过程或者 commit 过程
    也就是不是轻易就能产生中断,必须等以后工夫片完结
    判断是否须要中断,
  3. 渲染的优先级跟以后的优先级不一样
  4. 渲染 Root 跟以后的 Root 不一样

完结

整个渲染剖析真的是挺漫长,然而剖析完之后又会感觉没有设想中那么简单,基本原理还是很好了解,然而细节的中央的确很多,前面再持续剖析 hook 实现原理。

正文完
 0