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

开始

又是很久很久没写新货色了,刚好最近工夫都比拟闲暇,终于能有工夫整顿之前的浏览源码的笔记;人不知;鬼不觉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实现原理。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理