React Fiber源码笔记(四):vDOM-FiberNode

220 阅读8分钟

前言:最近离职准备面试,把之前写的笔记整理一下发出来,本人能力有限,如有错误的地方尽情指正(免责声明)
博客链接:pionpill

主要源码: ReactFiber

这章涉及到很多重要的数据结构及属性,比较难记,后文讲到建议多回来看几遍。

FiberNode 是一个包含 Fiber 架构所需要数据的对象,也是 vDOM 这一概念对应的具体数据结构。后文中 FiberNode 就是指 Fiber 架构中的 vDOM。

FiberNode 包含了两层重要的数据:

  • 作为静态数据结构: 保存了一个组件所需要的所有 DOM 信息,也即从 ReactElement 来的数据。
  • 作为动态工作单元: 保存了本次更新过程中组件需要执行操作的状态与信息。

看下 FiberNode 的源代码(✨约136行),这里的属性都非常重要,后面如果忘了建议回来多看几遍:

function FiberNode(
  tag: WorkTag,
  pendingProps: mixed,
  key: null | string,
  mode: TypeOfMode,
) {
  /** 基本静态属性 */
  this.tag = tag;                   // 对应节点的详细类型,非常重要
  this.key = key;                   // 优化用的key,源于 ReactElement
  this.elementType = null;          // 节点的元素类型,源于 ReactElement 的 type
  this.type = null;                 // 节点的类型,主要起作用于开发模式,生产环境下和 elementType 类似
  this.pendingProps = pendingProps; // 组件属性,源于 ReactElement
  this.mode = mode;                 // 优先级相关
  this.lanes = NoLanes;             // 优先级相关
  this.ref = null;                  // 引用
  this.refCleanup = null;           // 在生命周期中操作 ref

  /** DOM结构相关的属性 */
  this.return = null;               // 指向父 Fiber 节点
  this.child = null;                // 指向子 Fiber 节点
  this.sibling = null;              // 指向右侧 Fiber 节点
  this.index = 0;                   // 在父节点的 children 列表中的索引位置,优化用的

  /** 与组件更新相关的属性 */
  this.memoizedProps = null;        // 组件当前的 prop
  this.updateQueue = null;          // 更新队列
  this.memoizedState = null;        // hook 链表
  this.dependencies = null;         // 组件的 context 依赖项
  this.flags = NoFlags;             // render 阶段打上的 flag(一个标签),commit 阶段处理
  this.subtreeFlags = NoFlags;      // 子树的 flag
  this.deletions = null;            // 要删除的节点
  this.alternate = null;            // 状态的备份
  this.stateNode = null;            // 当前节点关联的实际DOM节点或其他类型的节点
  this.childLanes = NoLanes;        // 保存当前节点的子节点所在的优先级。
}

FiberNode 构造过程

先看一下 Fiber 是如何将 ReactElement 转换为 FiberNode 的,这里接收的参数多了两个:modelanesmode 是指节点模式,比如 StrictModelanes 则是优先级,后文会细讲(✨约659行):

export function createFiberFromElement(
  element: ReactElement,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
  let source, owner = null;
  const type = element.type;
  const key = element.key;
  const pendingProps = element.props;
  return createFiberFromTypeAndProps(type, key, pendingProps, source, owner, mode, lanes);
}

核心方法在 createFiberFromTypeAndProps 上(✨约488行):

export function createFiberFromTypeAndProps(
  type: any,
  key: null | string,
  pendingProps: any,
  source: null | Source,
  owner: null | Fiber,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
  // 这两个变量都是用于表示节点类型的
  let fiberTag = IndeterminateComponent; // 节点类型,初始化为不定类型
  let resolvedType = type; // 节点的最终类型

  // 这一段代码只干一件事: 确定 fiberTag 类型
  if (typeof type === 'function' && shouldConstruct(type)) {
    fiberTag = ClassComponent;
  } else if (typeof type === 'string') {
    // html 原生组件处理逻辑,确定 fiberTag 类型
  } else {
    // 处理一些特殊类型,比如 FRAGMENT,SUSPENSE,FORWARD,这里会直接返回对应的 FiberNode
  }

  // 这个方法就是调构造函数 new FiberNode(tag, pendingProps, key, mode)
  const fiber = createFiber(fiberTag, pendingProps, key, mode);
  fiber.elementType = type;
  fiber.type = resolvedType;
  fiber.lanes = lanes;
  return fiber;
}

这个过程有两个属性不知道哪来的,mode 和 lanes。这两个属性有很大的关联,会在讲 Scheduler 时提到。

节点类型

这一步最重要的就一件事: 确定 FiberNode 的 tag 类型,后续 render 和 commit 阶段会针对不同 tag 的 FiberNode 做详细处理。最常见的 tag 包括如下:

  • HostComponent: DOM 元素节点。
  • FunctionComponent: 函数组件节点。
  • ClassComponent: 类组件节点。

DOM 元素相关的 tag 类型:

  • HostRoot: 根 DOM 节点。
  • HostPortal: Portal 节点(通过 Portal 可以将节点插在 DOM 的任意位置)。

钩子 useContext 相关的 tag 类型:

  • ContextConsumer: Context 消费者节点
  • ContextProvider: Context 提供者节点

钩子 useRef 相关的 tag 类型:

  • ForwardRef: ForwardRef 组件节点

钩子 memo 相关的 tag 类型:

  • MemoComponent: Memo 组件节点
  • SimpleMemoComponent: 简单 Memo 组件节点

其他 tag 类型:

  • LazyComponent: 懒加载组件节点
  • SuspenseComponent: Suspense 组件节点
  • Fragment: Fragment 组件节点
  • Mode: 是一个 Mode
  • Profiler: 是一个 Profiler
  • ScopeComponent: 是一个 Scope

这个 tag 非常重要,在 FiberNode 的生命周期中,会针对不同的 FiberNode 做特殊处理。

DOM 结构相关属性

DOM 接收相关的属性有四个,前三个属性: return, child, sibling 可以确定 DOM 数结构。在 children 是列表的情况下, index 可以帮助我们快速定位子节点位置。假如有如下 DOM 结构:

const App:React.FC = () => {
  const list = [1, 2, 3];

  return (
    <div>
      list:
      <div>
        {list.map((item) => (
          <span key={item}>{item}</span>
        ))}
      </div>
    </div>
  )
}

它对应的 FiberNode 结构为:

这样在任一节点上,我们都能找到其邻近节点(左节点比较难找)。

FiberRootNode

FiberRootNode 是指 React 应用程序的根节点,FiberRootNodeFiberNode 不同,它所持有的属性和整个 Fiber 树关联,它是存储与管理 FiberNode 的容器(✨约47行):

function FiberRootNode(
  containerInfo: any,
  tag,
  hydrate: any,
  identifierPrefix: any,
  onRecoverableError: any,
  formState: ReactFormState<any, any> | null,
) {
  this.tag = tag;                     // 一般为 Root
  this.containerInfo = containerInfo; // React 应用程序的容器元素,即将要挂载或已经挂载的 DOM 元素
  this.pendingChildren = null;        // 下一次渲染的子元素
  this.current = null;                // 当前 Fiber 树的根节点
  this.pingCache = null;              // 缓存 FiberNode 的引用,用于快速访问
  this.finishedWork = null;           // 上一次完成的 Fiber 树的根节点
  this.timeoutHandle = noTimeout;     // 处理超时的计时器句柄
  this.cancelPendingCommit = null;    // 用于取消挂起的提交操作的函数
  this.context = null;                // 当前的上下文对象
  this.pendingContext = null;         // 下一次渲染的上下文对象
  this.next = null;                   // 下一个 FiberRootNode 对象
  this.callbackNode = null;           // 回调相关的节点
  this.callbackPriority = NoLane;     // 回调的优先级
  this.expirationTimes = createLaneMap(NoTimestamp); // 各个 Lane 的过期时间

  /** Lane 相关 */
  this.pendingLanes = NoLanes;        // 待处理的 Lane
  this.suspendedLanes = NoLanes;      // 被挂起的 Lane
  this.pingedLanes = NoLanes;         // 需要处理的 Lane
  this.expiredLanes = NoLanes;        // 已过期的 Lane
  this.finishedLanes = NoLanes;       // 已完成的 Lane
  this.errorRecoveryDisabledLanes = NoLanes;  // 禁用错误恢复的 Lane
  this.entangledLanes = NoLanes;      // 相关联的 Lane
  this.entanglements = createLaneMap(NoLanes);  // 相关联的 Lane 的映射
  this.hiddenUpdates = createLaneMap(null);     // 隐藏更新的 Lane 的映射
  this.shellSuspendCounter = 0;       // 挂起计数器

  this.identifierPrefix = identifierPrefix;     // 标识符前缀
  this.onRecoverableError = onRecoverableError; // 可恢复错误的回调函数

  if (enableCache) {
    this.pooledCache = null;          // 缓存的 FiberNode
    this.pooledCacheLanes = NoLanes;  // 缓存的 Lane
  }
  if (enableSuspenseCallback) {
    this.hydrationCallbacks = null;   // 用于注水的回调函数
  }

  this.formState = formState;         // 表单状态
  this.incompleteTransitions = new Map(); // 未完成的过渡的映射
}

这里有很多优先级,上下文相关的属性,我们会在后文中一一解释,只需要知道这些属性是从 FiberRootNode 上来的就行了。

构建过程

FiberRootNode 源于 createRoot 这个方法(✨约153行

export function createRoot(
  container: Element | Document | DocumentFragment,
  options?: CreateRootOptions,
): RootType {
  if (!isValidContainer(container)) {
    throw new Error('xxx');
  }

  /** 下面这部分属性会从 options 决定 */
  let isStrictMode = false;       // 严格模式
  let concurrentUpdatesByDefaultOverride = false;     // 是否覆盖默认的并发更新模式
  let identifierPrefix = '';      // 用于生成唯一标识符的前缀
  let onRecoverableError = defaultOnRecoverableError; // 处理可恢复的错误的回调
  let transitionCallbacks = null; // 用于存储过渡的回调函数

  if (options !== null && options !== undefined) {
    if (options.unstable_strictMode === true) {
      isStrictMode = true;
    }
    if (
      allowConcurrentByDefault &&
      options.unstable_concurrentUpdatesByDefault === true
    ) {
      concurrentUpdatesByDefaultOverride = true;
    }
    if (options.identifierPrefix !== undefined) {
      identifierPrefix = options.identifierPrefix;
    }
    if (options.onRecoverableError !== undefined) {
      onRecoverableError = options.onRecoverableError;
    }
    if (options.unstable_transitionCallbacks !== undefined) {
      transitionCallbacks = options.unstable_transitionCallbacks;
    }
  }

  const root = createContainer(
    container,
    ConcurrentRoot, // 默认开启并发模式,React16,17 会使用 LegacyRoot 模式 
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
  );

  // 给 #root DOM 元素设置一个内部属性,存储 root.current
  markContainerAsRoot(root.current, container);
  Dispatcher.current = ReactDOMClientDispatcher;

  const rootContainerElement: Document | Element | DocumentFragment =
    container.nodeType === COMMENT_NODE
      ? (container.parentNode: any)
      : container;
  listenToAllSupportedEvents(rootContainerElement);

  // 创建ReactDOMRoot实例对象并返回 将root应用根节点对象 存储为ReactDOM的内部对象
  return new ReactDOMRoot(root);
}

这个过程有三个重点步骤:

  1. 调用 createContainer,创建root应用根节点对象。
  2. #root 应用容器元素上监听所有事件
  3. 创建一个 ReactDOMRoot 实例对象并返回,这就是 createRoot 方法最后返回的root对象。

createContainer

createContainer 会返回一个 FiberRootNode✨约245行):

export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
  identifierPrefix: string,
  onRecoverableError: (error: mixed) => void,
  transitionCallbacks: null | TransitionTracingCallbacks,
): OpaqueRoot {
  const hydrate = false;  // SSR
  const initialChildren = null;
  return createFiberRoot(
    containerInfo,
    tag,
    hydrate,
    initialChildren,
    hydrationCallbacks,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
    null,
  );
}
export function createFiberRoot(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
  initialChildren: ReactNodeList,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
  identifierPrefix: string,
  onRecoverableError: null | ((error: mixed) => void),
  transitionCallbacks: null | TransitionTracingCallbacks,
  formState: ReactFormState<any, any> | null,
): FiberRoot {
  const root: FiberRoot = (new FiberRootNode(
    containerInfo,
    tag,
    hydrate,
    identifierPrefix,
    onRecoverableError,
    formState,
  ): any);

  // 创建一个根节点并让 root.current 引用它
  const uninitializedFiber = createHostRootFiber(
    tag,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
  );
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  if (enableCache) {
    const initialCache = createCache();
    retainCache(initialCache);
    root.pooledCache = initialCache;
    retainCache(initialCache);
    const initialState: RootState = {
      element: initialChildren,
      isDehydrated: hydrate,
      cache: initialCache,
    };
    uninitializedFiber.memoizedState = initialState;
  } else {
    const initialState: RootState = {
      element: initialChildren,
      isDehydrated: hydrate,
      cache: (null: any), // not enabled yet
    };
    uninitializedFiber.memoizedState = initialState;
  }

  // 初始化HostRootFiber根节点对象的updateQueue属性
  initializeUpdateQueue(uninitializedFiber);
  return root;
}

这个过程就是创建了两个东西: 一个 FiberRootNode 类型的 root 对象最终返回,一个 hostRoot 根节点。下面一篇文章会讲这两个 root 有什么区别。

updateContainer

前面说过 FiberRootNodeFiberNode 的容器,同时负责管理 FiberNode。那么当我们需要更新 DOM 结构时,就会调用对应的更新函数(✨约321行):

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  const current = container.current;
  // 1. 获取更新优先级与上下文
  const lane = requestUpdateLane(current);
  const context = getContextForSubtree(parentComponent);
  container.context === null ? container.context = context : container.pendingContext = context

  // 2. 创建一个update更新对象 
  const update = createUpdate(lane);
  update.payload = {element};

  callback = callback ?? null;
  callback !== null && update.callback = callback;

  // 3. 将 update 对象添加到目标Fiber对象的更新队列中
  const root = enqueueUpdate(current, update, lane);

  // 4. 开启一个新的调度更新任务
  if (root !== null) {
    scheduleUpdateOnFiber(root, current, lane);
    entangleTransitions(root, current, lane);
  }
  return lane;
}

这里我们看一下 update 更新对象:

export function createUpdate(lane: Lane): Update<mixed> {
  const update: Update<mixed> = {
    lane,
    tag: UpdateState, // 是个 number 类型的标志
    payload: null,  // 更新的内容
    callback: null, // 回调函数
    next: null,     // 指向下一个更新
  };
  return update;
}

简单看一下 tag:

// 1,默认情况:通过ReactDOM.createRoot或者this.setState触发
export const UpdateState = 0;
// 2,在classCompont组件生命周期函数中使用this.setState触发更新
export const ReplaceState = 1;
// 3,通过this.forceUpdate触发
export const ForceUpdate = 2;
// 4,发生错误的情况下在classComponent或者HostRoot中触发更新
export const CaptureUpdate = 3;

这个 update 对象最终会被塞到 FiberNodeupdateQueue 中(✨约225行):

export function enqueueUpdate<State>(
  fiber: Fiber,
  update: Update<State>,
  lane: Lane,
): FiberRoot | null {
  const updateQueue = fiber.updateQueue;
  if (updateQueue === null) { // 这表示 FiberNode 已经不存在 (unmount) 了
    return null;
  }

  const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;

  if (isUnsafeClassRenderPhaseUpdate(fiber)) {
    // 不安全,直接添加到更新中队列,以便在当前渲染期间处理
    const pending = sharedQueue.pending;
    if (pending === null) {
      update.next = update;
    } else {
      update.next = pending.next;
      pending.next = update;
    }
    sharedQueue.pending = update;
    return unsafe_markUpdateLaneFromFiberToRoot(fiber, lane);
  } else {
    // 安全,添加到 queue 中
    return enqueueConcurrentClassUpdate(fiber, sharedQueue, update, lane);
  }
}