React源码解析之FunctionComponent(中)

  • 2019 年 12 月 2 日
  • 筆記

前言

接上篇— —React源码解析之FunctionComponent(上)

一、reconcileSingleElement

作用: 当子节点不为 null,则复用子节点并删除其兄弟节点; 当子节点为 null,则创建新的 fiber 节点

源码:

  //当子节点不为 null,则复用子节点并删除其兄弟节点;    //当子节点为 null,则创建新的 fiber 节点    function reconcileSingleElement(      returnFiber: Fiber,      //旧      currentFirstChild: Fiber | null,      //新      element: ReactElement,      expirationTime: ExpirationTime,    ): Fiber {      const key = element.key;      let child = currentFirstChild;      //从当前已有的所有子节点中,找到可以复用的 fiber 对象,并删除它的 兄弟节点      while (child !== null) {        // TODO: If key === null and child.key === null, then this only applies to        // the first item in the list.        //key 相同的话复用节点        //ReactElement里面的key,也就是开发过程中加的 key,如<div key={'a'}></div>        //所以当有多个相同 element 放在同一组时,React 建议设置 key,方便不产生更新的节点能复用        //但是我自己试验了下,发现打印出的 ReactElement 的 key('a') 和_owner 下fiber 节点的 key(null) 是不一样的,        //而且设置 ReactElement 的 key 不影响 fiber 对象的 key 值一直为 null          //所以这边 fiber.key 和 ReactElement.key 相等的情况,大多数应为 null===null        if (child.key === key) {          //如果节点类型未改变的话          if (            child.tag === Fragment              ? element.type === REACT_FRAGMENT_TYPE              : child.elementType === element.type ||              // Keep this check inline so it only runs on the false path:              (__DEV__                ? isCompatibleFamilyForHotReloading(child, element)                : false)          ) {            //复用 child,删除它的兄弟节点            //因为旧节点它有兄弟节点,新节点只有它一个            deleteRemainingChildren(returnFiber, child.sibling);            //复制 fiber 节点,并重置 index 和 sibling            //existing就是复用的节点            const existing = useFiber(              child,              element.type === REACT_FRAGMENT_TYPE                ? element.props.children                : element.props,              expirationTime,            );            //设置正确的 ref            existing.ref = coerceRef(returnFiber, child, element);            //父节点            existing.return = returnFiber;            //删除了 dev 代码              return existing;          } else {            deleteRemainingChildren(returnFiber, child);            break;          }        } else {          deleteChild(returnFiber, child);        }        child = child.sibling;      }      //上面是子节点不为 null 的情况,能执行到这边说明之前是 null,现在要新建      //创建新的节点      if (element.type === REACT_FRAGMENT_TYPE) {        //创建Fragment类型的 fiber 节点        const created = createFiberFromFragment(          element.props.children,          returnFiber.mode,          expirationTime,          element.key,        );        created.return = returnFiber;        return created;      } else {        //创建Element类型的 fiber 节点        const created = createFiberFromElement(          element,          returnFiber.mode,          expirationTime,        );        created.ref = coerceRef(returnFiber, currentFirstChild, element);        created.return = returnFiber;        return created;      }    }

解析:

(1) 流程图如下:

(2) 针对child.key === ReactElement.key的情况,在开发过程中,大多数的 React 组件都是复用的,因为它们都是“列表”中的第一项,所以fiber.key(nulll)=ReactElement.key(null)true

(3) deleteRemainingChildren()在本文后面有讲到

(3) 执行useFiber(),通过复制 fiber 节点达到「复用」fiber 节点的目的,里面用到了double-buffer的技巧,本文后面会讲到

(4) 不能根据旧节点复用成新节点的话,则通过createFiberFromElement()创建FragmentElement类型的 fiber 节点

(5) 最后,返回处理过的节点

二、useFiber

作用: 复制 fiber 节点,并重置 index 和 sibling

源码:

 //复制 fiber 节点,并重置 index 和 sibling    function useFiber(      fiber: Fiber,      pendingProps: mixed,      expirationTime: ExpirationTime,    ): Fiber {      // We currently set sibling to null and index to 0 here because it is easy      // to forget to do before returning it. E.g. for the single child case.      //为防止忘记,提前将 index 置为 0,兄弟节点置为 null      //通过 doubleBuffer 重用未更新的 fiber 对象      const clone = createWorkInProgress(fiber, pendingProps, expirationTime);      clone.index = 0;      clone.sibling = null;      return clone;    }

解析: 执行createWorkInProgress(),通过 doubleBuffer 重用未更新的 fiber 对象,并将返回的 fiber 节点的 index、sibling 置为初始状态

三、createWorkInProgress

作用: 通过 doubleBuffer 重用未更新的 fiber 对象

源码:

// This is used to create an alternate fiber to do work on.  //通过 doubleBuffer 重用未更新的 fiber 对象  export function createWorkInProgress(    current: Fiber,    pendingProps: any,    expirationTime: ExpirationTime,  ): Fiber {    let workInProgress = current.alternate;    if (workInProgress === null) {      // We use a double buffering pooling technique because we know that we'll      // only ever need at most two versions of a tree. We pool the "other" unused      // node that we're free to reuse. This is lazily created to avoid allocating      // extra objects for things that are never updated. It also allow us to      // reclaim the extra memory if needed.        //因为一棵 fiber 树顶多有两个版本,所以当某一 fiber 节点不更新时,在更新 fiber 树的时候,      //不会去重新创建跟之前一样的 fiber 节点,而是从另一个版本的 fiber 树上重用它      workInProgress = createFiber(        current.tag,        pendingProps,        current.key,        current.mode,      );      workInProgress.elementType = current.elementType;      workInProgress.type = current.type;      workInProgress.stateNode = current.stateNode;        //删除了 dev 代码        workInProgress.alternate = current;      current.alternate = workInProgress;    } else {      workInProgress.pendingProps = pendingProps;        // We already have an alternate.      // Reset the effect tag.      workInProgress.effectTag = NoEffect;        // The effect list is no longer valid.      workInProgress.nextEffect = null;      workInProgress.firstEffect = null;      workInProgress.lastEffect = null;        if (enableProfilerTimer) {        // We intentionally reset, rather than copy, actualDuration & actualStartTime.        // This prevents time from endlessly accumulating in new commits.        // This has the downside of resetting values for different priority renders,        // But works for yielding (the common case) and should support resuming.        workInProgress.actualDuration = 0;        workInProgress.actualStartTime = -1;      }    }      workInProgress.childExpirationTime = current.childExpirationTime;    workInProgress.expirationTime = current.expirationTime;      workInProgress.child = current.child;    workInProgress.memoizedProps = current.memoizedProps;    workInProgress.memoizedState = current.memoizedState;    workInProgress.updateQueue = current.updateQueue;      // Clone the dependencies object. This is mutated during the render phase, so    // it cannot be shared with the current fiber.    const currentDependencies = current.dependencies;    workInProgress.dependencies =      currentDependencies === null        ? null        : {            expirationTime: currentDependencies.expirationTime,            firstContext: currentDependencies.firstContext,            events: currentDependencies.events,          };      // These will be overridden during the parent's reconciliation    workInProgress.sibling = current.sibling;    workInProgress.index = current.index;    workInProgress.ref = current.ref;      if (enableProfilerTimer) {      workInProgress.selfBaseDuration = current.selfBaseDuration;      workInProgress.treeBaseDuration = current.treeBaseDuration;    }      //删除了 dev 代码      return workInProgress;  }

解析: React 会准备 fiber 树的两个版本(新版本和旧版本),当新版本的某一新节点在旧版本上有时,可以复用旧 fiber 的属性,而不是重新创建新的节点。

新旧 fiber 树相互复用的思路来源于doubleBuffer

四、reconcileSingleTextNode

作用: 复用或创建文本节点

源码:

  //复用或创建文本节点    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.        //删掉旧节点        //returnFiber是当前正在更新的节点        //currentFirstChild是第一个子节点        deleteRemainingChildren(returnFiber, currentFirstChild.sibling);        //复用        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;    }

解析: 仍有则复用,没有则执行createFiberFromText()来新建

五、createFiberFromText

作用: 创建文本类型的 fiber

源码:

export function createFiberFromText(    content: string,    mode: TypeOfMode,    expirationTime: ExpirationTime,  ): Fiber {    const fiber = createFiber(HostText, content, null, mode);    fiber.expirationTime = expirationTime;    return fiber;  }

解析: content 就是要更新的文本

createFiber()里面逻辑不复杂,就不解析了,放下源码:

//pendingProps就是 props.children  const createFiber = function(    tag: WorkTag,    pendingProps: mixed,    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);  };

FiberNode()的源码:

function FiberNode(    tag: WorkTag,    pendingProps: mixed,    key: null | string,    mode: TypeOfMode,  ) {    // Instance    this.tag = tag;    this.key = key;    this.elementType = null;    this.type = null;    this.stateNode = null;      // Fiber    this.return = null;    this.child = null;    this.sibling = null;    this.index = 0;      this.ref = null;      this.pendingProps = pendingProps;    this.memoizedProps = null;    this.updateQueue = null;    this.memoizedState = null;    this.dependencies = null;      this.mode = mode;      // Effects    this.effectTag = NoEffect;    this.nextEffect = null;      this.firstEffect = null;    this.lastEffect = null;      this.expirationTime = NoWork;    this.childExpirationTime = NoWork;      this.alternate = null;      if (enableProfilerTimer) {      // Note: The following is done to avoid a v8 performance cliff.      //      // Initializing the fields below to smis and later updating them with      // double values will cause Fibers to end up having separate shapes.      // This behavior/bug has something to do with Object.preventExtension().      // Fortunately this only impacts DEV builds.      // Unfortunately it makes React unusably slow for some applications.      // To work around this, initialize the fields below with doubles.      //      // Learn more about this here:      // https://github.com/facebook/react/issues/14365      // https://bugs.chromium.org/p/v8/issues/detail?id=8538      this.actualDuration = Number.NaN;      this.actualStartTime = Number.NaN;      this.selfBaseDuration = Number.NaN;      this.treeBaseDuration = Number.NaN;        // It's okay to replace the initial doubles with smis after initialization.      // This won't trigger the performance cliff mentioned above,      // and it simplifies other profiler code (including DevTools).      this.actualDuration = 0;      this.actualStartTime = -1;      this.selfBaseDuration = 0;      this.treeBaseDuration = 0;    }      //删除了 dev 代码  }

六、deleteRemainingChildren

作用: 如果旧节点存在,但是更新的节点是 null 的话,需要删除旧节点的内容

源码:

  //删除旧节点    function deleteRemainingChildren(      returnFiber: Fiber,      currentFirstChild: Fiber | null,    ): null {      //第一次渲染的情况      //是没有子节点的,所以直接删除      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;    }

解析: 注意最后 return 的是 null

七、deleteChild

作用: 为要删除的子节点们做Deletion标记

源码:

//为要删除的子节点们做Deletion标记    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.      //fiber 链表的 effect 除了是deletions外,都是空的,可以根据这个进行删除        //标记副作用,以便在 commit 阶段进行删除      const last = returnFiber.lastEffect;      //做标记      if (last !== null) {        //要删除的节点(fiber)          last.nextEffect = childToDelete;        returnFiber.lastEffect = childToDelete;      } else {        returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;      }      childToDelete.nextEffect = null;      //这里并未执行删除操作,而仅仅是给effectTag赋值了Deletion      //因为这里仍是对 fiber 树的更新,未涉及到真正的 DOM 节点      //真正的删除留到 commit 阶段      childToDelete.effectTag = Deletion;    }

解析: 在 fiber 树上,循环每一个子节点,并做上 Deletion 标记,以便在commit 阶段进行真删除


本文主要讲了reconcileSingleElement()reconcileSingleTextNode()deleteRemainingChildren()的方法,下篇文章会继续讲FunctionComponent中的数组节点的更新:

 //数组节点      if (isArray(newChild)) {        return reconcileChildrenArray(          returnFiber,          currentFirstChild,          newChild,          expirationTime,        );      }

GitHub:

ReactChildFiber.js:

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

ReactFiber.js:

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