React18 源码分析(一):React的createRoot

180 阅读4分钟

新特性 React 18 引入了一些新的特性和API,其中包括了createRootrender函数的改变。

应用入口

import ReactDOM from 'react-dom/client';
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);

createRoot

// packages/react-dom/src/client/ReactDOM.js
function createRoot(
  container: Element | DocumentFragment,
  options?: CreateRootOptions,
): RootType {
  // 源码中出现的__DEV__的代码可以选择不看,是开发环境出现的警告或者错误
  if (__DEV__) {
    if (!Internals.usingClientEntryPoint) {
      console.error(
        'You are importing createRoot from "react-dom" which is not supported. ' +
          'You should instead import it from "react-dom/client".',
      );
    }
  }
  // 这个函数即是下面的createRoot
  return createRootImpl(container, options);
}
// packages/react-dom/src/client/ReactDOM.js
export function createRoot(
  container: Element | DocumentFragment,
  options?: CreateRootOptions,
): RootType {
  // 对传入的`container`,即`#root`进行校验,校验失败则抛出异常。
  if (!isValidContainer(container)) {
    throw new Error('createRoot(...): Target container is not a DOM element.');
  }

  warnIfReactDOMContainerInDEV(container);

  // 初始化一些默认的状态,React18默认没有开启严格模式,需要自己使用`<StrictMode>`组件包裹`<App>`根组件。
  let isStrictMode = false;
  let concurrentUpdatesByDefaultOverride = false;
  let identifierPrefix = '';
  let onRecoverableError = defaultOnRecoverableError;
  let transitionCallbacks = null;

  if (options !== null && options !== undefined) {
    if (__DEV__) {
      if ((options: any).hydrate) {
        console.warn(
          'hydrate through createRoot is deprecated. Use ReactDOMClient.hydrateRoot(container, <App />) instead.',
        );
      } else {
        if (
          typeof options === 'object' &&
          options !== null &&
          (options: any).$$typeof === REACT_ELEMENT_TYPE
        ) {
          console.error(
            'You passed a JSX element to createRoot. You probably meant to ' +
              'call root.render instead. ' +
              'Example usage:\n\n' +
              '  let root = createRoot(domContainer);\n' +
              '  root.render(<App />);',
          );
        }
      }
    }
    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.transitionCallbacks !== undefined) {
      transitionCallbacks = options.transitionCallbacks;
    }
  }

  // 创建容器对象 `FiberRootNode`
  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
  );
  markContainerAsRoot(root.current, container);

  // 事件监听处理
  const rootContainerElement: Document | Element | DocumentFragment =
    container.nodeType === COMMENT_NODE
      ? (container.parentNode: any)
      : container;
  // 给`#root`根元素绑定了所有事件,任何子孙元素触发的该类型事件都会委托给【根元素的事件回调】处理
  listenToAllSupportedEvents(rootContainerElement);
  
  // 创建ReactDOMRoot实例对象并返回 将root应用根节点对象 存储为ReactDOM的内部对象
  return new ReactDOMRoot(root);
}
function ReactDOMRoot(internalRoot: FiberRoot) {
   // 存储为ReactDOM的内部对象
  this._internalRoot = internalRoot;
}

createRoot方法中主要有三个重点逻辑处理:

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

createContainer

// packages/react-reconciler/src/ReactFiberReconciler.old.js
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 {
  // hydrate代表ssr,默认为false
  const hydrate = false;
  const initialChildren = null;
  return createFiberRoot(
    containerInfo,
    tag,
    hydrate,
    initialChildren,
    hydrationCallbacks,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks,
  );
}

createFiberRoot

// packages/react-reconciler/src/ReactFiberRoot.old.js
export function createFiberRoot(
  containerInfo: any,
  tag: RootTag,
  hydrate: boolean,
  initialChildren: ReactNodeList,
  hydrationCallbacks: null | SuspenseHydrationCallbacks,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
  // TODO: We have several of these arguments that are conceptually part of the
  // host config, but because they are passed in at runtime, we have to thread
  // them through the root constructor. Perhaps we should put them all into a
  // single type, like a DynamicHostConfig that is defined by the renderer.
  identifierPrefix: string,
  onRecoverableError: null | ((error: mixed) => void),
  transitionCallbacks: null | TransitionTracingCallbacks,
): FiberRoot {
   // 创建 root 应用根节点对象 FiberRootNode
  const root: FiberRoot = (new FiberRootNode(
    containerInfo,
    tag,
    hydrate,
    identifierPrefix,
    onRecoverableError,
  ): any);
  if (enableSuspenseCallback) {
    root.hydrationCallbacks = hydrationCallbacks;
  }

  if (enableTransitionTracing) {
    root.transitionCallbacks = transitionCallbacks;
  }

  // Cyclic construction. This cheats the type system right now because
  // stateNode is any.
  // 创建一个 tag 类型为 ConcurrentRoot 的FiberNode对象 HostRootFiber,它是Fiber树的根节点。
  const uninitializedFiber = createHostRootFiber(
    tag,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
  );
  // 关联起来,以便在后续的渲染过程中能够正确地处理该组件树的更新和重新渲染
  // 将 root 应用根节点对象的current属性指向了当前Current Fiber Tree 组件树的根节点 HostRootFiber 
  root.current = uninitializedFiber;
  // 然后将HostRootFiber.stateNode属性指向root应用根节点对象
  uninitializedFiber.stateNode = root;

  // 有缓存
  if (enableCache) {
    const initialCache = createCache();
    retainCache(initialCache);

    // The pooledCache is a fresh cache instance that is used temporarily
    // for newly mounted boundaries during a render. In general, the
    // pooledCache is always cleared from the root at the end of a render:
    // it is either released when render commits, or moved to an Offscreen
    // component if rendering suspends. Because the lifetime of the pooled
    // cache is distinct from the main memoizedState.cache, it must be
    // retained separately.
    root.pooledCache = initialCache;
    retainCache(initialCache);
    const initialState: RootState = {
      element: initialChildren,
      isDehydrated: hydrate,
      cache: initialCache,
      transitions: null,
    };
    uninitializedFiber.memoizedState = initialState;
  } else {
  // 没有缓存
    const initialState: RootState = {
      element: initialChildren,
      isDehydrated: hydrate,
      cache: (null: any), // not enabled yet
      transitions: null,
    };
    // 初始化数据
    uninitializedFiber.memoizedState = initialState;
  }
  // 初始化 HostRootFiber 根节点对象的 updateQueue 属性
  initializeUpdateQueue(uninitializedFiber);

  // 返回创建的 FiberRootNode
  return root;
}

FiberRootNode

// pakages/react-reconciler/src/ReactFiberRoot.old.js
function FiberRootNode(
  containerInfo,
  tag,
  hydrate,
  identifierPrefix,
  onRecoverableError,
) {
  this.tag = tag; // 应用模式: 1 ConcurrentRoot 表示并发渲染模式
  this.containerInfo = containerInfo; // containerInfo 是页面的根节点,也是真实节点 root
  this.pendingChildren = null;
  this.current = null; // 指向 Current Fiber Tree 的根节点 也就是HostFiberRoot
  this.pingCache = null;
  this.finishedWork = null; // 存储创建完成的 FiberTree
  this.timeoutHandle = noTimeout;
  this.context = null;
  this.pendingContext = null;
  this.callbackNode = null; // 回调节点,存储当前任务 task
  this.callbackPriority = NoLane; // 回调任务优先级
  this.eventTimes = createLaneMap(NoLanes);
  this.expirationTimes = createLaneMap(NoTimestamp);

  this.pendingLanes = NoLanes; // 默认 0
  this.suspendedLanes = NoLanes;
  this.pingedLanes = NoLanes;
  this.expiredLanes = NoLanes;
  this.mutableReadLanes = NoLanes;
  this.finishedLanes = NoLanes;

  this.entangledLanes = NoLanes;
  this.entanglements = createLaneMap(NoLanes);

  this.identifierPrefix = identifierPrefix;
  this.onRecoverableError = onRecoverableError;

  if (enableCache) {
    this.pooledCache = null;
    this.pooledCacheLanes = NoLanes;
  }

  if (supportsHydration) {
    this.mutableSourceEagerHydrationData = null;
  }

  if (enableSuspenseCallback) {
    this.hydrationCallbacks = null;
  }

  if (enableTransitionTracing) {
    this.transitionCallbacks = null;
    const transitionLanesMap = (this.transitionLanes = []);
    for (let i = 0; i < TotalLanes; i++) {
      transitionLanesMap.push(null);
    }
  }

  if (enableProfilerTimer && enableProfilerCommitHooks) {
    this.effectDuration = 0;
    this.passiveEffectDuration = 0;
  }

  if (enableUpdaterTracking) {
    this.memoizedUpdaters = new Set();
    const pendingUpdatersLaneMap = (this.pendingUpdatersLaneMap = []);
    for (let i = 0; i < TotalLanes; i++) {
      pendingUpdatersLaneMap.push(new Set());
    }
  }

  if (__DEV__) {
    switch (tag) {
      case ConcurrentRoot:
        this._debugRootType = hydrate ? 'hydrateRoot()' : 'createRoot()';
        break;
      case LegacyRoot:
        this._debugRootType = hydrate ? 'hydrate()' : 'render()';
        break;
    }
  }
}
  • createHostRootFiber
 // packages/react-reconciler/src/ReactFiber.old.js
export function createHostRootFiber(
  tag: RootTag,
  isStrictMode: boolean,
  concurrentUpdatesByDefaultOverride: null | boolean,
): Fiber {
  let mode;
  if (tag === ConcurrentRoot) {
    mode = ConcurrentMode;
    if (isStrictMode === true) {
      mode |= StrictLegacyMode;

      if (enableStrictEffects) {
        mode |= StrictEffectsMode;
      }
    } else if (enableStrictEffects && createRootStrictEffectsByDefault) {
      mode |= StrictLegacyMode | StrictEffectsMode;
    }
    if (
      // We only use this flag for our repo tests to check both behaviors.
      // TODO: Flip this flag and rename it something like "forceConcurrentByDefaultForTesting"
      !enableSyncDefaultUpdates ||
      // Only for internal experiments.
      (allowConcurrentByDefault && concurrentUpdatesByDefaultOverride)
    ) {
      mode |= ConcurrentUpdatesByDefaultMode;
    }
  } 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;
  }

  // 返回 WorkTag 为 HostRoot = 3; 的 HostRootFiber
  return createFiber(HostRoot, null, null, mode);
}
  • createFiber
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; // 节点key
  this.elementType = null; // 大部分情况同type,存储组件对象Element
  this.type = null; // 组件原始定义
  // 存储FiberNode对象对应的dom元素 hostComponent,
  // 函数组件此属性无值,
  // 类组件此属性存储的是组件实例 instance
  this.stateNode = null;

  // Fiber
  // FiberNode 节点之间的链接
  this.return = null; // 指向父级节点对象 FiberNode
  this.child = null; // 指向第一个子节点 FiberNode
  this.sibling = null; // 指向下一个兄弟节点 FiberNode
  this.index = 0;

  this.ref = null; // ref引用

  // hooks 相关
  this.pendingProps = pendingProps; // 新的,等待处理的 props
  this.memoizedProps = null; // 旧的,上一次存储的 props
  this.updateQueue = null; // 存储 update 更新对象链表
  this.memoizedState = null; // 类组件,旧的,上一次存储的state;函数组件:存储hook链表
  this.dependencies = null;

  this.mode = mode; // 存储的是当前应用渲染模式:默认是 concurrent mode

  // Effects
  // 各种 effect 副作用相关的执行标记
  this.flags = NoFlags;
  this.subtreeFlags = NoFlags; // 子树节点的副作用标记,默认无副作用
  this.deletions = null;// 删除标记

  // 优先级调度,默认为 0 
  this.lanes = NoLanes; // 节点自身的更新 Lanes
  this.childLanes = NoLanes; // 子树节点的更新 Lanes

  // 这个属性指向另外一个缓冲区对应的 FiberNode
  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;
  }

  if (__DEV__) {
    // This isn't directly used but is handy for debugging internals:

    this._debugSource = null;
    this._debugOwner = null;
    this._debugNeedsRemount = false;
    this._debugHookTypes = null;
    if (!hasBadMapPolyfill && typeof Object.preventExtensions === 'function') {
      Object.preventExtensions(this);
    }
  }
}

FiberRootNode是React应用的根节点(即#root容器节点),而HostRootFiber是虚拟DOM树的根节点,类型是FiberNode

FiberRootNode.current = HostRootFiber.alternate = HostRootFiber

下面的总结摘自一个大佬的博客。

FiberVDOM是在React源码中的实现,FiberNode即虚拟dom,在Vue源码中定义为VNode

在React源码中有两种Fiber节点定义:FiberRootNodeFiberNode,但实际上我们说的Fiber节点是指的普通的FiberNode,我们从上面两个构造函数的定义可以发现,它们的实例属性完全不同,因为它们本来就是不一样的作用。

  • FiberRootNode:是针对React应用挂载的容器元素#root创建的一个对象,它是应用根节点。它的作用是负责应用加载相关的内容【应用级】,比如应用加载模式mode,存储本次应用更新的回调任务以及优先级,存储创建完成的FiberTree等。

  • FiberNode:这才是针对普通DOM元素或者组件创建的Fiber对象,是虚拟DOM的真实体现,它的属性存储了元素的或者组件的类型,对应的DOM信息,以及数据State和组件更新的相关信息,所以FiberNode才是Fiber架构的核心。

  • initializeUpdateQueue

// packages/react-reconciler/src/ReactUpdateQueue.old.js
// 初始化一个 Fiber 对象的 updateQueue 属性
export function initializeUpdateQueue<State>(fiber: Fiber): void {
  const queue: UpdateQueue<State> = {
    baseState: fiber.memoizedState,
    firstBaseUpdate: null,
    lastBaseUpdate: null,
    shared: {
      pending: null,
      interleaved: null,
      lanes: NoLanes,
    },
    effects: null,
  };
  // 设置更新队列对象
  fiber.updateQueue = queue;
}

初始化HostFiber节点的updateQueue属性,为一个更新队列对象。

总结createContainer方法:

  • 创建FiberRootNode应用根节点对象。
  • 创建HostFiber虚拟DOM树根节点对象。
  • 关联两个对象,可以互相方位。
  • 初始化HostFiberupdateQueue属性。
  • 返回FiberRootNode节点。

总结

createRoot方法主要的作用是react应用加载做准备,初始化根对象和一些基础信息,最后返回了一个ReactDOMRoot对象,而render方法定义于ReactDOMRoot对象上。 下一节看看render方法做了什么。

// packages/react-dom/src/client/ReactDOMRoot.js
ReactDOMHydrationRoot.prototype.render = ReactDOMRoot.prototype.render = function(
  children: ReactNodeList,
): void {
  const root = this._internalRoot;
  if (root === null) {
    throw new Error('Cannot update an unmounted root.');
  }

  if (__DEV__) {
    if (typeof arguments[1] === 'function') {
      console.error(
        'render(...): does not support the second callback argument. ' +
          'To execute a side effect after rendering, declare it in a component body with useEffect().',
      );
    } else if (isValidContainer(arguments[1])) {
      console.error(
        'You passed a container to the second argument of root.render(...). ' +
          "You don't need to pass it again since you already passed it to create the root.",
      );
    } else if (typeof arguments[1] !== 'undefined') {
      console.error(
        'You passed a second argument to root.render(...) but it only accepts ' +
          'one argument.',
      );
    }

    const container = root.containerInfo;

    if (container.nodeType !== COMMENT_NODE) {
      const hostInstance = findHostInstanceWithNoPortals(root.current);
      if (hostInstance) {
        if (hostInstance.parentNode !== container) {
          console.error(
            'render(...): It looks like the React-rendered content of the ' +
              'root container was removed without using React. This is not ' +
              'supported and will cause errors. Instead, call ' +
              "root.unmount() to empty a root's container.",
          );
        }
      }
    }
  }
  updateContainer(children, root, null, null);
};