React源码分析=; Reac初次渲染分析

Posted 刘翾

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React源码分析=; Reac初次渲染分析相关的知识,希望对你有一定的参考价值。

文章目录

本不该有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以上是关于React源码分析=; Reac初次渲染分析的主要内容,如果未能解决你的问题,请参考以下文章

react渲染原理分析

源码分析-react1-根节点渲染

setState源码分析

源码分析-react2-根节点渲染

深入分析虚拟DOM的渲染过程和特性

React 源码分析:调用ReactDOM.render后发生了什么