美文网首页
React源码06 - 完成节点任务

React源码06 - 完成节点任务

作者: 晓风残月1994 | 来源:发表于2020-08-10 01:10 被阅读0次

    06 - 完成节点任务

    完成节点更新之后完成节点的创建,并提供优化到最小幅度的DOM更新列表。

    1. completeUnitOfWork

    第 04 篇说过 renderRoot 做的事情:

    • 调用 workLoop 进行循环单元更新
    • 捕获错误并进行处理
    • 走完流程之后善后

    现在在 workLoop 中调用 performUnitOfWork。
    next = beginWork(current, workInProgress, nextRenderExpirationTime)
    每次都 return child,以便继续循环向下单路查找,直到触及叶子节点(树枝到头了就是叶子)返回 null,也就是叶子节点没有 child 了。这算是 renderRoot 中完成了一次 workLoop 调用。
    此时,传入该叶子节点执行 completeUnitOfWork。
    之后尝试继续循环执行 wokrLoop,处理其他路。

    completeUnitOfWork:

      1. 根据是否中断来调用不同的处理方法

    在外部 renderRoot 使用 do...while 调用 workLoop 时会使用 try...catch,只要不是致命错误,就记录相应错误(比如 Suspense 的 promise 在中间过程中的合理报错),然后继续执行循环。

      1. 判断是否有兄弟节点来执行不同的操作
      1. 完成节点之后赋值 effect 链

    在之前的 beginWork 中为节点标记了相应的 sideEffect,也就是等到 commit 阶段中更新 dom 时的操作依据(增删改等)。而在 completeUnitOfWork 中则将 fiber 上的 sideEffect 进一步进行串联,方便 commit 时使用。
    之前第 03 篇中说过每个 fiber 上都有:

    • effectTag: SideEffectTag。用来记录 SideEffect。
    • nextEffect: Fiber | null。单链表用来快速查找下一个side effect。
    • firstEffect: Fiber | null。 子树中第一个side effect。
    • lastEffect: Fiber | null。子树中最后一个side effect。

    有些像是层层嵌套的文件夹 A/B/C/D,B中只记录了C/D。这些打了 effectTag 标记的 fiber 节点通过这些指针单独组成单向链表,反正都是些指针引用,也不占多少空间。

    通过不断地 completeUnitOfWork 将 effect 汇总串联到上层节点,最终 RootFiber 上的 firstEffect 到 lastEffect 这个链表中记录了所有带有 effectTag 的 fiber 节点,即最终在 commit 阶段所有需要应用到 dom 节点上的 SideEffect。

    TODO:commitRoot 方法
    **
    SideEffectTag 清单:

    export type SideEffectTag = number;
    
    // Don't change these two values. They're used by React Dev Tools.
    export const NoEffect = /*              */ 0b00000000000;
    export const PerformedWork = /*         */ 0b00000000001;
    
    // You can change the rest (and add more).
    export const Placement = /*             */ 0b00000000010;
    export const Update = /*                */ 0b00000000100;
    export const PlacementAndUpdate = /*    */ 0b00000000110;
    export const Deletion = /*              */ 0b00000001000;
    export const ContentReset = /*          */ 0b00000010000;
    export const Callback = /*              */ 0b00000100000;
    export const DidCapture = /*            */ 0b00001000000;
    export const Ref = /*                   */ 0b00010000000;
    export const Snapshot = /*              */ 0b00100000000;
    
    // Update & Callback & Ref & Snapshot
    export const LifecycleEffectMask = /*   */ 0b00110100100;
    
    // Union of all host effects
    export const HostEffectMask = /*        */ 0b00111111111;
    
    export const Incomplete = /*            */ 0b01000000000;
    export const ShouldCapture = /*         */ 0b10000000000;
    

    performUnitOfWork:

    function performUnitOfWork(workInProgress: Fiber): Fiber | null {
      // The current, flushed, state of this fiber is the alternate.
      // Ideally nothing should rely on this, but relying on it here
      // means that we don't need an additional field on the work in
      // progress.
      const current = workInProgress.alternate;
    
      // See if beginning this work spawns more work.
      startWorkTimer(workInProgress);
    
      let next;
      if (enableProfilerTimer) {
        if (workInProgress.mode & ProfileMode) {
          startProfilerTimer(workInProgress);
        }
    
        next = beginWork(current, workInPrognextRenderExpirationTimeress, );
        workInProgress.memoizedProps = workInProgress.pendingProps;
    
        if (workInProgress.mode & ProfileMode) {
          // Record the render duration assuming we didn't bailout (or error).
          stopProfilerTimerIfRunningAndRecordDelta(workInProgress, true);
        }
      } else {
        next = beginWork(current, workInProgress, nextRenderExpirationTime);
        workInProgress.memoizedProps = workInProgress.pendingProps;
      }
    
      if (next === null) {
        // 如果这次遍历时调用beginWork返回了null,说明已经到了单路的叶子节点了,于是调用completeUnitOfWork。
        // 当前的workInProgress就是叶子节点,因为寻找child却返回了null,说明到头了。
        // If this doesn't spawn new work, complete the current work.
        next = completeUnitOfWork(workInProgress);
      }
    
      ReactCurrentOwner.current = null;
    
      return next;
    }
    

    completeUnitOfWork:
    如果说 workLoop 在局部子树中是从上向下处理节点。那么 completeUnitOfWork 中则是在局部子树中从下向上处理节点。

    if (next === null) {
      // 如果这次遍历时调用beginWork返回了null,说明已经到了单路的叶子节点了,于是调用completeUnitOfWork。
      // 当前的workInProgress就是叶子节点,因为寻找child却返回了null,说明到头了。
      // If this doesn't spawn new work, complete the current work.
      next = completeUnitOfWork(workInProgress);
    }
    

    completeUnitOfWork 代码有些多,主体是个 while 循环,其中有下面这个遍历逻辑:

    • 在 performUnitOfWork 中,如果从上至下单路遍历到了叶子节点,则开始调用 completeUnitOfWork 进行向上遍历。
    • 如果有 sibling 兄弟节点则 return 兄弟节点,以便 workLoop 中再次调用 performUnitOfWork 对刚才的兄弟节点进行遍历。
    • 又一次单路到头了,遇到了叶子节点,则再次 completeUnitOfWork 处理叶子节点。
    • 如果当前子树兄弟节点全处理完了,则向上对父节点进行 completeUnitOfWork 处理。如果父节点也有兄弟节点,则同理。

    最终效果就是一整棵 RootFiber 树:

    • 每个节点都会先使用 performUnitOfWork 处理一次。
    • 再使用 completeUnitOfWork 处理一次。

    **
    completeUnitOfWork 节选:

    function completeUnitOfWork(workInProgress: Fiber): Fiber | null
    // ...
    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 {
      // 说明更新过程完成了,到rootFiber了。等着commit阶段了
      return null;
    }
    

    completeUnitOfWork 中的一些工作:

    • 重设 ChildExpirationTime
    • completeWork

    2. 重设 ChildExpirationTime

    ChildExpirationTime 记录了一个 fiber 的子树中优先级最高的更新时间。尽管产生更新需求的节点可能是整个应用 fiber 树中的某个节点,但进行更新调度时是从顶部 RootFiber 开始参与调度的。
    因此通过 ChildExpirationTime 不断向上汇总子树的最高优先级的更新时间,最终 RootFiber 的 ChildExpirationTime 记录了整棵树中最高优先级的更新时间。
    而在 completeUnitOfWork 从下向上进行信息汇总时,如果某个节点的更新任务已经得到执行,也就是没有自身的 expirationTime 了,那么 completeUnitOfWork 中就需要顺便不断向上重置 ChildExpirationTime。通过调用:
    resetChildExpirationTime(workInProgress, nextRenderExpirationTime);

    3. completeWork

    • pop 各种 context 相关的内容
    • 对于 HostComponent 执行初始化
    • 初始化监听事件

    大部分类型的 fiber 节点不需要在这一步做什么事情(Suspense 类型以后再说),而以下两种类型有点东西:

    • HostComponent
    • HostText

    下面主要谈谈 HostComponent(该 fiber 类型对应到原生 dom)在 completeWork 中的相关操作。

    4. HostComponent

    HostComponent 中涉及到 updateHostComponent。在一次更新而非渲染中:

    • diffProperties 计算需要更新的内容
    • 不同的 dom property 处理方式不同

    首次渲染时

    创建对应的 dom 实例:

    let instance = createInstance(
      type,
      newProps,
      rootContainerInstance,
      currentHostContext,
      workInProgress,
    );
    
    appendAllChildren(instance, workInProgress, false, false);
    
    if (
      finalizeInitialChildren( // finalizeInitialChildren 最终返回是否需要 auto focus 自动聚焦
        instance,
        type,
        newProps,
        rootContainerInstance,
        currentHostContext,
      )
    ) {
      markUpdate(workInProgress); // 如果是需要 autoFocus,那么还要设置 sideEffect。
    }
    workInProgress.stateNode = instance;
    
    function markUpdate(workInProgress: Fiber) {
      // Tag the fiber with an update effect. This turns a Placement into
      // a PlacementAndUpdate.
      workInProgress.effectTag |= Update;
    }
    
    function markRef(workInProgress: Fiber) {
      workInProgress.effectTag |= Ref;
    }
    

    appendAllChildren:
    找到子树中第一层 dom 类型的节点,append 到自身对应的 dom 实例下。即 workInProgress.stateNode 所记录的原生 dom 实例。
    因为在每次 completeUnitOfWork 时遇到 HostComponent 即原生 dom 对应的 fiber 类型时,都会 appendAllChildren。
    所以也就是对 fiber 树这个虚拟 dom 进行提纯,最终从下向上构建出纯 dom 树。
    **
    finalizeInitialChildren:
    appendAllChildren 之后紧接着 finalizeInitialChildren 初始化事件监听体系。
    setInitialProperties 初始化事件监听(后面会单独讲事件监听)。
    初始化 dom attribute(主要是一些原生 dom 操作)。
    初始化markUpdate、mark*、 defaultValue、isControlled、处理 style 属性,px 补全等。

    下面展开叙述。

    finalizeInitialChildren 最终返回是否需要 auto focus 自动聚焦:

    export function finalizeInitialChildren(
      domElement: Instance,
      type: string,
      props: Props,
      rootContainerInstance: Container,
      hostContext: HostContext,
    ): boolean {
      setInitialProperties(domElement, type, props, rootContainerInstance);
      return shouldAutoFocusHostComponent(type, props);
    }
    
    function shouldAutoFocusHostComponent(type: string, props: Props): boolean {
      switch (type) {
        case 'button':
        case 'input':
        case 'select':
        case 'textarea':
          return !!props.autoFocus;
      }
      return false;
    }
    
    // ...
    setInitialDOMProperties(
      tag,
      domElement,
      rootContainerElement,
      props,
      isCustomComponentTag,
    );
    

    setInitialDOMProperties:

    function setInitialDOMProperties(
      tag: string,
      domElement: Element,
      rootContainerElement: Element | Document,
      nextProps: Object,
      isCustomComponentTag: boolean,
    ): void {
      for (const propKey in nextProps) {
        if (!nextProps.hasOwnProperty(propKey)) {
          continue;
        }
        const nextProp = nextProps[propKey];
        if (propKey === STYLE) {
          // 设置 style 属性
          // Relies on `updateStylesByID` not mutating `styleUpdates`.
          CSSPropertyOperations.setValueForStyles(domElement, nextProp);
        } else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
          const nextHtml = nextProp ? nextProp[HTML] : undefined;
          if (nextHtml != null) {
            setInnerHTML(domElement, nextHtml);
          }
        } else if (propKey === CHILDREN) {
          if (typeof nextProp === 'string') {
            // Avoid setting initial textContent when the text is empty. In IE11 setting
            // textContent on a <textarea> will cause the placeholder to not
            // show within the <textarea> until it has been focused and blurred again.
            // https://github.com/facebook/react/issues/6731#issuecomment-254874553
            const canSetTextContent = tag !== 'textarea' || nextProp !== '';
            if (canSetTextContent) {
              setTextContent(domElement, nextProp);
            }
          } else if (typeof nextProp === 'number') {
            setTextContent(domElement, '' + nextProp);
          }
        } else if (
          propKey === SUPPRESS_CONTENT_EDITABLE_WARNING ||
          propKey === SUPPRESS_HYDRATION_WARNING
        ) {
          // Noop
        } else if (propKey === AUTOFOCUS) {
          // We polyfill it separately on the client during commit.
          // We could have excluded it in the property list instead of
          // adding a special case here, but then it wouldn't be emitted
          // on server rendering (but we *do* want to emit it in SSR).
        } else if (registrationNameModules.hasOwnProperty(propKey)) {
          if (nextProp != null) {
            if (__DEV__ && typeof nextProp !== 'function') {
              warnForInvalidEventListener(propKey, nextProp);
            }
            ensureListeningTo(rootContainerElement, propKey);
          }
        } else if (nextProp != null) {
          DOMPropertyOperations.setValueForProperty(
            domElement,
            propKey,
            nextProp,
            isCustomComponentTag,
          );
        }
      }
    }
    

    比如设置 style 样式属性 setValueForStyles
    分为 -- 开头的自定义和常规 css 属性,就是直接操作原生 dom 上的 style。

    /**
     * Sets the value for multiple styles on a node.  If a value is specified as
     * '' (empty string), the corresponding style property will be unset.
     *
     * @param {DOMElement} node
     * @param {object} styles
     */
    export function setValueForStyles(node, styles) {
      const style = node.style;
      for (let styleName in styles) {
        if (!styles.hasOwnProperty(styleName)) {
          continue;
        }
        const isCustomProperty = styleName.indexOf('--') === 0;
        // 尝试为number自动补全px
        const styleValue = dangerousStyleValue(
          styleName,
          styles[styleName],
          isCustomProperty,
        );
        if (styleName === 'float') {
          styleName = 'cssFloat';
        }
        if (isCustomProperty) {
          style.setProperty(styleName, styleValue);
        } else {
          style[styleName] = styleValue;
        }
      }
    }
    

    值得一提的是,会为一些缺省的 number 数值会自动补全 'px' 单位(显然还有好些例外的 css 属性本身的值就是纯 number):

    function dangerousStyleValue(name, value, isCustomProperty) {
      const isEmpty = value == null || typeof value === 'boolean' || value === '';
      if (isEmpty) {
        return '';
      }
    
      if (
        !isCustomProperty &&
        typeof value === 'number' &&
        value !== 0 &&
        !(isUnitlessNumber.hasOwnProperty(name) && isUnitlessNumber[name]) // 
      ) {
        return value + 'px'; // Presumes implicit 'px' suffix for unitless numbers
      }
    
      return ('' + value).trim();
    }
    

    这份列表就是刚才说的“例外”,它们的样式值就是纯 number 类型,没有 px:

    /**
     * CSS properties which accept numbers but are not in units of "px".
     */
    export const isUnitlessNumber = {
      animationIterationCount: true,
      borderImageOutset: true,
      borderImageSlice: true,
      borderImageWidth: true,
      boxFlex: true,
      boxFlexGroup: true,
      boxOrdinalGroup: true,
      columnCount: true,
      columns: true,
      flex: true,
      flexGrow: true,
      flexPositive: true,
      flexShrink: true,
      flexNegative: true,
      flexOrder: true,
      gridArea: true,
      gridRow: true,
      gridRowEnd: true,
      gridRowSpan: true,
      gridRowStart: true,
      gridColumn: true,
      gridColumnEnd: true,
      gridColumnSpan: true,
      gridColumnStart: true,
      fontWeight: true,
      lineClamp: true,
      lineHeight: true,
      opacity: true,
      order: true,
      orphans: true,
      tabSize: true,
      widows: true,
      zIndex: true,
      zoom: true,
    
      // SVG-related properties
      fillOpacity: true,
      floodOpacity: true,
      stopOpacity: true,
      strokeDasharray: true,
      strokeDashoffset: true,
      strokeMiterlimit: true,
      strokeOpacity: true,
      strokeWidth: true,
    };
    

    **
    更新时(进行 diff 判断)

    对于 input、select、textarea 就算 props 没变,也要 markUPdate(workInProgress) 标记后续需要更新

    for 循环遍历 lastProps 的 propKey,如果本次不是删除该 props,则继续遍历,否则进行后面的删除操作。
    通过 prop 来判断相关 dom attribute 是否发生变化:

    • 删除 style 相关属性:把 dom style 相关 css 属性设置为 '' 空字符串即可。
    • 其他 props 的若要删除:设置为 null。
    • 对于 children,只检查 string 或者 number 类型的 props 值是否变化,从而能否跳过更新。其他则一律更新。
    • 经过以上判断对比,可能成功地跳过了部分不必要的 props 的更新。而剩余的所有情况,则一律更新:

    updatePayload.push(propKey, nextProp);

    • 然后返回 updatePalyload 数组,通过 markUpdate(workInprogress) 进行标记。
    updateHostComponent(
      current,
      workInProgress,
      type,
      newProps,
      rootContainerInstance,
    );
    
    if (current.ref !== workInProgress.ref) {
      markRef(workInProgress);
    }
    

    updateHostComponent:

    • 如果当前 fiber 节点新老 props 没变,则跳过更新。TODO 未完待续,涉及 dom diff or props diff?
      updateHostComponent = function(
        current: Fiber,
        workInProgress: Fiber,
        type: Type,
        newProps: Props,
        rootContainerInstance: Container,
      ) {
        const oldProps = current.memoizedProps;
        // 跳过dom更新
        if (oldProps === newProps) {
          // In mutation mode, this is sufficient for a bailout because
          // we won't touch this node even if children changed.
          return;
        }
    
        const instance: Instance = workInProgress.stateNode; // 拿到dom实例
        const currentHostContext = getHostContext();
        const updatePayload = prepareUpdate(
          instance,
          type,
          oldProps,
          newProps,
          rootContainerInstance,
          currentHostContext,
        );
        // TODO: Type this specific to this type of component.
        workInProgress.updateQueue = (updatePayload: any);
        // If the update payload indicates that there is a change or if there
        // is a new ref we mark this as an update. All the work is done in commitWork.
        if (updatePayload) {
          markUpdate(workInProgress);
        }
      };
    

    5. HostText

    HostText 中涉及到 updateHostText,因为是 dom text 节点,所以只需对比新老 text 字符串。

    首次渲染
    调用 createTextInstance,其中调用 createTextNode,里面其实就是在调用原生的 document.createTextNode(text),另外想要找到 document 节点,只需在任意原生 dom 节点上查询 ownerDocument 属性即可。

    precacheFiberNode HostComponent 讲过了?我怎么没印象。

    6. renderRoot 中的错误处理

    • 给报错节点增加 Incomplete 副作用
    • 给父链上具有 error boundary 的节点增加副作用,收集错误以及进行一定的处理。
    • 创建错误相关的更新。

    首先,在 renderRoot 中:

    do {
        try {
          workLoop(isYieldy);
        } catch (thrownValue) {
          if (nextUnitOfWork === null) {
            // This is a fatal error.
            didFatal = true;
            onUncaughtError(thrownValue);
          } else {
            
            const failedUnitOfWork: Fiber = nextUnitOfWork;
            if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
              replayUnitOfWork(failedUnitOfWork, thrownValue, isYieldy);
            }
    
            const sourceFiber: Fiber = nextUnitOfWork;
            let returnFiber = sourceFiber.return;
            if (returnFiber === null) {
              // This is the root. The root could capture its own errors. However,
              // we don't know if it errors before or after we pushed the host
              // context. This information is needed to avoid a stack mismatch.
              // Because we're not sure, treat this as a fatal error. We could track
              // which phase it fails in, but doesn't seem worth it. At least
              // for now.
              didFatal = true;
              onUncaughtError(thrownValue);
            } else {
              throwException(
                root,
                returnFiber,
                sourceFiber,
                thrownValue,
                nextRenderExpirationTime,
              );
              nextUnitOfWork = completeUnitOfWork(sourceFiber);
              continue;
            }
          }
        }
        break;
      } while (true);
    

    onUncaughtError:
    **
    对于致命错误,会使用 onUncaughtError 来处理:

    function onUncaughtError(error: mixed) {
      invariant(
        nextFlushedRoot !== null,
        'Should be working on a root. This error is likely caused by a bug in ' +
          'React. Please file an issue.',
      );
      // Unschedule this root so we don't work on it again until there's
      // another update.
      nextFlushedRoot.expirationTime = NoWork;
      if (!hasUnhandledError) {
        hasUnhandledError = true;
        unhandledError = error;
      }
    }
    

    throwException:
    **
    否则使用 throwException 对捕获到的错误进行处理:

    • 先尝试处理 Suspense 中的异常。
    • 否则向上寻找可以错误处理的 ClassComponent 组件(比如使用了 getDerivedStateFromErrorcomponentDidCatch )。为该 ClassComponent 所在节点增加 ShouldCapture 副作用标记。 然后创建错误 update 并入队列,以便在 commit 阶段被调用。
    • 如果向上没能找到能处理错误的 class component,则最终会在 HostRoot 上进行类似操作。
    function throwException(
      root: FiberRoot,
      returnFiber: Fiber,
      sourceFiber: Fiber,
      value: mixed,
      renderExpirationTime: ExpirationTime,
    ) {
      // The source fiber did not complete.
      sourceFiber.effectTag |= Incomplete;
      // Its effect list is no longer valid.
      sourceFiber.firstEffect = sourceFiber.lastEffect = null;
    
      if (
        value !== null &&
        typeof value === 'object' &&
        typeof value.then === 'function'
      ) {
      // 处理 Suspense 相关合理的中间状态带来的异常
      }
      
      // We didn't find a boundary that could handle this type of exception. Start
      // over and traverse parent path again, this time treating the exception
      // as an error.
      renderDidError();
      value = createCapturedValue(value, sourceFiber);
      let workInProgress = returnFiber;
      do {
        switch (workInProgress.tag) {
          case HostRoot: {
            const errorInfo = value;
            workInProgress.effectTag |= ShouldCapture;
            workInProgress.expirationTime = renderExpirationTime;
            const update = createRootErrorUpdate(
              workInProgress,
              errorInfo,
              renderExpirationTime,
            );
            enqueueCapturedUpdate(workInProgress, update);
            return;
          }
          case ClassComponent:
            // Capture and retry
            const errorInfo = value;
            const ctor = workInProgress.type;
            const instance = workInProgress.stateNode;
            if (
              (workInProgress.effectTag & DidCapture) === NoEffect &&
              (typeof ctor.getDerivedStateFromError === 'function' ||
                (instance !== null &&
                  typeof instance.componentDidCatch === 'function' &&
                  !isAlreadyFailedLegacyErrorBoundary(instance)))
            ) {
              workInProgress.effectTag |= ShouldCapture;
              workInProgress.expirationTime = renderExpirationTime;
              // Schedule the error boundary to re-render using updated state
              const update = createClassErrorUpdate(
                workInProgress,
                errorInfo,
                renderExpirationTime,
              );
              enqueueCapturedUpdate(workInProgress, update);
              return;
            }
            break;
          default:
            break;
        }
        workInProgress = workInProgress.return;
      } while (workInProgress !== null);
    }
    

    ** nextUnitOfWork = completeUnitOfWork(sourceFiber) :**
    **
    如果某个节点产生了错误,那么处理完错误相关的之后,会调用 completeUnitOfwork 做收尾工作,向上遍历。不会再往下遍历渲染子节点,因为已经报错了,向下的局部子树没意义了。

    7. unwindWork

    • 类似于 completeWork 对不同类型组件进行处理(其整个过程是跟 completeWork 区分开的)。
    • 对于 ShouldCapture 组件设置 DidCapture 副作用

    unwindWork 和 completeWork 最大的区别是前者会判断有 ShouldCapture ,然后取消设置,并新增 DidCapture ,然后 return。

    如果一个节点报错了,对于父组件 completeUnitOfWork 时会全部走 unwindWork 而不是 completeWork。
    会全部从跟组件重走一遍更新过程,即 workLoop 那一套向下遍历。

    TODO 待深究。

    相关文章

      网友评论

          本文标题:React源码06 - 完成节点任务

          本文链接:https://www.haomeiwen.com/subject/woxarktx.html