前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >React源码解析之FunctionComponent(中)

React源码解析之FunctionComponent(中)

作者头像
进击的小进进
发布2019-12-02 21:09:33
5520
发布2019-12-02 21:09:33
举报

前言

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

一、reconcileSingleElement

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

源码:

代码语言:javascript
复制
  //当子节点不为 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

源码:

代码语言:javascript
复制
 //复制 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 对象

源码:

代码语言:javascript
复制
// 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

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

源码:

代码语言:javascript
复制
  //复用或创建文本节点
  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

源码:

代码语言:javascript
复制
export function createFiberFromText(
  content: string,
  mode: TypeOfMode,
  expirationTime: ExpirationTime,
): Fiber {
  const fiber = createFiber(HostText, content, null, mode);
  fiber.expirationTime = expirationTime;
  return fiber;
}

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

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

代码语言:javascript
复制
//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()的源码:

代码语言:javascript
复制
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 的话,需要删除旧节点的内容

源码:

代码语言:javascript
复制
  //删除旧节点
  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标记

源码:

代码语言:javascript
复制
//为要删除的子节点们做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中的数组节点的更新:

代码语言:javascript
复制
 //数组节点
    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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-11-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 webchen 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 一、reconcileSingleElement
  • 二、useFiber
  • 三、createWorkInProgress
  • 四、reconcileSingleTextNode
  • 五、createFiberFromText
  • 六、deleteRemainingChildren
  • 七、deleteChild
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档