关于前端:React-源码解析系列-React-的-render-阶段二beginWork

1次阅读

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

上面来介绍 React Render 的“递”阶段 —— beginWork,在《React 源码解析系列 – React 的 render 阶段(一):根本流程介绍》中咱们可知 beginWork 的次要作用是创立本次循环 (performUnitOfWork) 主体 (unitOfWork) 的子 Fiber 节点,其流程如下:

从上图可知,beginWork 的工作门路有四条:

  • mount(首屏渲染)时创立新的子 Fiber 节点,并返回该新建节点;
  • update 时若不满足复用条件,则与 mount 时一样创立新的子 Fiber 节点,并 diff 出相应的 effectTag 挂在子 Fiber 节点上,并返回该新建节点;
  • update 时若满足复用条件,则复用 current 树上对应的子 Fiber 节点(current.child),返回复用后的节点
  • update 时若满足复用条件,则复用 current 树上对应的子 Fiber 节点(current.child),间接返回 null 值;

演绎一下:

  • 前两者是次要的工作门路;
  • 第三条工作门路 ——“复用节点”实际上在第二条工作门路 —— reconcileChildFibers(update) 时也会有相似的实现,或者说是 不同档次 的“复用节点”;
  • 而第四条工作门路 ——“间接返回 null 值”这就是属于“深度遍历”过程中,名为“剪枝”的优化策略,能够缩小不必要的渲染,进步性能。

beginWork 的入参

function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {// ... 省略函数体}

beginWork 有 3 个参数,但目前咱们只关注前两个:

  • current:与本次循环主体 (unitOfWork) 对应的 current 树上的节点,即 workInProgress.alternate。
  • workInProgress:本次循环主体(unitOfWork),也即待处理的 Fiber 节点。

判断是 mount 还是 update

从 beginWork 的流程图中可知,第一个流程分支是判断以后为 mount(首屏渲染) 还是 update;其判断的根据是:入参 current 是否为null,这是因为 mount(首屏渲染) 时,FiberRootNode 的 current 指针指向null,后续还有很多中央都须要依据这个判断来做不同的解决。

次要工作门路

switch (workInProgress.tag) {
  case IndeterminateComponent: 
    // ... 省略
  case LazyComponent: 
    // ... 省略
  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,
        renderLanes,
      );
    }
  case ClassComponent: 
    // ... 省略
  case HostRoot:
    // ... 省略
  case HostComponent:
    // ... 省略
  case HostText:
    // ... 省略
  // ... 省略其余类型
}

mount(首屏渲染) 时会依据不同的 workInProgress.tag(组件类型)来进入到不同的子节点创立逻辑,咱们关注最常见的组件类型:FunctionComponent(函数组件) / ClassComponent(类组件) / HostComponent(对标 HTML 标签),最终这些逻辑都会进入 reconcileChildren 办法。

reconcileChildren

上面来看看 reconcileChildren 办法:

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {if (current === null) {
    // 对于 mount 的组件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 对于 update 的组件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

从函数名 —— reconcileChildren 就能看出这是 Reconciler 模块的外围局部;这里咱们看到会依据 mount(首屏渲染)还是 update 来走不同的办法 —— mountChildFibers | reconcileChildFibers,但不管走哪个逻辑,最终都会生成新的子 Fiber 节点并赋值给 workInProgress.child,并作为下次循环 (performUnitOfWork) 执行时的循环主体 (unitOfWork);
上面咱们来看看这两个办法是什么。

export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

从下面代码能够看出,mount 时执行的 reconcileChildFibers 和 update 时执行的 mountChildFibers 形式,实际上都是由 ChildReconciler 这个办法封装进去的,差异只在于传参不同。

ChildReconciler

上面来看 ChildReconciler:

// shouldTrackSideEffects 示意是否追踪副作用
function ChildReconciler(shouldTrackSideEffects) {
    /* 外部函数汇合 */
    function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {if (!shouldTrackSideEffects) { // 如不须要追踪副作用则间接返回
            // Noop.
            return;
        }
        /* 在以后节点 (returnFiber) 上标记删除指标节点 */
        const deletions = returnFiber.deletions;
        if (deletions === null) {returnFiber.deletions = [childToDelete]; // 退出“待删除子节点”的数组中
            returnFiber.flags |= ChildDeletion; // 标记以后节点须要删除子节点
        } else {deletions.push(childToDelete);
        }
    }
    function placeSingleChild(newFiber: Fiber): Fiber {
        /* 标记用新节点去代替原来的节点(如果有“原来的节点”的话)*/
        if (shouldTrackSideEffects && newFiber.alternate === null) {newFiber.flags |= Placement;}
        return newFiber;
    }
    
    // ... 还有其它很多外部函数

    /* 主流程 */
    function reconcileChildFibers(
        returnFiber: Fiber,
        currentFirstChild: Fiber | null,
        newChild: any,
        lanes: Lanes,
    ): Fiber | null { }

    return reconcileChildFibers; // 返回主办法,其中曾经通过闭包分割上一堆外部办法了
}

从下面的代码咱们能够看出 ChildReconciler 实际上是通过闭包封装了一堆外部函数,其次要流程实际上就是 reconcileChildFibers 这个办法,而在 reconcileChildren 办法中的调用也正是调用的这个 reconcileChildFibers 办法;咱们解读一下该办法的入参:

  • returnFiber:以后 Fiber 节点,即 workInProgress
  • currentFirstChild:current 树上对应的以后 Fiber 节点的第一个子 Fiber 节点,mount 时为 null
  • newChild:子节点(ReactElement)
  • lanes:优先级相干

而后咱们回过头来看这 ChildReconciler 办法的入参 —— shouldTrackSideEffects,这个参数的字面意思是“是否须要追踪副作用”,所谓的“副作用”,指的就是是否须要做 DOM 操作,需要的话就会在以后 Fiber 节点中打上 EffectTag,即“追踪”副作用;而也仅有在 update 的时候,才须要“追踪副作用”,即把 current 这个 Fiber 节点与本次更新组件状态后的 ReactElement 做比照(diff),而后得出本次更新的 Fiber 节点,以及在该节点上打上 diff 的后果 —— EffectTag。

子节点(ReactElement)

这里须要开展阐明一下 子节点 (ReactElement) 是怎么来的:

  • 针对组件中的 jsx 代码,babel 会在编译阶段将其转换成一个 React.createElement() 调用的代码段。
  • 如果是类组件,则执行其 render 成员办法,并失去 React.createElement() 执行的后果 —— 一个 ReactElement 对象。
  • 如果是函数组件,则间接执行,同样失去一个 ReactElement 对象。
  • 如果是 HostComponent,即个别的 HTML,同样也是取得一个 ReactElement 对象。
  • React.createElement 的源代码请看这里。

reconcileChildFibers

在 reconcileChildFibers 办法中,首先会判断 newChild 的类型,来进入到不同逻辑中。

次要有这些类型:

  • ReactElement
  • Portal
  • React.Lazy 包裹后的元素
  • 数组
  • 纯文本(包含 number 和 string)
    function reconcileChildFibers(
        returnFiber: Fiber,
        currentFirstChild: Fiber | null,
        newChild: any,
        lanes: Lanes,
    ): Fiber | null {if (typeof newChild === 'object' && newChild !== null) {switch (newChild.$$typeof) { // 依据 $$typeof 属性来进一步辨别类型
                case REACT_ELEMENT_TYPE:
                    return placeSingleChild(
                        reconcileSingleElement(
                            returnFiber,
                            currentFirstChild,
                            newChild,
                            lanes,
                        ),
                    );
                case REACT_PORTAL_TYPE:
                    // 省略
                case REACT_LAZY_TYPE:
                    // 省略
            }
            /* 解决子节点是一个数组的状况 */
            if (isArray(newChild)) {
                return reconcileChildrenArray(
                    returnFiber,
                    currentFirstChild,
                    newChild,
                    lanes,
                );
            }

            // 省略
        }
        /* 解决纯文本 */
        if (typeof newChild === 'string' || typeof newChild === 'number') {
            return placeSingleChild(
                reconcileSingleTextNode(
                    returnFiber,
                    currentFirstChild,
                    '' + newChild,
                    lanes,
                ),
            );
        }

        // 省略
    }

$$typeof

从下面的代码中,咱们看到除了间接用 newChild 的数据类型来判断走哪个代码分支外,还用了 newChild.$$typeof` 来判断,这个 `$$typeof 就是以后 ReactElement 的类型,它的值是一个 Symbol 值,并且是曾经事后 定义 好的,咱们能够看到在 ReactElement 的工厂函数中,曾经对 $$typeof 复制为 REACT_ELEMENT_TYPE 了。

为什么须要有这 $$typeof` 属性呢?是因为须要避免 **XSS 攻打 **:当利用容许存储并回显一个 JSON 对象时,歹意用户可构建一个 ** 伪 ReactElement 对象 **,形如上面的例子,如果 React 不加分辨,则会间接将该伪 ReactElement 对象渲染到 DOM 树上。因而从 React 0.14 版本后,React 会为每个真正的 ReactElement 增加 `$$typeof 属性,只有领有该属性的 ReactElement 对象才会被 React 渲染;而因为该属性为 Symbol 类型,无奈应用 JSON 来结构,因而便能堵住这一破绽。

/* 歹意的 json 对象 */
var xssJsonObject = {
  type: 'div',
  props: {
    dangerouslySetInnerHTML: {__html: '/* 歹意脚本 */'},
  },
  // ...
};

reconcileSingleElement

接着,咱们以 ReactElement 类型的解决逻辑为示例持续往下走,会调用 reconcileSingleElement 办法。

尝试复用 current 树上对应的子 Fiber 节点

在该办法中,首先会有这么一个 while 循环:

const key = element.key;
let child = currentFirstChild;
while (child !== null) {if (child.key === key) {
    const elementType = element.type;
    if (elementType === REACT_FRAGMENT_TYPE) {if (child.tag === Fragment) {deleteRemainingChildren(returnFiber, child.sibling); // 删除掉该 child 节点的所有 sibling 节点
          const existing = useFiber(child, element.props.children); // 复用 child 节点
          existing.return = returnFiber; // 重置新 Fiber 节点的 return 指针,指向以后 Fiber 节点
          return existing;
        }
    } else {if (child.elementType === elementType) {deleteRemainingChildren(returnFiber, child.sibling); // 删除掉该 child 节点的所有 sibling 节点
            const existing = useFiber(child, element.props); // 复用 child 节点
            existing.ref = coerceRef(returnFiber, child, element); // 解决 ref
            existing.return = returnFiber; // 重置新 Fiber 节点的 return 指针,指向以后 Fiber 节点
            return existing;
        }

        // Didn't match.
        deleteRemainingChildren(returnFiber, child);
        break;
  } else {deleteChild(returnFiber, child); // 在 returnFiber 标记删除该子节点
  }
  child = child.sibling; // 指针指向 current 树中的下一个节点
}

下面这段代码的作用是找出上次更新中,current 树对应 Fiber 节点中所有不可复用的子节点,并在 以后 Fiber 节点 (returnFiber) 中标记须要删除的 effectTag;判断的规范大抵是:

  • 若某个 current Fiber 子节点的 key 属性与本次渲染中的 child.key 不统一,则标记删除
  • 在 key 属性雷同的前提下:若某个 current Fiber 子节点与本次渲染中的 child 均为 Fragment,或是它们的 elementType 属性统一,那么则执行复用。

复用的流程根本如下:

  1. deleteRemainingChildren(returnFiber, child.sibling),这是因为走到 reconcileSingleElement 这个办法中意味着以后解决节点只有一个子节点,因而找到可复用的子节点后,能够标记删除掉剩下的 (sibling) 子节点。
  2. const existing = useFiber(child, element.props);,调用 useFiber 办法来复用子 Fiber 节点。
  3. existing.return = returnFiber;,建设子 Fiber 节点 (existing) 与以后 Fiber 节点 (returnFiber) 的父子关系 (return 属性)。

无奈复用,创立新的 Fiber 子节点

如果没有可复用的子节点的话,会进入创立新的子节点的逻辑:

if (element.type === REACT_FRAGMENT_TYPE) {// ... 创立 Fragment 类型的子节点,疏忽} else {const created = createFiberFromElement(element, returnFiber.mode, lanes); // 依据以后子节点的 ReactElement 来创立新的 Fiber 节点
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    created.return = returnFiber;
    return created;
}

下具体介绍 如何复用子节点 以及 如何创立一个全新的子节点

复用子节点 —— useFiber

复用子节点所调用的是 useFiber 办法,咱们回顾下是怎么调用这个办法的:const existing = useFiber(child, element.props);

这里的 child 指的是确定能够复用的 current 树子 Fiber 节点,而 element.props 则是本次更新时 ReactElement 取得的 props 值(该值也被称为 pendingProps)。

而后咱们再看 useFiber 这个办法自身:

function useFiber(fiber: Fiber, pendingProps: mixed): Fiber {const clone = createWorkInProgress(fiber, pendingProps);
    clone.index = 0; // 重置一下:以后子节点必然为第一个子节点
    clone.sibling = null; // 重置一下:以后子节点没有 sibling
    return clone;
}

能够看出这个办法次要就是调用了 createWorkInProgress 办法。

createWorkInProgress

咱们接下来看看 createWorkInProgress 办法干了什么:

export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
  /*
    如果 current.alternate 为空(这里先不要了解成是 workInProgress),则复用 current 节点,再依据本次更新的 props 来 new 一个 FiberNode 对象
  */
  if (workInProgress === null) {// createFiber 是 Fiber 节点 (FiberNode) 的工厂办法
    workInProgress = createFiber(
      current.tag,
      pendingProps,
      current.key,
      current.mode, // mode 属性示意渲染模式,一个二进制值
    );
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode; // DOM 节点

    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {// 如果 current.alternate 不为空,则重置 workInProgress 的 pendingProps/type/effectTag 等属性}
  // 复制 current 的子节点、上次更新时的 props 和 state
  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;

  // 复制 current 的指针
  workInProgress.sibling = current.sibling;
  workInProgress.index = current.index;
  workInProgress.ref = current.ref;

  return workInProgress;
}

这里须要关注的重点是:

  • 如果 current.alternate 不为空,那此时 current.alternate 应该是上上次更新时的树节点,咱们能够留意到这种场景下,并没有创立新的 Fiber 节点,而是间接复用了这个 current.alternate 节点(只是对它的一些属性进行重置),这就能够看出“双缓存”的实质,并非是“每创立一棵新的 Fiber 树就把上上次更新时的 Fiber 树摈弃掉”,而是”在创立本次更新的 Fiber 树时,尽量复用上上次更新时的 Fiber 树,保障任一时刻最多只有两棵 Fiber 树”;而所谓的 current 和 workInProgress,其实都是绝对的,只是取决于此时的 FiberRootNode 的 current 属性指向哪棵 Fiber 树而已。
  • FiberNode 上的 node 属性示意渲染模式,是一个二进制值,具体定义在这里。

创立全新子节点 —— createFiberFromElement

创立全新子节点所调用的办法是 createFiberFromElement

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;
}

能够看出,createFiberFromElement 办法次要就是执行了 createFiberFromTypeAndProps 这个办法,而该办法次要是解析确定下新节点的 tag、type 属性,并调用 createFiber 办法 new 了一个新节点对象。

reconcileChildrenArray

当一个节点有多个子节点(如:<div><span>2</span>3</div>),那么此时 newChild 就是一个数组,此时便会进入到 reconcileChildrenArray 的办法中

回顾下在 reconcileChildFibers 办法中是如何调用该办法的:

if (isArray(newChild)) {
    return reconcileChildrenArray(
        returnFiber, // 以后的 Fiber 节点
        currentFirstChild, // current 树中对应的子 Fiber 节点
        newChild, // 本次更新的子 ReactElement
        lanes, // 优先级相干
    );
}

与 reconcileSingleElement 办法相似,reconcileChildrenArray 实际上也是尝试复用 current 树上的对应子节点,如遇到无奈复用的子节点,则创立新节点;但不同点在于,reconcileChildrenArray 须要解决的子节点实际上是一个数组,因而须要进行新数组(本次更新中创立的 ReactElement)与原数组(current 树上对应的子 Fiber 节点)间的比照,其大略思路如下:

  1. 依据 index 遍历新老数组元素,一一比照新老数组,比照的根据是 key 属性是否雷同;
  2. 若 key 属性雷同,则复用节点并持续进行遍历,直到遇到不能复用的状况(或老数组中的所有节点都曾经被复用)则完结遍历。
  3. 如果老数组所有节点都曾经被复用,但新数组尚有未解决的局部,则根据新数组该未解决局部来创立新的 Fiber 节点。
  4. 如果老数组有节点尚未被遍历(即在第一次遍历中碰到不能复用的状况而中途退出),那么将这部分放进一个 map 里,而后持续遍历新数组,看看有没有能从 map 里找到能复用的;若能复用的,则进行复用,否则创立新 Fiber 节点;对于未被复用的旧节点,则全副标记删除(deleteChild)。

须要留神的是,尽管 reconcileChildrenArray 把整个数组 (newChild) 的 Fiber 节点都创立进去了,但其最终 return 的实际上是数组中的第一个 Fiber 节点,换句话说:在下次 performUnitOfWork 中的循环主体 (unitOfWork) 实际上是这个数组中的第一个 Fiber 节点;而当这“第一个 Fiber 节点”执行到 completeWork 阶段时,会取出它的 sibling —— 也就是这个数组中的第二个 Fiber 节点来作为下次 performUnitOfWork 中的循环主体(unitOfWork)。

优化的工作门路

上文花了十分多的篇幅来一路深刻介绍 beginWork 的次要工作门路,上面咱们还是回到 beginWork 处:

if (current !== null) {
    const oldProps = current.memoizedProps
    const newProps = workInProgress.pendingProps

    if (
        oldProps !== newProps ||
        hasLegacyContextChanged() // 判断 context 是否有变动) {
        /* 该 didReceiveUpdate 变量代表本次更新中本 Fiber 节点是否有变动 */
        didReceiveUpdate = true
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
        didReceiveUpdate = false
        switch (workInProgress.tag) {// 省略}
        return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes)
    } else {didReceiveUpdate = false}
} else {didReceiveUpdate = false}

以后代码段的作用是判断以后 Fiber 节点是否有变动,其判断的根据是: props 和 fiber.type(如函数组件的函数、类组件的类、html 标签等)和 context 没有变动 ;并且在 Fiber 节点没有变动的前提下(!includesSomeLane(renderLanes, updateLanes) 波及优先级暂不探讨),尝试一成不变地复用子 Fiber 节点或是间接“剪枝”:bailoutOnAlreadyFinishedWork 办法。

bailoutOnAlreadyFinishedWork

接下来咱们来看 bailoutOnAlreadyFinishedWork 办法:

function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // 省略

  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) { // 判断子节点中是否须要查看更新
    return null; // 剪枝:不须要关注子节点 (ReactElement) 了
  } else {cloneChildFibers(current, workInProgress);
    return workInProgress.child;
  }
}
  • !includesSomeLane(renderLanes, workInProgress.childLanes) === true 时,会间接 return null,这就是上文说到的“剪枝”策略:不再关注其下的子节点,转到本节点的 completeWork 阶段。
  • 不满足上述条件时,则 克隆 current 树上对应的子 Fiber 节点 并返回,作为下次 performUnitOfWork 的主体。

克隆 current 树上对应的子 Fiber 节点 —— cloneChildFibers

这里的“克隆 current 树上对应的子 Fiber 节点”可能会造成一些蛊惑,咱们间接看 cloneChildFibers 代码:

export function cloneChildFibers(
  current: Fiber | null,
  workInProgress: Fiber,
): void {
  // 省略
  /* 判断子节点为空,则间接返回 */
  if (workInProgress.child === null) {return;}

  let currentChild = workInProgress.child; // 这里怎么会是拿 workInProgress.child 来充当 currentChild 呢?解释看下文
  let newChild = createWorkInProgress(currentChild, currentChild.pendingProps); // 复用 currentChild
  workInProgress.child = newChild;

  newChild.return = workInProgress; // 让子 Fiber 节点与以后 Fiber 节点建立联系
  /* 遍历子节点的所有兄弟节点并进行节点复用 */
  while (currentChild.sibling !== null) {
    currentChild = currentChild.sibling;
    newChild = newChild.sibling = createWorkInProgress(
      currentChild,
      currentChild.pendingProps,
    );
    newChild.return = workInProgress;
  }
  newChild.sibling = null;
}

这里咱们看到明明是拿 workInProgress.child 去创立子节点的,怎么会说成是克隆 current 树上对应的子 Fiber 节点 呢?而且按理说此时还没创立子 Fiber 节点,workInProgress.child 怎么会有值呢?

其实是这样的,以后节点是在父节点的 beginWork 阶段通过 createWorkInProgress 办法创立进去的,会执行 workInProgress.child = current.child,因而在本节点创立本人的子节点并笼罩 workInProgress.child 之前,workInProgress.child 其实指向的就是 current.child

上面以图例阐明:

EffectTag

上文说到,在 update 的场景下,除了与 mount 时一样创立子 Fiber 节点外,还会与上次渲染的子节点进行 diff,从而得出须要进行什么样的 DOM 操作,并将其“标记”在新建的子 Fiber 节点上,上面就来介绍一下这个“标记”—— EffectTag

EffectTag 是 Fiber Reconciler 绝对于 Stack Reconciler 的一大变革,以往 Stack Reconciler 是每 diff 出一个节点就进行 commit 的(当然,因为 Stack Reconciler 是同步执行的,因而直到所有节点都 commit 完了才会轮到浏览器 GUI 线程进行渲染,这样就不会造成“仅局部更新”的问题),而 Fiber Recconciler 则在 diff 进去后,仅在指标节点打上 effectTag,而不会走到 commit 阶段,待所有节点都实现 render 阶段后才对立进 commit 阶段,这样便实现了 reconciler(render 阶段)renderer(commit 阶段) 的解耦。

EffectTag 类型的定义

effectTag 实际上就是须要对节点须要执行的 DOM 操作(也可认为是副作用,即 sideEffect),定义有以下这些类型(仅节选局部 EffectTag 类型):

// DOM 须要插入到页面中
export const Placement = /*                */ 0b00000000000010;
// DOM 须要更新
export const Update = /*                   */ 0b00000000000100;
// DOM 须要插入到页面中并更新
export const PlacementAndUpdate = /*       */ 0b00000000000110;
// DOM 须要删除
export const Deletion = /*                 */ 0b00000000001000;

为什么须要应用二进制来示意 effectTag 呢?

这是因为同一个 Fiber 节点,可能须要执行多种类型的 DOM 操作,即须要打上多种类型的 effectTag,那么这时候只有将这些 effectTag 做“按位或”(|)运算,那么就能够汇总成以后 Fiber 节点领有的所有 effectTag 类型了。

若要判断某个 Fiber 节点是否有某种类型的 effectTag,其实也很简略,拿 fiber.effectTag 跟这个类型的 effectTag 所对应的二进制值来做“按位与”(&)运算,再依据运算后果是否为 NoEffect(0) 即可。

renderer 依据 EffectTag 来执行 DOM 操作

以 renderer“判断以后节点是否须要进行插入 DOM 操作”为例:

  • fiber.stateNode 存在,即 Fiber 节点中保留了对应的 DOM 节点
  • (fiber.effectTag & Placement) !== 0,即 Fiber 节点存在 Placement effectTag。

以上对于 update 操作都很好了解,但 mount 时在 reconcileChildren 中调用的 mountChildFibers 的要怎么办呢?

mount 时的 fiber.stateNode 为 null,那不就不会执行插入 DOM 操作?

fiber.stateNode 会在节点的“归”阶段,即 completeWork 中进行创立。

mount 时每个节点上都会有 Placement EffectTag

假如 mountChildFibers 也会赋值 effectTag,那么能够预感 mount 时整棵 Fiber 树所有节点都会有 Placement effectTag。那么 commit 阶段在执行 DOM 操作时每个节点都会执行一次插入操作,这样大量的 DOM 操作是极低效的。

为了解决这个问题,在 mount 时只有 rootFiber 会赋值 Placement effectTag,在 commit 阶段只会执行一次插入操作。

正文完
 0