React源码分析(二)=> Reac初次渲染分析

本文详细剖析了React的首次渲染过程,从`render`阶段的`legacyRenderSubtreeIntoContainer`开始,逐步深入到`updateContainer`、`scheduleWork`、`performSyncWork`和`commitRoot`等关键步骤。在`render`阶段,讲解了如何创建`ReactRoot`、`FiberRoot`和`FiberNode`,以及如何调度和更新工作。在`commit`阶段,重点分析了`commitRoot`和`invokeGuardedCallback`,解释了在Dev环境下的错误处理。整个过程揭示了React如何协调组件树和DOM的同步,以及如何处理交互和性能优化。
摘要由CSDN通过智能技术生成

文章目录

本不该有4级标题, 没办法东西太多, 不加4级标题更不清晰.

继续上一节回来继续看代码, 本章还涉及到了work调度问题, 由于内容太多, 存在if (__DEV__)的判断语句, 本章选择性忽略.

由于React源码使用了flow的静态类型检查器,所以会有:类型这样的语法,对于熟悉Typescript的肯定很熟悉这样的写法,不懂的直接把类型忽略就好(例如第一行的ReactDOM:Object,后面的:Object就是ReactDOM的类型,有些类型是自定义的有些是框架自带的,render函数的参数里React$Element这种类型一看就是自定义的),没什么影响。


源码基于React 16.8.6, 内容极长, 必须配合源码一起来看

单步调试的例子: ReactDOM.render('hello', document.getElementById('root'));
下文基于这个例子来一步一步分析, 你要是有什么见解可以直接在下面的评论区留言, 我会及时查看.

1. render阶段 legacyRenderSubtreeIntoContainer

从下面的源码可以看到,调用了ReactDOM.render时首先执行了legacyRenderSubtreeIntoContainer函数

// 例
ReactDOM.render(<App />, document.getElementById('root'));


// packages\react-dom\src\client\ReactDOM.js
/**
 * nodeType 属性返回节点类型。
  如果节点是一个元素节点,nodeType 属性返回 1。
  如果节点是属性节点, nodeType 属性返回 2。
  如果节点是一个文本节点,nodeType 属性返回 3。
  如果节点是一个注释节点,nodeType 属性返回 8。
  如果节点是一个整个文档(DOM 树的根节点),nodeType 属性返回 9。
  如果节点是一个DocumentFragment,nodeType 属性返回 11。
  ELEMENT_NODE = 1,
  DOCUMENT_NODE = 9,
  DOCUMENT_FRAGMENT_NODE = 11,
  COMMENT_NODE = 8
 */
function isValidContainer(node) {
   
  return !!(
    node &&
    (node.nodeType === ELEMENT_NODE ||
      node.nodeType === DOCUMENT_NODE ||
      node.nodeType === DOCUMENT_FRAGMENT_NODE ||
      (node.nodeType === COMMENT_NODE &&
        node.nodeValue === ' react-mount-point-unstable '))
  );
}


const ReactDOM: Object = {
   
  render(
    element: React$Element<any>,
    container: DOMContainer,
    callback: ?Function,
  ) {
   
	invariant(
      isValidContainer(container),
      'Target container is not a DOM element.',
    );
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  },

下面来看下legacyRenderSubtreeIntoContainer函数内部

// packages\react-dom\src\client\ReactDOM.js
function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>, // 首次渲染, partComponet为null
  children: ReactNodeList, // children为使用React编写的组件
  container: DOMContainer, // container为真实DOM元素
  forceHydrate: boolean, // forceHydrate写死的false, true时为服务端渲染
  callback: ?Function,
) {
   
  let root: _ReactSyncRoot = (container._reactRootContainer: any);
  let fiberRoot;
  if (!root) {
   
    // Initial mount 初始化容器
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
   // !!!未完!!!

1.1. legacyCreateRootFromDOMContainer

legacyCreateRootFromDOMContainer这个函数主要是拿着真是DOM创建一个ReactRoot并返回, 见下方代码

// packages\react-dom\src\client\ReactDOM.js
function legacyCreateRootFromDOMContainer(
  container: DOMContainer, // 真实DOM元素
  forceHydrate: boolean, // 目前为false
): _ReactSyncRoot {
   
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  if (!shouldHydrate) {
   
    let warned = false;
    let rootSibling;
    while ((rootSibling = container.lastChild)) {
   
      // 删除跟节点内部元素
      container.removeChild(rootSibling);
    }
  }

  // Legacy roots are not batched.
  // 返回一个ReactRoot对象
  const isConcurrent = false;
  return new ReactRoot(container, isConcurrent, shouldHydrate);
}

1.1.1. ReactRoot

继续上个函数, 见下方代码:

// packages\react-dom\src\client\ReactDOM.js
function ReactRoot(
  container: DOMContainer, // 真实DOM元素
  tag: RootTag, // 0
  hydrate: boolean, // false
) {
   
  // Tag is either LegacyRoot or Concurrent Root
  const root = createContainer(container, tag, hydrate);
  this._internalRoot = root;
}

下面来看下createContainer函数

// packages\react-reconciler\src\ReactFiberReconciler.js
function createContainer(
  containerInfo: Container, // 真实DOM元素
  isConcurrent: boolean, // false
  hydrate: boolean, // false
): OpaqueRoot {
   
  return createFiberRoot(containerInfo, tag, hydrate);
}

// 直接返回的createFiberRoot函数, 继续

1.1.1.1. createFiberRoot

这个函数返回了一个FiberRoot

function createFiberRoot(
  containerInfo: any, // 真实DOM元素
  isConcurrent: boolean, // false
  hydrate: boolean, // false
): FiberRoot {
   
  // Cyclic construction. This cheats the type system right now because
  // stateNode is any.
  // Fiber Node
  const uninitializedFiber = createHostRootFiber(isConcurrent);

  let root;
  if (enableSchedulerTracing) {
   
  // react 19的alpha.0版本这里被抽成工厂函数了
    root = ({
    // 创建一个FiberRootNode
      current: uninitializedFiber, // 上面那个Fiber Node
      containerInfo: containerInfo, // 真实DOM元素
      pendingChildren: null,

      earliestPendingTime: NoWork, // 0
      latestPendingTime: NoWork, // 0
      earliestSuspendedTime: NoWork, // 0
      latestSuspendedTime: NoWork, // 0
      latestPingedTime: NoWork, // 0

      pingCache: null,

      didError: false,

      pendingCommitExpirationTime: NoWork, // 0
      finishedWork: null,
      timeoutHandle: noTimeout,
      context: null,
      pendingContext: null,
      hydrate,
      nextExpirationTimeToWorkOn: NoWork, // 0
      expirationTime: NoWork, // 0
      firstBatch: null,
      nextScheduledRoot: null,

      interactionThreadID: unstable_getThreadID(),
      memoizedInteractions: new Set(),
      pendingInteractionMap: new Map(),
    }: FiberRoot);
  } else {
   
// ...省略
  }

  uninitializedFiber.stateNode = root;

  return ((root: any): FiberRoot);
1.1.1.2. createHostRootFiber

这个函数返回一个FiberNode

// packages\react-reconciler\src\ReactFiber.js
function createHostRootFiber(isConcurrent: boolean): Fiber {
   
  // isConcurrent = false; ConcurrentMode = 0b001, StrictMode = 0b010, NoContext = 0b000
  let mode = isConcurrent ? ConcurrentMode | StrictMode : NoContext;

  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;
  }
  // HostRoot是常量, 值为3, mode = 0b000
  return createFiber(HostRoot, null, null, mode);
}	

// 下面来看下createFiber
// packages\react-reconciler\src\ReactFiber.js
const createFiber = function(
  tag: WorkTag, // 3
  pendingProps: mixed, // null
  key: null | string, // null
  mode: TypeOfMode, // 0
): Fiber {
   
  return new FiberNode(tag, pendingProps, key, mode);
};

// 继续
// packages\react-reconciler\src\ReactFiber.js
function FiberNode(
  tag: WorkTag, // 3
  pendingProps: mixed, // null
  key: null | string, // null
  mode: TypeOfMode, // 0
) {
   
  // Instance
  this.tag = tag; // 3
  this.key = key; // null
  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; // null
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.contextDependencies = null;

  this.mode = mode; // 0

  // Effects
  this.effectTag = NoEffect; // 0b000000000000
  this.nextEffect = null;

  this.firstEffect = null;
  this.lastEffect = null;

  this.expirationTime = NoWork; // 0
  this.childExpirationTime = NoWork; // 0

  this.alternate = null;

  if (enableProfilerTimer) {
   
    // Note: The following is done to avoid a v8 performance cliff.
	// 避免V8性能悬崖
    // 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;
  }

  // ...省略
}

截止到目前为止关于节点总结: (可以打开一个react项目, 选择跟节点的_reactRootContainer属性验证一下)

在这里插入图片描述


续1. legacyRenderSubtreeIntoContainer

接着最开始来,

// 接着1.legacyRenderSubtreeIntoContainer内部的代码
// packages\react-dom\src\client\ReactDOM.js

    // 此处的root是ReactRoot
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    if (typeof callback === 'function') {
   
      const originalCallback = callback;
      callback = function() {
   
        const instance = getPublicRootInstance(root._internalRoot);
        // 将函数this注入
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    unbatchedUpdates(() => {
   
      if (parentComponent != null) {
   
        root.legacy_renderSubtreeIntoContainer(
          parentComponent,
          children,
          callback,
        );
      } else {
   
        // 首次进到这里
        root.render(children, callback);
      }
    });
  } else {
   
	// 非首次暂时省略
  }
  return getPublicRootInstance(root._internalRoot);
}

1.2. root.render()

该函数在ReactRoot的原型链上, 下面来看一下代码:

function ReactWork() {
   
  this._callbacks = null;
  this._didCommit = false;
  this._onCommit = this._onCommit.bind(this); // ReactWork原型链上有这个函数, 暂时不看
}

ReactRoot.prototype.render = function(
  children: ReactNodeList, // children为使用React编写的组件
  callback: ?() => mixed,
): Work {
   
  const root = this._internalRoot; // FiberRoot
  const work = new ReactWork();
  callback = callback === undefined ? null : callback;
  if (__DEV__) {
   
    warnOnInvalidCallback(callback, 'render');
  }
  if (callback !== null) {
   
    work.then(callback);
  }
  updateContainer(children, root, null, work._onCommit);
  return work;
};

1.3. updateContainer 这节内部函数太多, 拆成二级标题, 续1.2.

unbatchedUpdates就不看了, 大体就是立即执行updateContainer函数.
updateContainer函数里面都是调用其他函数直接看代码吧, 代码如下:

// packages\react-reconciler\src\ReactFiberReconciler.js
function updateContainer(
  element: ReactNodeList, // 使用React编写的组件
  container: OpaqueRoot, // FiberRoot
  parentComponent: ?React$Component<any, any>, // null
  callback: ?Function, // ReactWork._onCommit
): ExpirationTime {
   
  const current = container.current; // FibeNode
  const currentTime = requestCurrentTime(); // 获取当前已经花费的时间
  const expirationTime = computeExpirationForFiber(currentTime, current);
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
  );
}

具体函数见下面1.3.x.下节

1.3.1. requestCurrentTime

这个函数主要用来计算到期时间, 代码如下:

// packages\react-reconciler\src\ReactFiberScheduler.js
function findHighestPriorityRoot() {
   
  let highestPriorityWork = NoWork; // 0
  let highestPriorityRoot = null;
  // lastScheduledRoot初始化为null
  if (lastScheduledRoot !== null) {
   
	// ... 省略
  }

  nextFlushedRoot = highestPriorityRoot; // null
  nextFlushedExpirationTime = highestPriorityWork; // 0
}


// packages\react-reconciler\src\ReactFiberWorkLoop.js
function requestCurrentTime() {
    // 计算到期时间
  // 通过添加到当前时间(开始时间)来计算到期时间。 但是,如果在同一事件中安排了两次更新,我们应将它们的开始时间视为同步,即使第一次和第二次呼叫之间的实际时钟时间已提前。

  // 换句话说, 因为过期时间决定了如何批量更新

  // 我们希望在同一事件中发生的所有类似优先级的更新都会收到相同的到期时间。 否则我们会撕裂。
   
  // 我们跟踪两个不同的时间:renderer time和scheduler time。 renderer time可以随时使用`performance.now`更新;
  
  // 但是scheduler time只能更新在 没有待处理的工作,或者我们确定我们不在事件的中间时
  if (isRendering) {
   
    // We're already rendering. Return the most recently read time.
    return currentSchedulerTime;
  }
  // Check if there's pending work.
  findHighestPriorityRoot();
  if (
    nextFlushedExpirationTime === NoWork ||
    nextFlushedExpirationTime === Never
  ) {
   
    // If there's no pending work, or if the pending work is offscreen, we can
    // read the current time without risk of tearing.
    // 没有待处理的工作,或者待处理的工作是在屏幕外,
    recomputeCurrentRendererTime();
    currentSchedulerTime = currentRendererTime; // 上一个函数计算的值赋值到了currentRendererTime
    return currentSchedulerTime;
  }
  // There's already pending work. We might be in the middle of a browser
  // event. If we were to read the current time, it could cause multiple updates
  // 已经有pending work了。 我们可能正处于浏览器事件的中间。 如果我们要读取当前时间,可能会导致多次更新
 
  // within the same event to receive different expiration times, leading to
  // tearing. Return the last read time. During the next idle callback, the
  // time will be updated.
  // 防止在事件返回不同时间, 返回上次的时间, 下次空闲回调时时间会更新.
  return currentSchedulerTime;
}

下面来看一下recomputeCurrentRendererTime函数

1.3.1.1. recomputeCurrentRendererTime

此处now函数就是上一节说的performance.now, 具体:
https://liuxuan.blog.csdn.net/article/details/90641799

function recomputeCurrentRendererTime() {
   
 // originalStartTimeMs = now(); 启动时执行的
  const currentTimeMs = now() - originalStartTimeMs;
  currentRendererTime = msToExpirationTime(currentTimeMs);
}
1.3.1.2. msToExpirationTime
// packages\react-reconciler\src\ReactFiberExpirationTime.js
const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = MAX_SIGNED_31_BIT_INT - 1;

// 1 unit of expiration time represents 10ms.
// 1个到期时间单位是10ms
export function msToExpirationTime(ms: number): ExpirationTime {
   
  // Always add an offset so that we don't clash with the magic number for NoWork.
  // NoWork是常量, 值为0, 应该是怕减数正好为0吧
  return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}

/ 10抹掉10ms时间差, | 0的意思是取整,

1.3.2 computeExpirationForFiber

这个函数主要根据Fiber类型来计算到期时间, 代码如下:

// packages\react-reconciler\src\ReactFiberWorkLoop.js
function computeExpirationForFiber(
  currentTime: ExpirationTime, // requestCurrentTime算得的时间
  fiber: Fiber, // FiberNode
) {
   
  const priorityLevel = getCurrentPriorityLevel(); // 初始返回NormalPriority => 3

  let expirationTime;
  // ConcurrentMode是常量为0b001
  // NoContext是常量为0b000
  // fiber.mode 为 0
  if ((fiber.mode & ConcurrentMode) === NoContext) {
   
    // Outside of concurrent mode, updates are always synchronous.
    // 在并发模式之外,更新始终是同步的
    // Sync是常量, 值为32位系统的V8中的最大整数, Math.pow(2, 30) - 1
    expirationTime = Sync;
  } else if (isWorking && !isCommitting) {
   
    // During render phase, updates expire during as the current render.
    // 在render阶段
    expirationTime = nextRenderExpirationTime;
  } else {
   
    switch (priorityLevel) {
   
      case ImmediatePriority:
        expirationTime = Sync;
        break;
      case UserBlockingPriority:
      	// 计算交互状态的过期时间
        expirationTime = computeInteractiveExpiration(currentTime);
        break;
      case NormalPriority:
        // This is a normal, concurrent update
        // 计算异步状态的过期时间
        expirationTime = computeAsyncExpiration(currentTime);
        break;
      case LowPriority:
      case IdlePriority:
        expirationTime = Never;
        break;
      default:
        invariant(
          false,
          'Unknown priority level. This error is likely caused by a bug in ' +
            'React. Please file an issue.',
        );
    }

    // If we're in the middle of rendering a tree, do not update at the same
    // expiration time that is already rendering.
    // 如果正在rendering tree, 不要更新过期时间
    if (nextRoot !== null && expirationTime === nextRenderExpirationTime) {
   
      expirationTime -= 1;
    }
  }

  // Keep track of the lowest pending interactive expiration time. This
  // allows us to synchronously flush all interactive updates
  // when needed.
  // UserBlockingPriority是常量为2
  if (
    priorityLevel === UserBlockingPriority &&
    (lowestPriorityPendingInteractiveExpirationTime === NoWork ||
      expirationTime < lowestPriorityPendingInteractiveExpirationTime)
  ) {
   
    lowestPriorityPendingInteractiveExpirationTime = expirationTime;
  }

  return expirationTime; // 返回Sync
}

其中computeInteractiveExpirationcomputeAsyncExpiration看一下

1.3.2.1. computeInteractiveExpiration

计算交互情况的过期时间:

// packages\react-reconciler\src\ReactFiberExpirationTime.js
const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
const HIGH_PRIORITY_BATCH_SIZE = 100;

function computeInteractiveExpiration(currentTime: ExpirationTime) {
   
  return 
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值