React源码阅读(三)- render

  • React源码入门篇(一)第一篇文章我们主要初步对React源码进行划分, 然后从React.createRoot()ReactDom.reader中的renderupdateContainer函数入手
  • Scheduler调用了一系列函数,最后又调用了renderRootSync或者renderRootConcurrent走入了我们的第三部分render, 那么这篇文章就从renderRootSync开始

一、renderRootSync函数

我们直接看关键逻辑,在起始的时候workInProgressRootworkInProgressRootRenderLanes都为null, 故会调用prepareFreshStack。 接着通过一个while循环, 调用了workLoopSync()函数, 当workLoopSync()流程走完后, 重置workInProgress进入commit标识render阶段结束

js 复制代码
function renderRootSync(root: FiberRoot, lanes: Lanes) {
  const prevExecutionContext = executionContext;
  executionContext |= RenderContext;
  const prevDispatcher = pushDispatcher();
  ....
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
     ...
    prepareFreshStack(root, lanes);
  }
  // 进入了一个循环, 主要是出现问题的时候会重试吧
  do {
    try {
      workLoopSync();
      break; // 实际上正常执行完一次就会被break出去
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
  resetContextDependencies(); // 重置上下文信息

  executionContext = prevExecutionContext;
  popDispatcher(prevDispatcher);
  ...
  // 置空标识当前render阶段结束, 没有正在执行的render过程
  workInProgressRoot = null;
  workInProgressRootRenderLanes = NoLanes;
  return workInProgressRootExitStatus;
}

prepareFreshStack

  • 在初始化步骤去调用prepareFreshStack的作用就是给各种变量做初始化。 其中很重要的包括给workInprogress赋值
ts 复制代码
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
  ...
  
  workInProgressRoot = root;
  // 根据hostRootFiber创建rootWorkInProgress. 赋值给workInPropgress
  const rootWorkInProgress = createWorkInProgress(root.current, null);
  workInProgress = rootWorkInProgress;
  workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
  workInProgressRootExitStatus = RootInProgress;
  workInProgressRootFatalError = null;
  workInProgressRootSkippedLanes = NoLanes;
  workInProgressRootInterleavedUpdatedLanes = NoLanes;
  workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
  workInProgressRootPingedLanes = NoLanes;
  workInProgressRootConcurrentErrors = null;
  workInProgressRootRecoverableErrors = null;

  return rootWorkInProgress;
}

二、 workLoopSync函数

看到xxxxLoop的命名就知道这里又存在了一个循环, workLoopSync函数的逻辑很简单, 当workInPropgress不为空的时候, 循环调用performUnitOfWork

js 复制代码
function workLoopSync() {
  // Perform work without checking if we need to yield between fiber.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

三、 performUnitOfWork函数

  • performUnitOfWork其实就是通过beginWorknext节点的过程,unitOfWork就是当前传入的workInppogress, 当next节点为空时则说明完成当前工作单元完成, 此时需要completeUnitOfWorknext节点不为空时, 赋值给workInProgress, 这样就会继续调用performUnitOfWork
js 复制代码
function performUnitOfWork(unitOfWork: Fiber): void {
 // 拿到是这个节点上一次的内容,也就是旧节点
 // 当在初次挂载阶段的时候, 只有在处理hostFiberRoot时,current才不为空,处理其他节点时都为空
  const current = unitOfWork.alternate; 
  ...
  // 通过调用beginWork生成next节点
  let next;
  // 对mode进行了判断, 无论走哪个逻辑核心都在next = beginWork(...)
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, renderLanes);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, renderLanes);
  }
    ....
  if (next === null) {
    // 没有子节点的话则说明当前工作单元完成
    completeUnitOfWork(unitOfWork);
  } else {
    // 还有子节点的话workInProgress执行子节点, 构成循环链路
    workInProgress = next;
  }
  ReactCurrentOwner.current = null;
}

四、beginWork函数

  • 传入的current指的是当前页面使用的Fiber结构
  • 传入的workInProgress指的是当前正在构造的Fiber结构
  • 这其实就是我们经常提到的双缓存, React的流程就是从根节点开始遍历Fiber树,然后标记发生改变的FiberNode,然后再将改变映射到真实的UI上,那这个过程不应该直接影响到当前显示使用的Fiber, 故就存在了两个fiber, 旧的展示于页面, 然后根据旧Fiberstate的变化生成新的Fiber, 然后再切换。两颗Fiber树之间通过alternate属性链接。
  • 一开始第一次进入beginWork的时候拿到的currentworkInProgress都是hostRootFibertag为3。
js 复制代码
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
   ...
  // 对不同tag进行不同的处理
  switch (workInProgress.tag) {
    .....
   }
}

其中通过Switch Case对不同的tag进行了不同的处理, 但是不同的方法内部都会调用 reconcileChildren, React内部定义了25种tag, 比较常见的有FunctionComponentClassComponentHostComponent, hostRootForwardRefMemoComponent, LazyComponent, 等等

五、updateHostRoot

我们先从hostFiber入手, 在遇到tagHostRoot的时候会调用updateHostRoot

  • updateHostRoot: 针对于taghostRoot的时候会执行的函数, 用于处理HostFiberNode
js 复制代码
function updateHostRoot(current, workInProgress, renderLanes) {
  pushHostRootContext(workInProgress);

  const nextProps = workInProgress.pendingProps;
  const prevState = workInProgress.memoizedState;
  const prevChildren = prevState.element;
  // 更新updateQueue
  cloneUpdateQueue(current, workInProgress);
  // 根据updateQueue更新memozidState---后面再做详细解释
  processUpdateQueue(workInProgress, nextProps, null, renderLanes);
  // 拿到更新之后的state
  const nextState: RootState = workInProgress.memoizedState;
  // 文章一的createFiberRoot中我们讲过了, hostFiberNode的stateNode指向的是FiberRootNode
  const root: FiberRoot = workInProgress.stateNode;
  pushRootTransition(workInProgress, root, renderLanes);
  ....
  const nextChildren = nextState.element;
  if (supportsHydration && prevState.isDehydrated) {
   ....// 服务端渲染相关, 这里直接跳过了
  } else {
    // 判断是否可以直接复用
    if (nextChildren === prevChildren) {
     // 判断是否有child更新
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
    // 开始reconcile
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  }
  return workInProgress.child;
}

cloneUpdateQueue

  • 其中通过cloneUpdateQueue处理updateQueue。 本质上就是将current FiberupdateQueue复制一遍给workInProgress Fiber
js 复制代码
export function cloneUpdateQueue<State>(
  current: Fiber,
  workInProgress: Fiber,
): void {
  const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
  const currentQueue: UpdateQueue<State> = (current.updateQueue: any);
  if (queue === currentQueue) { // 还没处理过的时候引用一致
    const clone: UpdateQueue<State> = {
      baseState: currentQueue.baseState,
      firstBaseUpdate: currentQueue.firstBaseUpdate,
      lastBaseUpdate: currentQueue.lastBaseUpdate,
      shared: currentQueue.shared,
      effects: currentQueue.effects,
    };
    workInProgress.updateQueue = clone;
  }
}

reconcileChildren

  • 对于hostRoot来说,第一次走到beiginWork后会走到reconcileChildren, 这个函数的逻辑很简单,就做了一个判断, 判断current是否为空,空的话调用mountChildFiberscurrentFirstChild传的为空, current不为空的话调用reconcileChildFibers进行更新, currentFirstChild的为当前current Fiberchild
js 复制代码
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

接着我们发现这两个方法其实都是调用同一个方法的返回值, 区别就在于传入的参数shouldTrackSideEffects不同(用于表示是否追踪副作用)

六、 reconcileChildFibers

前面我们也提到了 在首次挂载处理hostRoot的时候,current不为null, 故自然走到了reconcileChildFibers, 在这个函数中又涉及了$$typeofSwitch Case判断, React中也定义了多种类型的$$typeof

js 复制代码
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    const isUnkeyedTopLevelFragment =
      typeof newChild === 'object' &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children;
    }
    // 单子节点的处理
    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) { 
        case REACT_ELEMENT_TYPE: // 最常见的函数组件呀, 类组件呀
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_PORTAL_TYPE:
          return placeSingleChild(
            reconcileSinglePortal(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        case REACT_LAZY_TYPE: // lazy组件
          const payload = newChild._payload;
          const init = newChild._init;
          // TODO: This function is supposed to be non-recursive.
          return reconcileChildFibers(
            returnFiber,
            currentFirstChild,
            init(payload),
            lanes,
          );
      }
      // 数组子节点的处理
      if (isArray(newChild)) {
        return reconcileChildrenArray(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }

      if (getIteratorFn(newChild)) {
        return reconcileChildrenIterator(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }

      throwOnInvalidObjectType(returnFiber, newChild);
    }
    // 文本子节点
    if (
      (typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number'
    ) {
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '' + newChild,
          lanes,
        ),
      );
    }
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }
  • 可以看到无论是走到了哪个分支, 都会调用placeSingleChild, 传入的参数是根据不同节点情况调用不同reconcile函数的返回值
  • 我们从最常见的react-element的情况深入
js 复制代码
return placeSingleChild(
  reconcileSingleElement(
    returnFiber,
    currentFirstChild,
    newChild,
    lanes,
  ),
);

reconcileSingleElement

  • 先看reconcileSingleElement 一开始我们拿到的childnull, 故会直接走到createFiberElement, 然后处理它的Refreturn属性, 最后返回一个Fiber结构

createFiberFromElement

  • 从语义上也很好理解, 这个函数的用途就是通过element构造出对应的Fiber结构,调用链路是 createFiberFromTypeAndProps-> reateFiber -> FiberNode, 文章一已经描述了Fiber相关的这里就不赘述了
js 复制代码
export function createFiberFromElement(
  element: ReactElement,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
  let owner = null;
  const type = element.type;
  const key = element.key;
  const pendingProps = element.props;
  const fiber = createFiberFromTypeAndProps(
    type,
    key,
    pendingProps,
    owner,
    mode,
    lanes,
  );
  return fiber;
}

placeSingleChild

  • 那我们再看placeSingleChild, 其中判断了shouldTrackSideEffects, 我们在前面也提到了在进行首次Fiber处理的时候, 只有在处理hostFiberRoot的时候current是不为null的,此时shouldTrackSideEffectstrue, 故Fiber会打上Placement的标记, 最后直接整个Fiber挂载上去即可
js 复制代码
// mount都是插入的操作
function placeSingleChild(newFiber) {
    if (shouldTrackSideEffects && newFiber.alternate === null) {
      newFiber.flags |= Placement;
    }
    return newFiber;
  }
  • 我们对以下的简单代码执行以上流程,然后看看他生成的Fiber树. 可以看到现在就是一条路走到嘿, 通过child串到底,在return回来
    • tag: 3 hostRoot
    • tag: 8 Mode
    • tag: 0 FunctionComponenet
    • tag 5 hostComponent

七、 reconcileChildrenArray

  • 诶你会发现我们的三剑客之一sibling还没出现呢, 还记得我们之前在reconcileChildFibers还做了一个isArray(newChild)的判断,那我们浅浅改一下代码, 让他们出现多个节点并行,然后再调试一波, 可以看到当前newChild是一个长度为4的数组

  • isArray()判断返回为true的时候会调用reconcileChildrenArray(), 这个函数就是用来处理多节点的。其实就是遍历newChild 然后挨个生成Fiber, 通过sibling连接起来, 最后返回头结点
js 复制代码
function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ): Fiber | null {
    let resultingFirstChild: Fiber | null = null;  // 结果返回出去的child
    let previousNewFiber: Fiber | null = null; //  上一个处理的child
    let oldFiber = currentFirstChild; // 旧的child
    let lastPlacedIndex = 0;
    let newIdx = 0; // 从0开始
    let nextOldFiber = null;
    .....
    // 说明处于初始化过程呢, 还没有对应的旧Fiber, 此时会快速生成, 因为不需要经过对比,直接都是插入
    if (oldFiber === null) {
       // 遍历element数组
      for (; newIdx < newChildren.length; newIdx++) {
         // 根据element生成对应的Fiber结构
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) { // 无效的Fiber, 跳过
          continue;
        }
        ...
        if (previousNewFiber === null) { // 初次执行的时候这个就为null
          resultingFirstChild = newFiber; // 给他赋值让最后返回的结果是指向头指针的
        } else {
         // 不为null的时候说明前面已经有生成一些Fiber结构了
         // 此时通过sibling将他们链接起来即可
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber; // 移动指针, 让previous总是指向当前处理的前一个Fiber
     }
    ...
      return resultingFirstChild;
    }
    .....

八、completeUnitOfWork

依照我们上面所讲述的逻辑,因为Fiber是通过next逐行 生成的,故最后生成的fiber树如下所示(包含在step中的节点), 但是我们最终的目标的Fiber结构应该包括虚线框中的节点

  • 相关的逻辑还是处于performUnitOfWork, 当我们处理完step5之后, 再执行beiginWork此时拿到的结果为null, 此时会走向completeUnitOfWork()
  • 可以看到该函数的核心逻辑在completeWork, 拿到next节点, 如果next节点存在值, 则赋值给workInprogress, 此时因为workInprogress不为空,故又会继续执行beginWork。 如果next节点为空的话,此时就会找到该节点的sibling节点,然后赋值给workInprogress, 那又开始找beginwork。 如果sibling节点还是为空的话, 则通过return找到其父节点,
js 复制代码
function completeUnitOfWork(unitOfWork: Fiber): void {
  // 当前的节点, 其子节点找完为null 
  let completedWork = unitOfWork;
  // 又开循环
  do {
    // 旧节点,也就是当前用于页面展示的节点
    const current = completedWork.alternate;
    // 该节点的父节点
    const returnFiber = completedWork.return;
    // 通过判断当前节点的flags标签, 当该标签未被处理过的时候flags为Incomplete
    if ((completedWork.flags & Incomplete) === NoFlags) {
      let next;
      if (
        !enableProfilerTimer ||
        (completedWork.mode & ProfileMode) === NoMode
      ) {
        next = completeWork(current, completedWork, subtreeRenderLanes);
      } else {
        startProfilerTimer(completedWork);
        next = completeWork(current, completedWork, subtreeRenderLanes);
        stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
      }
      if (next !== null) {
        workInProgress = next;
        return;
      }
    } else {....}

    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    completedWork = returnFiber
    workInProgress = completedWork;
  } while (completedWork !== null);
  ...

来个图, 会更清晰, 可以看到无论走哪条分支, 最后总会对workInProgress进行重新赋值, 那么就有可能会重新走到beginWork

先不考虑beginworkcompleteUnitofWork具体还做了其他什么, 我们可以拿一个比较复杂的Fiber结构走一遍流程, 大体的一个过程如图所示

  • 大体就是beiginWork就负责一条路走到黑, 啥也不管就直走, 发现,诶, 没路了就交给completeUnitofWork去处理
  • completeUnitofWork就像拿着电灯, 找找这找找那, 看后看右看前, 哪有路就往哪走, 直到实在没有路了为止

九、completeWork

再来具体看看completeUnitOfWork的核心逻辑completeWork拿到的next节点是什么, 以及他具体做了哪些操作。

  • 整个函数有七百多行,整体都是基于Fiber tag的判断然后执行不同的逻辑, 我们抽取部分我们关注的看看就OK
js 复制代码
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
  popTreeContext(workInProgress);
  // 这里跟beiginWork一致的,都是通过判断Fiber的tag标签去判断执行什么逻辑
  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent: .....
    case ClassComponent: ....
    case HostRoot: ...
    case HostComponent: ....
    case HostText: ...
    case SuspenseComponent: ....
    case HostPortal: ...
    case ContextProvider: ...
    case IncompleteClassComponent: ...
    case SuspenseListComponent: ....
    case OffscreenComponent:
    case LegacyHiddenComponent: ....
    case CacheComponent: ...

根据我们之前的Fiber结构, 当我们走入completeWork的时候,此时Fibertag为5, 那么此时会走到hostComponent, 也是比较核心的逻辑, 我们可以了解一波, 对应逻辑如下

js 复制代码
{
   popHostContext(workInProgress);
   const rootContainerInstance = getRootHostContainer(); //拿到根节点的DOM
   const type = workInProgress.type; // 拿到类型
   // 存在旧节点的时候进行更新操作
   if (current !== null && workInProgress.stateNode != null) {
     ...
   } else {
     ... 
     const currentHostContext = getHostContext();  // 拿到上下文信息
     const wasHydrated = popHydrationState(workInProgress);
     if (wasHydrated) { ... } else {
       // 根据Fiber创建对应的DOM结构信息
       const instance = createInstance(
         type,
         newProps,
         rootContainerInstance,
         currentHostContext,
         workInProgress,
       );
       // 基于appendChild通过找寻Fiber收集DOM
       appendAllChildren(instance, workInProgress, false, false);
       // 将创建好的DOM结构信息存放到stateNode属性中(这样appendAllChildren才能通过Fiber收集到hhh)
       workInProgress.stateNode = instance;  
       if (
         finalizeInitialChildren(
           instance,
           type,
           newProps,
           rootContainerInstance,
           currentHostContext,
         )
       ) {
         markUpdate(workInProgress);
       }
     }

     if (workInProgress.ref !== null) {
       markRef(workInProgress);
     }
   }
   bubbleProperties(workInProgress);
   return null;  // 处理该类型节点的时候都是直接返回null, 因为没有下一个节点需要去处理
 } 
  • 其中getRootHostContainer就是拿到当前的rootDOM节点, 本质上是去拿了rootInstanceStackCursor.current的值,我们在上面执行beginwork -> updateHostRoot的时候第一步就调用了pushHostRootContext,其中就是拿到hostRootstateNode(其实就是根应用节点)的containerInfo(指向的就是传入的DOM了)。
js 复制代码
function getRootHostContainer(): Container {
  const rootInstance = requiredContext(rootInstanceStackCursor.current);
  return rootInstance;
}

createInstance

  • 其中的createInstance的逻辑也比较重要, 可以看到分别调用了三个函数,我们逐个来了解一下
js 复制代码
export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance {
  let parentNamespace: string;
  if (__DEV__) {... } else {
    parentNamespace = ((hostContext: any): HostContextProd);
  }
  // 创建真实DOM对象
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  // 将Fiber挂载在创建好的DOM上
  precacheFiberNode(internalInstanceHandle, domElement);
  // 将props挂载在创建好的DOM上
  updateFiberProps(domElement, props);
  return domElement;
}
  • creatElement, 通过判断节点类型决定采创建对应的DOM对象, 当typesvg或者math的时候, 会修改namespaceURI, 然后调用createElementNS创建节点。其他情况则采用createElement
js 复制代码
export function createElement(
  type: string,
  props: Object,
  rootContainerElement: Element | Document | DocumentFragment,
  parentNamespace: string,
): Element {
  let isCustomComponentTag;
  // 根据上面拿到的DOM对象拿到对应的document对象
  const ownerDocument: Document = getOwnerDocumentFromRootContainer(
    rootContainerElement,
  );
  let domElement: Element;
  let namespaceURI = parentNamespace;
  if (namespaceURI === HTML_NAMESPACE) {
    namespaceURI = getIntrinsicNamespace(type);
  }
  if (namespaceURI === HTML_NAMESPACE) {
    ...
    domElement = ownerDocument.createElement(type);
  } else {
    domElement = ownerDocument.createElementNS(namespaceURI, type);
  }
  return domElement;
}

拿到的document和创建好的DOM如图所示

  • 再看precacheFiberNode, 就是把对应的Fiber挂载在创建好的DOM节点上
  • 再看updateFiberProps, 将对应的props挂载在创建好的DOM节点上

createInstance总体的逻辑都是在创建一个我们所需要的DOM节点。

appendAllChildren

紧接着又调用了appendAllChildren, 可以看到该函数的逻辑都是基于child不为空的情况下调用的。 故我们调试该节点进入completeWork

js 复制代码
appendAllChildren = function(
    parent: Instance,  // 创建好的DOM节点
    workInProgress: Fiber,  // 当前的Fiber
    needsVisibilityToggle: boolean, // false
    isHidden: boolean, // false
  ) {
    let node = workInProgress.child; 
    while (node !== null) {  // 有child的情况下
      if (node.tag === HostComponent || node.tag === HostText) {
        appendInitialChild(parent, node.stateNode);  
      } else if (node.tag === HostPortal) {
      } else if (node.child !== null) {
        node.child.return = node;
        node = node.child;  // 有子节点的话就往子节点走
        continue;
      }
      if (node === workInProgress) {
        return;  // 找完所有子级节点又回到他本身就可以拜拜了
      }
      while (node.sibling === null) {
        if (node.return === null || node.return === workInProgress) {
          return;  // 找完所有子级节点又回到他本身就可以拜拜了
        }
        node = node.return // 没有兄弟节点之后就往回走
      }
      node.sibling.return = node.return; 
      node = node.sibling; // 有兄弟节点就走向兄弟节点
    }
  };
  • 其中在tagHostComponent或者HostText(相当于叶子节点)的时候又调用了appendInitialChild, 逻辑如下, 调用了DOM上的appendChild方。

可以说appendAllChildren的作用就是把他所有孩子级的DOM节点通过appendChild收集起来,这个例子最后是拿到了四个Children node

这一步很重要, 可以说把零散的DOM节点串成了一棵DOM树 分别为以下这几个节点对应的DOM结构

bubbleProperties

继续看bubbleProperties, 看名字就大概他的作用了。将一些属性冒泡上去。 我们再来看具体逻辑。实际上就是收集了子节点的lanesflags, 然后累加了对应的actualDurationtreeBaseDuration

收集flags的步骤也很重要。 commit阶段很多都需要依赖subtreeFlags去判断

js 复制代码
function bubbleProperties(completedWork: Fiber) {
  const didBailout =
    completedWork.alternate !== null &&
    completedWork.alternate.child === completedWork.child; // 判断是否发生了回退

  let newChildLanes = NoLanes;                                                       
  let subtreeFlags = NoFlags;

  if (!didBailout) {
    if ((completedWork.mode & ProfileMode) !== NoMode) {
      let actualDuration = completedWork.actualDuration;
      let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
      let child = completedWork.child;
      while (child !== null) {
        newChildLanes = mergeLanes(
          newChildLanes,
          mergeLanes(child.lanes, child.childLanes),
        ); // 收集lanes
        subtreeFlags |= child.subtreeFlags; // 将子节点收集的flags记录起来
        subtreeFlags |= child.flags; // 以及子节点本身的flags
        actualDuration += child.actualDuration; // actualDuration也进行累加
        treeBaseDuration += child.treeBaseDuration; // treeBaseDuration进行累加
        child = child.sibling; // 通过while+sibling找完同级子节点
      }
      // 收集完进行赋值
      completedWork.actualDuration = actualDuration;
      completedWork.treeBaseDuration = treeBaseDuration;
    } else {...}
    }
    completedWork.subtreeFlags |= subtreeFlags; // 收集完进行赋值
  } else {....}
  completedWork.childLanes = newChildLanes;
  return didBailout;
}

十、renderRootConcurrent

搞定整个renderRootSync流程后,我们再继续了解一波renderRootConcurrent。 其基本逻辑和renderRootSync很类似。 核心都是使用了一个do while循环去调用函数。 renderRootSync调用的是workLoopSyncrenderRootConcurrent调用的是workLoopConcurrent

js 复制代码
function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
    ...
  do {
    try {
      workLoopConcurrent();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
  resetContextDependencies();
  popDispatcher(prevDispatcher);
  executionContext = prevExecutionContext;
  if (workInProgress !== null) 
    return RootInProgress;
  } else {
    // Completed the tree.
    if (enableSchedulingProfiler) {
      markRenderStopped();
    }
    workInProgressRoot = null;
    workInProgressRootRenderLanes = NoLanes;
    return workInProgressRootExitStatus;
  }
}

workLoopConcurrent

该函数的逻辑和workLoopSync基本一致。都是开了一个while循环去调用performUnitOfWork。 但是该函数多了一个条件,也就是shouldYield

js 复制代码
function workLoopConcurrent() {
  // Perform work until Scheduler asks us to yield
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

shouldYield

  • shouldYield的作用就是判断是否应该中断当前执行。 如果返回false则表示还有可执行时间。 如果返回true则会暂停执行。 剩下的工作留到下一次宏任务进行处理。 这也就是renderRootConcurrentrenderRootSync区别的核心所在了。 前者可以中断FiberTree的创建而后者无法
  • 目前的逻辑就是判断间隔的时间, 是否小于frameInterval(定义为5ms)
  • 其中还可以存在逻辑通过enableIsInputPending字段判断是否要依赖其他依据再更精准的进行判断防止高频地打断,减少打断的频率,其中利用了Scheduling/isInputPending, 该API可以检查事件队列中是否有挂起的输入事件,表明用户正试图与页面交互。
js 复制代码
function shouldYieldToHost() {
  const timeElapsed = getCurrentTime() - startTime;
  if (timeElapsed < frameInterval) {
    return false;
  }
  if (enableIsInputPending) {
    if (needsPaint) {
      return true;
    }
    if (timeElapsed < continuousInputInterval) {
      if (isInputPending !== null) {
        return isInputPending();
      }
    } else if (timeElapsed < maxInterval) {
      if (isInputPending !== null) {
        return isInputPending(continuousOptions);
      }
    } else 
      return true;
    }
  }
  return true;
}
  • 既然有打断就需要有回复的过程。 我们可以注意到如果当然workInprogress不为空的情况下, 被打断的话。 此时renderRootConcurrent会返回statusRootInProgress
js 复制代码
if (workInProgress !== null) return RootInProgress; }
  • 接着我们回看performConcurrentWorkOnRoot的逻辑.可以看到RootInProgress的状态时,就直接调用ensureRootIsSchedule重新开启调度了。
  • 如果前后优先级一致的话, callbackNode不会修改。
    • 此时会直接ensureRootIsSchedule会直接return回来。
    • 那么此时performConcurrentWorkOnRoot判断root.callbackNode ==== originialCallbackNodetrue, 那么就返回了performConcurrentWorkOnRoot他本身。
    • 再继续回看workLoop逻辑。 如果此时performConcurrentWorkOnRoot返回的是一个函数, 那么会继续赋值给currentTaskcallback, 然后继续调用。不过workLoop同样存在shouldYield逻辑。故最后可能也是走向了重新创建宏任务执行
    • 此时workInProgress不会重置, 还是从上次暂停的地方继续开始构建。
  • 如果有更高优先级的任务。 那么此时就按照scheduleCallback去进行任务调度了

此部分内容需要结构第二章一起看比较清晰

十一、总结

react fiberTree的创建过程逻辑主要采用的是DFS深度优先遍历。 beginwork自上而下, 逐层地根据element创建对应的Fiber节点。 当走到底的时候,再通过completeWork进行自下而上的回找完善。当然completeWork也承担了比如说创建DOM节点, 冒泡DOM,lanes, flags等任务。 上述两个过程是交替执行的, 当workInProgressnull的时候说明Fiber树已经构建完毕。如果调用的是renderRootConcurrent那么整个Fiber构建的过程都是可以被打断的。

这一部分内容实在太多了,因为不同的Fiber节点对应的方法都存在差异,包括挂载和更新也存在差异。并发和同步也存在差异。 render过程是一个很庞大的过程。故此文章基本跟着调试走,理解一些主要的逻辑,然后尽可能的多发散一些逻辑。 后面会再更细致地处理比如函数组件如何处理, 类组件如何处理, hook如何处理。

相关推荐
江城开朗的豌豆1 分钟前
🔥 Vue组件传值:小杨教你玩转父子组件通信
前端·javascript·面试
江城开朗的豌豆1 分钟前
Vue组件花式聊天指南:6种传值妙招,总有一款适合你
前端·javascript·vue.js
黑土豆6 分钟前
在Vue3项目中实现PDF文件解析与预览的完整实践
前端·javascript·vue.js
han_11 分钟前
前端如何动态执行JavaScript代码?
前端·javascript
10年前端老司机3 小时前
10道js经典面试题助你找到好工作
前端·javascript
小小小小宇8 小时前
TS泛型笔记
前端
小小小小宇8 小时前
前端canvas手动实现复杂动画示例
前端
codingandsleeping8 小时前
重读《你不知道的JavaScript》(上)- 作用域和闭包
前端·javascript
小小小小宇9 小时前
前端PerformanceObserver使用
前端
zhangxingchao10 小时前
Flutter中的页面跳转
前端