React16源码: React中的reconcileChildren的源码实现

reconcileChildren

1 )概述

  • 在更新了一个节点之后,拿到它的props.children
  • 要根据这个children里面的 ReactElement 来去创建子树的所有的 fiber 对象
  • 要根据 props.children 来生成 fiber 子树,然后判断 fiber 对象它是否是可以复用的
    • 因为我们在第一次渲染的时候,就已经渲染了整个 fiber 子树
    • 再有一个更新进来之后,state 变化可能会导致一些子节点产生一个新的变化
    • 可能就不能复用之前的 fiber 节点了,它里面的很多东西都变得不一样
    • 大部分情况下所有 fiber 节点都是可以可以重复利用的
    • 这个时候我们根据什么进行判断,是这里面的一个非常重要的一个点
  • 在这里就会拿出react当中的非常重要的key,列表根据key来优化

2 )源码

packages/react-reconciler/src/ReactFiberBeginWork.js#L137

js 复制代码
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderExpirationTime: ExpirationTime,
) {
  if (current === null) {
    // If this is a fresh new component that hasn't been rendered yet, we
    // won't update its child set by applying minimal side-effects. Instead,
    // we will add them all to the child before it gets rendered. That means
    // we can optimize this reconciliation pass by not tracking side-effects.
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderExpirationTime,
    );
  } else {
    // If the current child is the same as the work in progress, it means that
    // we haven't yet started any work on these children. Therefore, we use
    // the clone algorithm to create a copy of all the current children.

    // If we had any progressed work already, that is invalid at this point so
    // let's throw it out.
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderExpirationTime,
    );
  }
}
  • 可见,根据 current 是否为 null 调用两个方法: mountChildFibers, reconcileChildFibers

    • 两个方法的区别,在第二个参数
    • 在前者方法中传递的是 null, 因为 第一次渲染,不存在 current.child
    • 因为第一次渲染的时候,都是父亲节点先渲染,子节点后渲染
    • 只有在后续的渲染过程当中,已经过第一次渲染了我们的父节点
    • 这个时候才有一个子节点存在,所以这是一个区别
    • 在 react-reconciler/src/ReactFiberBeginWork.js 中 的 reconcileChildren 调用这个 reconcileChildFibers
  • 看一下这两个方法,它们来自于 ReactChildFiber.js

    js 复制代码
    export const reconcileChildFibers = ChildReconciler(true);
    export const mountChildFibers = ChildReconciler(false);
  • 可见,上述两个方法的区别是 参数的 true 和 false

    • 这个参数的意思是: 表示是否要跟踪副作用
  • 进入 ChildReconciler,这个方法 1k 多行,这里不去全部摘抄

  • 用到哪里,逐个分析

整体框架

js 复制代码
// 参数 shouldTrackSideEffects 表示是否要跟踪副作用
function ChildReconciler(shouldTrackSideEffects) {
  // ... 跳过很多代码

  // This API will tag the children with the side-effect of the reconciliation
  // itself. They will be added to the side-effect list as we pass through the
  // children and the parent.
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    expirationTime: ExpirationTime,
  ): Fiber | null {
    // 省略很多代码
    // Remaining cases are all treated as empty.
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

  return reconcileChildFibers;
}
  • ChildReconciler 方法的最后, return reconcileChildFibers;,先看下这个方法

    js 复制代码
    // This API will tag the children with the side-effect of the reconciliation
    // itself. They will be added to the side-effect list as we pass through the
    // children and the parent.
    function reconcileChildFibers(
      returnFiber: Fiber,
      currentFirstChild: Fiber | null,
      newChild: any,
      expirationTime: ExpirationTime,
    ): Fiber | null {
      // This function is not recursive.
      // If the top level item is an array, we treat it as a set of children,
      // not as a fragment. Nested arrays on the other hand will be treated as
      // fragment nodes. Recursion happens at the normal flow.
    
      // Handle top level unkeyed fragments as if they were arrays.
      // This leads to an ambiguity between <>{[...]}</> and <>...</>.
      // We treat the ambiguous cases above the same.
    
      // newChild 是计算出来的 新children
      // 这个节点是一个 top level 的节点,因为 <></> 这种类型会匹配 REACT_FRAGMENT_TYPE
      // 符合以下就是没有key的 topLevel节点
      const isUnkeyedTopLevelFragment =
        typeof newChild === 'object' &&
        newChild !== null &&
        newChild.type === REACT_FRAGMENT_TYPE &&
        newChild.key === null;
      // 如果匹配了,那么 Fragment 是没有任何的操作更新的, 本身并没有什么实际的意义
      // 如果匹配了,则将newChild赋值
      if (isUnkeyedTopLevelFragment) {
        newChild = newChild.props.children;
      }
    
      // Handle object types
      const isObject = typeof newChild === 'object' && newChild !== null;
    
      // 如果是对象,说明有下面两种情况
      // 下面两种方法调用相似,都是基于 placeSingleChild 传递不同参数
      // 一个是 reconcileSingleElement
      // 另一个是 reconcileSinglePortal
      if (isObject) {
        switch (newChild.$$typeof) {
          // 匹配 REACT_ELEMENT,是通过 React.createElement 产生的
          case REACT_ELEMENT_TYPE:
            return placeSingleChild(
              reconcileSingleElement(
                returnFiber,
                currentFirstChild,
                newChild,
                expirationTime,
              ),
            );
          // 匹配 REACT_PORTAL, 是通过 ReactDOM.createPortal
          case REACT_PORTAL_TYPE:
            return placeSingleChild(
              reconcileSinglePortal(
                returnFiber,
                currentFirstChild,
                newChild,
                expirationTime,
              ),
            );
        }
      }
    
      // 匹配到 string 和 number 类就是 text类型的Node
      if (typeof newChild === 'string' || typeof newChild === 'number') {
        return placeSingleChild(
          reconcileSingleTextNode(
            returnFiber,
            currentFirstChild,
            '' + newChild,
            expirationTime,
          ),
        );
      }
    
      // 匹配到数组
      if (isArray(newChild)) {
        return reconcileChildrenArray(
          returnFiber,
          currentFirstChild,
          newChild,
          expirationTime,
        );
      }
      
      // 匹配到可迭代的对象
      if (getIteratorFn(newChild)) {
        return reconcileChildrenIterator(
          returnFiber,
          currentFirstChild,
          newChild,
          expirationTime,
        );
      }
    
      // 以上都没有匹配,但仍然是对象,则 throw error
      if (isObject) {
        throwOnInvalidObjectType(returnFiber, newChild);
      }
    
      // 忽略 DEV
      if (__DEV__) {
        if (typeof newChild === 'function') {
          warnOnFunctionType();
        }
      }
    
      // 匹配到 undefined 的对象
      if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) {
        // If the new child is undefined, and the return fiber is a composite
        // component, throw an error. If Fiber return types are disabled,
        // we already threw above.
        switch (returnFiber.tag) {
          // 匹配到 ClassComponent 忽略
          case ClassComponent: {
            if (__DEV__) {
              const instance = returnFiber.stateNode;
              if (instance.render._isMockFunction) {
                // We allow auto-mocks to proceed as if they're returning null.
                break;
              }
            }
          }
          // Intentionally fall through to the next case, which handles both
          // functions and classes
          // eslint-disable-next-lined no-fallthrough
          // 匹配到 FunctionComponent 提醒
          case FunctionComponent: {
            const Component = returnFiber.type;
            invariant(
              false,
              '%s(...): Nothing was returned from render. This usually means a ' +
                'return statement is missing. Or, to render nothing, ' +
                'return null.',
              Component.displayName || Component.name || 'Component',
            );
          }
        }
      }
    
      // Remaining cases are all treated as empty.
      // 以上都不符合,则 是 null, 删除现有所有 children
      // 新的 props.children 都是 null, 老的 props 节点都应该被删除
      return deleteRemainingChildren(returnFiber, currentFirstChild);
    }
  • 接下来看不同节点的更新 reconcileSingleElement

    js 复制代码
    // 从当前已有的所有子节点中,找到一个可以复用新的子节点的Fiber对象
    // 复用它之后,把剩下兄弟节点全部删掉
    function reconcileSingleElement(
      returnFiber: Fiber,
      currentFirstChild: Fiber | null, // 是当前已有的fiber节点,初次渲染没有,后续渲染后很可能存在
      element: ReactElement,
      expirationTime: ExpirationTime,
    ): Fiber {
      // 获取 key 和 child
      const key = element.key;
      let child = currentFirstChild;
      // child 存在
      while (child !== null) {
        // TODO: If key === null and child.key === null, then this only applies to
        // the first item in the list.
        if (child.key === key) {
          // 注意这个判断
          if (
            child.tag === Fragment
              ? element.type === REACT_FRAGMENT_TYPE // 是否是 frament
              : child.elementType === element.type // 新老 element type
          ) {
            // 删除 已存在当前节点的兄弟节点
            // 为何要删除兄弟节点,因为我们这次渲染只有一个节点,老的节点有兄弟节点,新的节点只有一个,删除之
            deleteRemainingChildren(returnFiber, child.sibling);
            const existing = useFiber(
              child,
              element.type === REACT_FRAGMENT_TYPE
                ? element.props.children
                : element.props,
              expirationTime,
            );
            // 挂载属性
            existing.ref = coerceRef(returnFiber, child, element);
            existing.return = returnFiber;
            if (__DEV__) {
              existing._debugSource = element._source;
              existing._debugOwner = element._owner;
            }
            // 找到可复用节点,直接return
            return existing;
          } else {
            // 条件不符合,删除节点并退出 
            deleteRemainingChildren(returnFiber, child);
            break;
          }
        } else {
          // key 不同,则删除
          deleteChild(returnFiber, child);
        }
        // 当前兄弟节点 等于 child 再次进入while循环
        child = child.sibling;
      }
      // 创建新的节点, 基于各种不同的类型,调用不同的创建 Fiber的方式
      if (element.type === REACT_FRAGMENT_TYPE) {
        const created = createFiberFromFragment(
          element.props.children, // 注意这个参数
          returnFiber.mode,
          expirationTime,
          element.key,
        );
        created.return = returnFiber;
        return created;
      } else {
        const created = createFiberFromElement(
          element,
          returnFiber.mode,
          expirationTime,
        );
        created.ref = coerceRef(returnFiber, currentFirstChild, element);
        created.return = returnFiber;
        return created;
      }
    }
    • 进入 createFiberFromFragment

      js 复制代码
      export function createFiberFromFragment(
        elements: ReactFragment,
        mode: TypeOfMode,
        expirationTime: ExpirationTime,
        key: null | string,
      ): Fiber {
        const fiber = createFiber(Fragment, elements, key, mode);
        fiber.expirationTime = expirationTime;
        return fiber;
      }
      
      // This is a constructor function, rather than a POJO constructor, still
      // please ensure we do the following:
      // 1) Nobody should add any instance methods on this. Instance methods can be
      //    more difficult to predict when they get optimized and they are almost
      //    never inlined properly in static compilers.
      // 2) Nobody should rely on `instanceof Fiber` for type testing. We should
      //    always know when it is a fiber.
      // 3) We might want to experiment with using numeric keys since they are easier
      //    to optimize in a non-JIT environment.
      // 4) We can easily go from a constructor to a createFiber object literal if that
      //    is faster.
      // 5) It should be easy to port this to a C struct and keep a C implementation
      //    compatible.
      const createFiber = function(
        tag: WorkTag,
        pendingProps: mixed, // 对于 fragment来说,props只有children, 直接把children作为props
        key: null | string,
        mode: TypeOfMode,
      ): Fiber {
        // $FlowFixMe: the shapes are exact here but Flow doesn't like constructors
        return new FiberNode(tag, pendingProps, key, mode);
      };
    • 以上的 fragment 可以在 ReactFiberBeginWork.js 中的 updateFragment 方法中找到验证

      js 复制代码
      function updateFragment(
        current: Fiber | null,
        workInProgress: Fiber,
        renderExpirationTime: ExpirationTime,
      ) {
        // 看这里,直接通过 .pendingProps 而非 props.children 获取,参考上述 createFiber 写明原因
        const nextChildren = workInProgress.pendingProps;
        reconcileChildren(
          current,
          workInProgress,
          nextChildren,
          renderExpirationTime,
        );
        return workInProgress.child;
      }
    • 进入 createFiberFromElement

      js 复制代码
      export function createFiberFromElement(
        element: ReactElement,
        mode: TypeOfMode,
        expirationTime: ExpirationTime,
      ): Fiber {
        let owner = null;
        if (__DEV__) {
          owner = element._owner;
        }
        // 获取各类属性
        const type = element.type;
        const key = element.key;
        const pendingProps = element.props;
        // 创建 Fiber
        const fiber = createFiberFromTypeAndProps(
          type,
          key,
          pendingProps,
          owner,
          mode,
          expirationTime,
        );
        if (__DEV__) {
          fiber._debugSource = element._source;
          fiber._debugOwner = element._owner;
        }
        return fiber;
      }
    • 进入 createFiberFromTypeAndProps

      js 复制代码
      // 这个方法比较复杂,要判断不同的 type 给 fiber 对象增加 不同的 tag
      // 主要是去匹配 fiberTag
      export function createFiberFromTypeAndProps(
        type: any, // React$ElementType
        key: null | string,
        pendingProps: any,
        owner: null | Fiber,
        mode: TypeOfMode,
        expirationTime: ExpirationTime,
      ): Fiber {
        let fiber;
        // 组件tag未知时的初始化配置 未指定状态
        let fiberTag = IndeterminateComponent;
        // The resolved type is set if we know what the final type will be. I.e. it's not lazy.
        let resolvedType = type;
        // 根据不同类型来处理 fiberTag
        if (typeof type === 'function') {
          // 判断是否有 constructor 方法
          /*
          function shouldConstruct(Component: Function) {
            const prototype = Component.prototype;
            // 注意,这里 isReactComponent 是一个 {}
            return !!(prototype && prototype.isReactComponent);
          }
          */
          if (shouldConstruct(type)) {
            fiberTag = ClassComponent;
          }
        } else if (typeof type === 'string') {
          fiberTag = HostComponent;
        } else {
          getTag: switch (type) {
            case REACT_FRAGMENT_TYPE:
              return createFiberFromFragment(
                pendingProps.children,
                mode,
                expirationTime,
                key,
              );
            case REACT_CONCURRENT_MODE_TYPE:
              return createFiberFromMode(
                pendingProps,
                mode | ConcurrentMode | StrictMode,
                expirationTime,
                key,
              );
            case REACT_STRICT_MODE_TYPE:
              return createFiberFromMode(
                pendingProps,
                mode | StrictMode,
                expirationTime,
                key,
              );
            case REACT_PROFILER_TYPE:
              return createFiberFromProfiler(pendingProps, mode, expirationTime, key);
            case REACT_SUSPENSE_TYPE:
              return createFiberFromSuspense(pendingProps, mode, expirationTime, key);
            default: {
              if (typeof type === 'object' && type !== null) {
                switch (type.$$typeof) {
                  case REACT_PROVIDER_TYPE:
                    fiberTag = ContextProvider;
                    break getTag;
                  case REACT_CONTEXT_TYPE:
                    // This is a consumer
                    fiberTag = ContextConsumer;
                    break getTag;
                  case REACT_FORWARD_REF_TYPE:
                    fiberTag = ForwardRef;
                    break getTag;
                  case REACT_MEMO_TYPE:
                    fiberTag = MemoComponent;
                    break getTag;
                  case REACT_LAZY_TYPE:
                    fiberTag = LazyComponent;
                    resolvedType = null;
                    break getTag;
                }
              }
              let info = '';
              if (__DEV__) {
                if (
                  type === undefined ||
                  (typeof type === 'object' &&
                    type !== null &&
                    Object.keys(type).length === 0)
                ) {
                  info +=
                    ' You likely forgot to export your component from the file ' +
                    "it's defined in, or you might have mixed up default and " +
                    'named imports.';
                }
                const ownerName = owner ? getComponentName(owner.type) : null;
                if (ownerName) {
                  info += '\n\nCheck the render method of `' + ownerName + '`.';
                }
              }
              invariant(
                false,
                'Element type is invalid: expected a string (for built-in ' +
                  'components) or a class/function (for composite components) ' +
                  'but got: %s.%s',
                type == null ? type : typeof type,
                info,
              );
            }
          }
        }
        // 最终创建 Fiber
        fiber = createFiber(fiberTag, pendingProps, key, mode);
        fiber.elementType = type;
        fiber.type = resolvedType;
        fiber.expirationTime = expirationTime;
      
        return fiber;
      }
    • 以上,reconcileSingleElement 就完了

  • 进入 reconcileSingleTextNode

    js 复制代码
    function reconcileSingleTextNode(
      returnFiber: Fiber,
      currentFirstChild: Fiber | null,
      textContent: string,
      expirationTime: ExpirationTime,
    ): Fiber {
      // There's no need to check for keys on text nodes since we don't have a
      // way to define them.
      // 处理 原生 标签
      if (currentFirstChild !== null && currentFirstChild.tag === HostText) {
        // We already have an existing node so let's just update it and delete
        // the rest.
        // 删除兄弟节点,新节点只是一个纯的文本节点
        deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
        // 基于 useFiber 复用当前节点
        const existing = useFiber(currentFirstChild, textContent, expirationTime);
        existing.return = returnFiber;
        return existing;
      }
      // The existing first child is not a text node so we need to create one
      // and delete the existing ones.
      deleteRemainingChildren(returnFiber, currentFirstChild);
      const created = createFiberFromText(
        textContent,
        returnFiber.mode,
        expirationTime,
      );
      created.return = returnFiber;
      return created;
    }
    • 这个流程就比较简单了
  • 现在我们主要看下 deleteRemainingChildren 这个api

    js 复制代码
    function deleteRemainingChildren(
      returnFiber: Fiber, // 当前正在更新的节点
      currentFirstChild: Fiber | null, // 子节点
    ): null {
      // 这是第一次渲染的时候,没有子节点
      // 直接 return
      if (!shouldTrackSideEffects) {
        // Noop.
        return null;
      }
    
      // TODO: For the shouldClone case, this could be micro-optimized a bit by
      // assuming that after the first child we've already added everything.
      // 一个个的删除
      let childToDelete = currentFirstChild;
      while (childToDelete !== null) {
        deleteChild(returnFiber, childToDelete);
        childToDelete = childToDelete.sibling;
      }
      return null;
    }
    • 进入 deleteChild

      js 复制代码
      // 可以看到这里没有实施删除的操作,只是改变了节点上的 effectTag
      // 这里我们只更新 Fiber Tree, 如果真的要 delete 就会影响到 dom节点
      // 所以删除的流程不在这里做,只有在下个阶段,也就是 commit 阶段来做
      // 就是把Fiber Tree 需要更新的流程都过一遍之后,把整个需要更新的属性
      // 通过 firstEffect,lastEffect 这种链条,最终链到根节点上面
      // 最终要执行更新 只需要在 commit 阶段拿到 根节点上面的 这种链条把每个节点去执行即可
      // 在处理更新的时候任务是可以中断的,但是在 commit 阶段任务是不可中断的
      function deleteChild(returnFiber: Fiber, childToDelete: Fiber): void {
        if (!shouldTrackSideEffects) {
          // Noop.
          return;
        }
        // Deletions are added in reversed order so we add it to the front.
        // At this point, the return fiber's effect list is empty except for
        // deletions, so we can just append the deletion to the list. The remaining
        // effects aren't added until the complete phase. Once we implement
        // resuming, this may not be true.
        const last = returnFiber.lastEffect;
        // 存在 last
        if (last !== null) {
          last.nextEffect = childToDelete;
          returnFiber.lastEffect = childToDelete;
        } else {
          // 不存在 last
          returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
        }
        childToDelete.nextEffect = null; // 已经是要删除的节点,上面的其他副作用都是没有任何意义的
        // effectTag 就是告诉后续的 commit 阶段,对于这个节点需要执行什么操作,可以看到,这里是删除
        childToDelete.effectTag = Deletion; // 只需要设置这个即可
      }
  • reconcileChildrenArrayreconcileChildrenIterator 这两个api比较复杂,先跳过

  • 其他的 API 都是比较简单了

相关推荐
吃杠碰小鸡40 分钟前
commitlint校验git提交信息
前端
虾球xz1 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
我爱李星璇1 小时前
HTML常用表格与标签
前端·html
疯狂的沙粒1 小时前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员2 小时前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐2 小时前
前端图像处理(一)
前端
程序猿阿伟2 小时前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒2 小时前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪2 小时前
AJAX的基本使用
前端·javascript·ajax
力透键背2 小时前
display: none和visibility: hidden的区别
开发语言·前端·javascript