react源码阅读-react-domRender

本文主要探讨React DOM的入口函数及数据类型,重点关注react-dom Render函数的源码。在React 16.8.6版本中,我们分析了ReactDOM.render()的逻辑结构,包括清理容器、创建DOM节点、构建root实例等关键步骤。通过源码阅读,揭示React应用中root节点、fiber节点及其属性的重要性,为后续理解React更新机制奠定基础。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

  react-dom入口函数以及基本数据类型。阅读React包的源码版本为16.8.6

  在第一章节我们了解到,react包本质上是一个数据结构建立的抽象屏障,提供起来供react的其它包,诸如react-domreact-native调用。在这一章中,进入react-dom的源码阅读。

  根据package.jsonmain字段入口,我们可以找到react-dom的入口文件为src/client/ReactDOM.js。我们发现该文件最后的代码export default ReactDOM仅仅对外暴露了一个对象模块。我们简单看一下这个对象模块。

// 函数内部代码均先省略
const ReactDOM: Object = {
  createPortal,
  findDOMNode() {  },
  hydrate() {},
  render() {},
  unstable_renderSubtreeIntoContainer() {},
  unmountComponentAtNode() {},
  unstable_createPortal() {},
  unstable_interactiveUpdates() {},
  unstable_discreteUpdates,
  unstable_flushDiscreteUpdates,
  flushSync,
  unstable_createRoot,
  unstable_createSyncRoot,
  unstable_flushControlled,
}

  其实这里的对象模块就是对面暴露的react-dom提供的Api部分。我们可以看到包括最熟悉的render方法,用于服务端渲染的hydrate,还有findDOMNodecreatePortal等。

  我们本章节就来查看下最常使用的render函数的源码大体逻辑结构。

// 调用方式 ReactDOM.render(element, container[, callback])
render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {
    // 判断dom节点是否正确
    invariant(
      isValidContainer(container),
      'Target container is not a DOM element.',
    );
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  }

  react-dom源码中使用了flow来定义数据类型,函数入参中如element: React$Element<any>这种写法就是flow的语法。近似于typescript

  render函数在除去DEV调试部分逻辑后,剩余的代码非常简单,判断传入的container节点是否为Dom节点,是就进入legacyRenderSubtreeIntoContainer函数,我们来跟着代码接着看。

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: DOMContainer,
  // 是否复用dom节点,服务端渲染调用
  forceHydrate: boolean,
  callback: ?Function,
) {
  // 从 container 中获得root节点
  let root: _ReactSyncRoot = (container._reactRootContainer: any);
  let fiberRoot;
  if (!root) {
    // 没有root,创建root节点, 移除所有子节点
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;
    // 有无callback
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    unbatchedUpdates(() => {
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;
    // 有无callback 逻辑同上
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Update
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}

  legacyRenderSubtreeIntoContainer首先取出container中的root节点,根据有无root节点来划分不同的创建更新逻辑。首次使用render函数的时候是不存在root节点的,此时通过legacyCreateRootFromDOMContainer创建一个root节点给container._reactRootContainer。然后如果存在callback就进行调用,最后进行了一个unbatchedUpdates。存在root节点的时候,就省去了创建root节点部分的代码,直接进行callback的判断和updateContainer

  我们先来看创建root节点的legacyCreateRootFromDOMContainer部分的代码。

function legacyCreateRootFromDOMContainer(
  container: DOMContainer,
  forceHydrate: boolean,
): _ReactSyncRoot {
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  // 不需要进行 shouldHydrate 过程,即我们正常的render过程
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    // 当有子节点的时候,一直循环,删除完子节点
    while ((rootSibling = container.lastChild)) {
      container.removeChild(rootSibling);
    }
  }
  // Legacy roots are not batched.
  /**
   * LegacyRoot 为一个常量标识符,具体细节如下
   * export type RootTag = 0 | 1 | 2;
   * export const LegacyRoot = 0;
   * export const BatchedRoot = 1;
   * export const ConcurrentRoot = 2;
   */
  return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
}

  前面提到过,forceHydrate这个布尔值是用于标识是否是服务端渲染的,在浏览器环境下是不触碰这部分的逻辑的,这个相关部分就先跳过。那么legacyCreateRootFromDOMContainer就做了两件事情:

  1. 删除container容器部分的所有子节点。这也就是为什么我们使用ReactDom.render渲染在目标节点之后,节点的子元素全部消失的原因。
  2. 返回了ReactSyncRoot类,实例化了一个root根节点的实例。

  接下来的ReactSyncRoot代码更简单:

function ReactSyncRoot(
  container: DOMContainer,
  tag: RootTag,
  hydrate: boolean,
) {
  // Tag is either LegacyRoot or Concurrent Root
  const root = createContainer(container, tag, hydrate);
  this._internalRoot = root;
}

  我们追寻createContainer函数,发现这个函数文件在react-reconciler/src/ReactFiberReconciler包中。我们跟着去查看一下:

export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
): OpaqueRoot {
  return createFiberRoot(containerInfo, tag, hydrate);
}

// 在 `react-reconciler/src/ReactFiberRoot`文件中
export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
): FiberRoot {
  const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);

  // Cyclic construction. This cheats the type system right now because
  // stateNode is any.
  const uninitializedFiber = createHostRootFiber(tag);
  // 相互指
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  return root;
}

// fiber root 结构的真身
function FiberRootNode(containerInfo, tag, hydrate) {
  this.tag = tag;
  // root 节点对应的Fiber对象
  this.current = null;
  // dom 节点
  this.containerInfo = containerInfo;
  // 持久化更新会用到
  this.pendingChildren = null;
  this.pingCache = null;
  this.finishedExpirationTime = NoWork;
  this.finishedWork = null;
  this.timeoutHandle = noTimeout;
  this.context = null;
  this.pendingContext = null;
  this.hydrate = hydrate;
  this.firstBatch = null;
  this.callbackNode = null;
  this.callbackExpirationTime = NoWork;
  this.firstPendingTime = NoWork;
  this.lastPendingTime = NoWork;
  this.pingTime = NoWork;

  if (enableSchedulerTracing) {
    this.interactionThreadID = unstable_getThreadID();
    this.memoizedInteractions = new Set();
    this.pendingInteractionMap = new Map();
  }
}

  终于在FiberRootNode中发现了rootRoot的真身,就是一个带标识的对象。其中比较重要的一个为containerInfo,就是reactElement将要渲染上的容器节点信息。我们还能发现,很多标识赋值了NoWorkNoWork设计到后续我们更新会提及的ExpirationTime的概念,是React更新算法的基础。目前你可以就把NoWork理解为一个标识0的常量(源码export const NoWork = 0;)。

  我们最后来看current,在createFiberRoot中将其指向了createHostRootFiber创建的uninitializedFiber。这个uninitializedFiber就是reactElement对应的fiber节点,我们一起来看一下这部分逻辑。

// 位于react-reconciler/src/ReactFiber.js
function createHostRootFiber(tag: RootTag): Fiber {
  let mode;
  // 根据 tag 的不同,获得不同的mode模式
  if (tag === ConcurrentRoot) {
    mode = ConcurrentMode | BatchedMode | StrictMode;
  } else if (tag === BatchedRoot) {
    mode = BatchedMode | StrictMode;
  } else {
    mode = NoMode;
  }

  if (enableProfilerTimer && isDevToolsPresent) {
    // Always collect profile timings when DevTools are present.
    // This enables DevTools to start capturing timing at any point–
    // Without some nodes in the tree having empty base times.
    mode |= ProfileMode;
  }

  return createFiber(HostRoot, null, null, mode);
}

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);
};

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;
  // pendingProps 将要更新
  this.pendingProps = pendingProps;
  // 之前的props
  this.memoizedProps = null;
  // update对象
  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) {
    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;
  }
}

  这部分逻辑比较长,我们来拆成两部来看。createHostRootFiber总共做了两件事情,根据tag存在的标识,调整了mode字段。然后使用mode字段创建了FiberNode对象。

  这里我们稍微提一下使用|&来进行一个打标的设计模式。比如我现在有三个属性的标识符a/b/c,我们用二进制来定义它们,保证每个模式1所在的位置不同。

var a = 0b001;
var b = 0b010;
var c = 0b100;

  我们现在对一个demo变量进行属性的赋值,比如我想要这个demo变量拥有属性a属性c。那我只需要var demo = a | c。在后续我对demo进行一个拥有属性判断的时候,我只需要使用&,如果得到的结果大于0,即转换为true,就说明demo拥有该属性。如我想要判断demo是否含有a属性,只需要if (demo | a) { /* ... */ }即可。如果我想要给demo添加一个属性,比如添加属性b,只需要将demo |= b即可,如果不是很了解一元操作符的同学,可以去mdn上面查一下相关的资料就能明白。

  我们前面在legacyCreateRootFromDOMContainer函数的注释中提到过,rootTag是通过LegacyRoot | BatchedRoot | ConcurrentRoot取得的三个模式的综合。所以createHostRootFiber这里我们走的是最后一个else分支,mode=NoMode。然后创建Fiber节点。

  Fiber节点就是对应每一个ReactElement的节点了,它上面记载了很多我们熟悉的属性,比如ref,比如props相关的pendingPropsmemoizedProps。然后还需要关注一下的概念就是expirationTimeexpirationTime前面root的时候也提到了,这是节点更新操作的依据,在后续的源码部分也会单独拆分一章节来阐述它。

  还需要提一下的是我注释了Fiber相关的几个属性sibling,return,childreact中的Fiber节点对应的是一个单向列表结构。比如我有这样的一个jsx结构:

function Demo() {
  return (
    <ul>
      <li></li>
      <li></li>
      <li></li>
    </ul>
  );
}

  那么这个结构在Fiber中会这样存在ul.child -> li(1).sibling -> li(2).sibling -> li(3)。每个节点的return则对应总的父节点li(1).return -> ul

  这一章当中,我们简单看了一下ReactDom.render的总体函数逻辑和创建数据结构部分的源码。首次创建的时候,render会创建一个FiberRootNode对象,该对象作为整个React应用的根节点,同时给RootNode创建对应的Fiber对象。每一个Fiber对象对应一个ReactElement元素,它带有各种用于React调度的属性元素,DOM以单向链表的数据结存在于React应用当中。

  下一章我们会接着render函数的逻辑进入unbatchedUpdates部分代码,大体介绍一下React-dom在更新中的一些框架设计。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值