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

React源码解析之FunctionComponent(中)

前言

接上篇— —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

本文分享自微信公众号 - webchen(webchen1995),作者:webchen

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

原始发表时间:2019-11-30

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

我来说两句

0 条评论
登录 后参与评论

相关文章

  • React源码解析之completeUnitOfWork

    (1) 关于completeUnitOfWork()在哪里使用到,请看下 React源码解析之workLoop 中的二、performUnitOfWork

    进击的小进进
  • React源码解析之「错误处理」流程

    在 React源码解析之renderRoot概览 中提到了,当有异常抛出的时候,会执行completeUnitOfWork():

    进击的小进进
  • React源码解析之FunctionComponent(上)

    在 React源码解析之workLoop 中讲到当workInProgress.tag为FunctionComponent时,会进行FunctionCompon...

    进击的小进进
  • JavaScript贪食蛇游戏制作详解

    之前闲时开发过一个简单的网页版贪食蛇游戏程序,现在把程序的实现思路写下来,供有兴趣同学参考阅读。 代码的实现比较简单,整个程序由三个类,一组常量和一些游戏逻辑...

    用户1608022
  • React源码解析之completeUnitOfWork

    (1) 关于completeUnitOfWork()在哪里使用到,请看下 React源码解析之workLoop 中的二、performUnitOfWork

    进击的小进进
  • 【自然框架】之通用权限(六):权限到节点

          “直率没有错,但是也要考虑对方的承受能力呀!对方都承受不了了,你还直率,那就是你的错了!”  ——我的名言,呵呵。     ===========...

    用户1174620
  • React源码解析之FunctionComponent(上)

    在 React源码解析之workLoop 中讲到当workInProgress.tag为FunctionComponent时,会进行FunctionCompon...

    进击的小进进
  • webpack源码阅读之Compiler

    本篇记录了阅读Compiler.js过程中的一些笔记。(Webpack版本4.41.0)

    IMWeb前端团队
  • 学习笔记 TF061 : 分布式 TensorFlow,分布式原理、最佳实践

    分布式 TensorFlow 由高性能 gRPC 库底层技术支持。Martin Abadi、Ashish Agarwal、Paul Barham 论文《 Ten...

    利炳根
  • NEO4J 图数据库哪里和哪里 从哪里开始

    上期已经安装了图数据库,本期就该讨论到底这个图数据库里面的一些基本的概念和如何操作。最近听到一句话,年轻不年轻,不是看年龄,而是看你对新鲜事物的热情,即使你20...

    AustinDatabases

扫码关注云+社区

领取腾讯云代金券