专栏首页前端干货和生活感悟React源码解析之FunctionComponent(下)

React源码解析之FunctionComponent(下)

前言

React源码解析之FunctionComponent(中) 中,讲到了reconcileSingleElement()reconcileSingleTextNode()

function reconcileChildFibers() {
  if (isObject) {
      switch (newChild.$$typeof) {
        // ReactElement节点
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement());
      }
    }
    //文本节点
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      return placeSingleChild(
        reconcileSingleTextNode());
    }
    //数组节点,也是本文要讲的
    if (isArray(newChild)) {
      return reconcileChildrenArray();
    }
}

接下来,我们讲reconcileChildrenArray()是如何更新数组节点的

一、reconcileChildrenArray

作用: 更新数组节点

源码:

  function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    //待更新的数组节点
    newChildren: Array<*>,
    expirationTime: ExpirationTime,
  ): Fiber | null {
    // This algorithm can't optimize by searching from both ends since we
    // don't have backpointers on fibers. I'm trying to see how far we can get
    // with that model. If it ends up not being worth the tradeoffs, we can
    // add it later.

    // Even with a two ended optimization, we'd want to optimize for the case
    // where there are few changes and brute force the comparison instead of
    // going for the Map. It'd like to explore hitting that path first in
    // forward-only mode and only go for the Map once we notice that we need
    // lots of look ahead. This doesn't handle reversal as well as two ended
    // search but that's unusual. Besides, for the two ended optimization to
    // work on Iterables, we'd need to copy the whole set.

    // In this first iteration, we'll just live with hitting the bad case
    // (adding everything to a Map) in for every insert/move.

    // If you change this code, also update reconcileChildrenIterator() which
    // uses the same algorithm.

    //删除了 dev 代码

    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++) {
      // 当要更新的节点的 index 大于 newIndex 时,
      // 说明它不在所期盼的位置上,则需要“认真处理”oldFiber
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      }
      //否则,则处理该节点的下一个兄弟节点
      else {
        nextOldFiber = oldFiber.sibling;
      }
      //复用或新建节点
      const newFiber = updateSlot(
        //当前节点的父节点
        returnFiber,
        //旧节点
        oldFiber,
        //待更新的新节点
        newChildren[newIdx],
        expirationTime,
      );
      //说明key 不相同,节点不能复用,此时就跳出循环
      //如果不跳出循环,说明可以是相同的
      //也就是说当跳出循环的时候,我们可以知道截至目前,复用节点的个数,和不可复用节点的 index,
      if (newFiber === null) {
        // TODO: This breaks on empty slots like null children. That's
        // unfortunate because it triggers the slow path all the time. We need
        // a better way to communicate whether this was a miss or null,
        // boolean, undefined, etc.
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      //初次渲染的情况下
      if (shouldTrackSideEffects) {
        //newFiber.alternate表示并没有复用 oldFiber 来赋值,而是 return 了新的 fiber
        //所以要删除存在的 旧的fiber
        if (oldFiber && newFiber.alternate === null) {
          // We matched the slot, but we didn't reuse the existing fiber, so we
          // need to delete the existing child.
          deleteChild(returnFiber, oldFiber);
        }
      }
      //将 newFiber 节点挂载到 DOM 树上,返回最新移动的 index
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      //表示是新节点
      if (previousNewFiber === null) {
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {
        // TODO: Defer siblings if we're not at the right index for this slot.
        // I.e. if we had null values before, then we want to defer this
        // for each null value. However, we also don't want to call updateSlot
        // with the previous one.
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }
    //跳出循环后
    //index=length,说明截止到最后,所有节点都是可以复用的
    //故可以删除老节点
    if (newIdx === newChildren.length) {
      // We've reached the end of the new children. We can delete the rest.
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }

    if (oldFiber === null) {
      // If we don't have any more existing children we can choose a fast path
      // since the rest will all be insertions.
      //老节点已经被复用完,但是仍有部分新节点没有被创建
      for (; newIdx < newChildren.length; newIdx++) {
        //新建节点
        const newFiber = createChild(
          returnFiber,
          newChildren[newIdx],
          expirationTime,
        );
        if (newFiber === null) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          // TODO: Move out of the loop. This only happens for the first run.
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
    }

    // Add all children to a key map for quick lookups.
    //数组可能存在顺序的变化,oldfiber和 newfiber 还有可以复用的 fiber 节点
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // Keep scanning and use the map to restore deleted items as moves.
    //继续遍历剩下的 new 节点
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        expirationTime,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          //不为 null 说明 fiber 节点已经被复用了,所以可以从 Map 中删除
          if (newFiber.alternate !== null) {
            // The new fiber is a work in progress, but if there exists a
            // current, that means that we reused the fiber. We need to delete
            // it from the child list so that we don't add it to the deletion
            // list.

            existingChildren.delete(
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

    if (shouldTrackSideEffects) {
      // Any existing children that weren't consumed above were deleted. We need
      // to add them to the deletion list.
      //删除没有复用的节点
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }

    return resultingFirstChild;
  }

解析: (1) 循环数组节点,在循环中主要做了如下几点:

① 将 oldFiber 的 index与 newIdx 进行比较, 如果 oldFIber.index 大,则将 oldFiber 赋值给 nextOldFiber(表示需要处理); 如果 newIdx 大,则将 oldFiber.sibling 赋值给 nextOldFiber

② 执行updateSlot(),复用或新建节点,返回的结果赋值给newFiber

③ 如果newFiber的值为空的话,说明该节点不能复用,则跳出循环(break

④ 如果是第一次渲染(即shouldTrackSideEffects为 true),并且 newFiber 没有要复用的 oldFiber 的话,则删除该 fiber 下的所有子节点

关于deleteChild的讲解,请看:React源码解析之FunctionComponent(中)

⑤ 执行placeChild(),将 newFiber 节点挂载到 DOM 树上,并判断更新后是否移动过,如果移动,则需要重新挂载,返回最新移动的 index,并赋值给lastPlacedIndex

previousNewFiber那段,意思是为数组里的每一个 fiber 节点设置 sibling 属性,即它旁边的 fiber(index+1)

(2) 跳出循环后,如果newIdx和更新的数组长度相等,则表示所有节点都是可以复用的,那么就执行deleteRemainingChildren(),删除旧节点

(3) 如果旧节点都已经被复用完了,但是仍有部分新节点需要被创建的话,则循环剩余数组的长度,并依次创建新节点(部分代码与上面重复,不再赘述)

(4) 如果仍有旧节点剩余的话,则执行mapRemainingChildren(),将这些旧节点用 Map 结构集合起来,看有没有方便 newFiber 复用的节点

(5) 继续遍历剩下的 new 节点 ① 执行updateFromMap(),查找有没有 key/index 相同的点,方便复用

if (newFiber !== null)的部分逻辑与上面相同,不再赘述

(6) 如果是第一次渲染的话,则删除没有复用的节点

(7) 最终返回 更新后的数组的第一个节点(根据它的 silbing 属性,可找到其他节点)

后面的部分是针对reconcileChildrenArray()出现的一些函数的补充

二、updateSlot

作用: 复用或新建节点

源码:

  //复用或新建节点

  //key 相同的情况下,进行节点复用;
  //key 不同的情况下,无法复用
  function updateSlot(
    //当前节点的父节点
    returnFiber: Fiber,
    //旧节点
    oldFiber: Fiber | null,
    //待更新的新节点
    newChild: any,
    expirationTime: ExpirationTime,
  ): Fiber | null {
    // Update the fiber if the keys match, otherwise return null.

    const key = oldFiber !== null ? oldFiber.key : null;
    //文本节点是没有 key 的
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      // Text nodes don't have keys. If the previous node is implicitly keyed
      // we can continue to replace it without aborting even if it is not a text
      // node.
      //如果老节点有 key 的话,说明是从 ReactElement 节点转变为文本节点了
      // 这样也没关系,可以不间断更新
      if (key !== null) {
        return null;
      }
      //执行updateTextNode,对文本节点进行更新
      return updateTextNode(
        returnFiber,
        oldFiber,
        '' + newChild,
        expirationTime,
      );
    }

    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE: {
          //前后 key 相同,说明可以复用
          if (newChild.key === key) {
            if (newChild.type === REACT_FRAGMENT_TYPE) {
              return updateFragment(
                returnFiber,
                oldFiber,
                newChild.props.children,
                expirationTime,
                key,
              );
            }
            return updateElement(
              returnFiber,
              oldFiber,
              newChild,
              expirationTime,
            );
          }
          //否则不能复用
          else {
            return null;
          }
        }
        case REACT_PORTAL_TYPE: {
          if (newChild.key === key) {
            return updatePortal(
              returnFiber,
              oldFiber,
              newChild,
              expirationTime,
            );
          } else {
            return null;
          }
        }
      }

      if (isArray(newChild) || getIteratorFn(newChild)) {
        if (key !== null) {
          return null;
        }

        return updateFragment(
          returnFiber,
          oldFiber,
          newChild,
          expirationTime,
          null,
        );
      }

      throwOnInvalidObjectType(returnFiber, newChild);
    }

    if (__DEV__) {
      if (typeof newChild === 'function') {
        warnOnFunctionType();
      }
    }

    return null;
  }

解析: (1) 如果是文本节点的话,是不能根据 key 去判断是否复用的,注意下

(2) 如果是其他节点类型的话,则执行相应的函数,来进行节点更新(key 相同则复用)

三、placeChild

作用: 将 newFiber 节点挂载到 DOM 树上,并判断更新后是否移动过,如果移动,则需要重新挂载,返回最新移动的 index

源码:

  function placeChild(
    newFiber: Fiber,
    lastPlacedIndex: number,
    newIndex: number,
  ): number {
    newFiber.index = newIndex;
    if (!shouldTrackSideEffects) {
      // Noop.
      return lastPlacedIndex;
    }
    const current = newFiber.alternate;
    if (current !== null) {
      const oldIndex = current.index;
      //移动了的节点
      if (oldIndex < lastPlacedIndex) {
        // This is a move.
        //因为是移动的节点,所以要重新挂载到 DOM 上
        newFiber.effectTag = Placement;
        return lastPlacedIndex;
      } else {
        //没有移动
        // This item can stay in place.
        return oldIndex;
      }
    }
    //current 为 null 说明该节点没有被渲染过
    //所以是新插入的节点
    else {
      // This is an insertion.
      newFiber.effectTag = Placement;
      return lastPlacedIndex;
    }
  }

解析: (1) 如果不是初次渲染的话(shouldTrackSideEffects 为 true),无需更新shouldTrackSideEffects

(2) newFiber.alternate有值的话,说明是由旧节点更新来的,那么就需要比较oldIndexlastPlacedIndex,有移动过的话,则返回lastPlacedIndex,否则返回oldIndex

(3) newFiber.alternate没有值的话,说明不是由旧节点更新来的,而是新插入的节点,返回lastPlacedIndex

四、mapRemainingChildren

作用: 将旧节点用 Map 结构集合起来,方便 newFiber 复用

源码:

  function mapRemainingChildren(
    returnFiber: Fiber,
    currentFirstChild: Fiber,
  ): Map<string | number, Fiber> {
    // Add the remaining children to a temporary map so that we can find them by
    // keys quickly. Implicit (null) keys get added to this set with their index
    // instead.
    const existingChildren: Map<string | number, Fiber> = new Map();

    let existingChild = currentFirstChild;
    //遍历剩下的节点,获取其 key
    while (existingChild !== null) {
      if (existingChild.key !== null) {
        existingChildren.set(existingChild.key, existingChild);
      } else {
        existingChildren.set(existingChild.index, existingChild);
      }
      existingChild = existingChild.sibling;
    }
    //创建了一个 Map 对象,以便找到key 相同的节点,方便复用
    return existingChildren;
  }

解析: 利用 Map 结构,遍历剩下的 oldFiber,以key-value的形式,将这些旧节点存到 Map 中,如果没有key的话,则说明是文本节点,则以index-value的形式存储,最终返回这个 Map 对象

五、updateFromMap

作用: 在 Map 对象中查找有没有 key/index 相同的 fiber 节点,方便复用

源码:

function updateFromMap(
    existingChildren: Map<string | number, Fiber>,
    returnFiber: Fiber,
    newIdx: number,
    newChild: any,
    expirationTime: ExpirationTime,
  ): Fiber | null {
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      // Text nodes don't have keys, so we neither have to check the old nor
      // new node for the key. If both are text nodes, they match.
      //如果是文本节点的话,会从 Map 对象中寻找是否有相同的 index(为什么不是key?因为文本节点没有 key 属性)
      const matchedFiber = existingChildren.get(newIdx) || null;
      return updateTextNode(
        returnFiber,
        matchedFiber,
        '' + newChild,
        expirationTime,
      );
    }

    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE: {
          //updateSlot()是根据 key 是否相同来判断,这边是根据 Map 中是否有key/index 来判断
          const matchedFiber =
            existingChildren.get(
              newChild.key === null ? newIdx : newChild.key,
            ) || null;
          if (newChild.type === REACT_FRAGMENT_TYPE) {
            return updateFragment(
              returnFiber,
              matchedFiber,
              newChild.props.children,
              expirationTime,
              newChild.key,
            );
          }
          return updateElement(
            returnFiber,
            matchedFiber,
            newChild,
            expirationTime,
          );
        }
        case REACT_PORTAL_TYPE: {
          const matchedFiber =
            existingChildren.get(
              newChild.key === null ? newIdx : newChild.key,
            ) || null;
          return updatePortal(
            returnFiber,
            matchedFiber,
            newChild,
            expirationTime,
          );
        }
      }

      if (isArray(newChild) || getIteratorFn(newChild)) {
        const matchedFiber = existingChildren.get(newIdx) || null;
        return updateFragment(
          returnFiber,
          matchedFiber,
          newChild,
          expirationTime,
          null,
        );
      }

      throwOnInvalidObjectType(returnFiber, newChild);
    }

    if (__DEV__) {
      if (typeof newChild === 'function') {
        warnOnFunctionType();
      }
    }

    return null;
  }

解析:二、updateSlot的内容差不多,不再赘述

关于FunctionComponent的更新讲解就先到这里结束了

GitHub: ReactChildFiber.js:https://github.com/AttackXiaoJinJin/reactExplain/tree/master/react16.8.6/packages/react-reconciler/src/ReactChildFiber.js


本文分享自微信公众号 - webchen(webchen1995)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-12-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • React Native 架构一览

    Native 管理 UI 更新及交互,JavaScript 调用 Native 能力实现业务功能,Bridge 在二者之间传递消息。即:

    ayqy贾杰
  • 在追寻极致体验的康庄大道上,React 玩出了花

    前者是 loading(或 skeleton)带来的好处,而后者得益于 Concurrent Mode 下的间歇调度

    ayqy贾杰
  • concurrent 模式 API 参考(实验版)

    本章节为 concurrent 模式的 React API 参考。如果你想找导览,请查看 concurrent UI 模式。

    Fonkie
  • 这可能是最通俗的 React Fiber 打开方式

    写一篇关于 React Fiber 的文章, 这个 Flag 立了很久,这也是今年的目标之一。最近的在掘金的文章获得很多关注和鼓励,给了我很多动力,所以下定决心...

    前端劝退师
  • React Native 在 Airbnb 的起起落落

    感谢支持ayqy个人订阅号,每周义务推送1篇(only unique one)原创精品博文,话题包括但不限于前端、Node、Android、数学...

    ayqy贾杰
  • React Native 架构演进

    上一篇(React Native 架构一览)从设计、线程模型等方面介绍了 React Native 的现有架构,本篇将分析这种架构的局限性,以及 React N...

    ayqy贾杰
  • 微前端在解决什么问题?

    上一篇微前端到底是什么已经从概念定义及实现思路上探究了微前端是什么的问题,而要彻底理解微前端的话,还需要想清楚这些问题:

    ayqy贾杰
  • 尝试:Script Lab,Excel 基础操作(1)

    前期01:尝试:Script Lab,快速 Office 365 开发工具 //SL01

    寒树Office与RPA
  • 1000千米高空俯瞰 React Native

    Native 用上 React 的话,也能获得 React 的种种好处。当然,这只是一方面,背后的真正源动力是希望 Native 开发能像 Web 一样 mov...

    ayqy贾杰
  • JSX AS DSL? 写个 Mock API 服务器看看

    这几天打算写一个简单的 API Mock 服务器,老生常谈哈?其实我是想讲 JSX, Mock 服务器只是一个幌子。我在寻找一种更简洁、方便、同时又可以灵活扩展...

    _sx_

扫码关注云+社区

领取腾讯云代金券