react图解源码之初始化挂载

react内部的初始化挂载

现在下面有如下页面渲染代码:

html 复制代码
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>React in HTML</title>
    <script src="./react.development.js"></script>
    <script src="./react-dom.development.js"></script>
    <script crossorigin src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<style>
  .component {
    border: 1px solid #ccc;
    padding: 10px;
    margin: 10px;
  }
</style>
<body>
  <div id="root"></div>

  <!-- 使用 type="text/babel" 让 Babel 编译 JSX -->
  <script type="text/babel">

    function A() {
      return (
        <div className="component" data-name="A">
          <div>A</div>
          <div>B</div>
        </div>
      );
    }


    // 创建 React 组件
    function App() {
      return (
        <A />
      );
    }

    // 渲染组件到 DOM
    const root = ReactDOM.createRoot(document.getElementById('root'));
    root.render(<App />);
  </script>
</body>
</html>

Fiber结构介绍

上面的代码大概是如下的层级结构:

jsx 复制代码
<App>
    <A>
        <div>
            <div>A</div>
            <div>B</div>
        </div>
    </A>
</App>

上面的结构在React中会构建如下图的一个FiberTree

从上图中可以看到,在该FiberTree中一共包含2种不同的类型:

  • FiberRootNode: FiberRootNode 是一个特殊节点,充当 React 的根节点,它保存着整个应用程序所需的元数据。其 current 属性指向实际的 Fiber 树结构,每次构建新的 Fiber 树时,它都会将 current 重新指向新的 HostRoot
  • FiberNode: react内部中对结点的一种表示,包含很多属性可以对其结点进行描述
    • tag: FiberNode有许多不同的子类型,在render以及commit阶段会根据该值进行不同的处理,如HostRootFunctionComponentClassComponentHostComponent等等
    • stateNode: 对于tagHostComponetFiberNode,其指向页面中实际渲染的DOM节点
    • childsiblingreturn: 分别指向子节点,兄弟节点以及父节点,用于构造完整Fiber
    • flags: 用于表示在 commit 阶段需要更新的类型。subtreeFlags 表示其子树需要更新的类型

上面对于FiberNode的属性介绍只包含当前初始化页面所需要的属性,其他属性在后面需要用到时再进行解释。

大致过程

我们先来把整个渲染过程进行一个粗略的介绍,我们在渲染页面时,会执行下面的代码

jsx 复制代码
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

上面的代码中,分为了以下两个主要过程:

  1. createRoot
  2. render

createRoot

上面的createRoot会创建一个FiberRootNode

jsx 复制代码
function createRoot(container, options) {
    var root = createContainer(container, ConcurrentRoot, null, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError);
    return new ReactDOMRoot(root);
  }

function createContainer(containerInfo, tag, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError, transitionCallbacks) {
    var hydrate = false;
    var initialChildren = null;
    return createFiberRoot(containerInfo, tag, hydrate, initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError);
  }


function createFiberRoot(containerInfo, tag, hydrate, initialChildren, hydrationCallbacks, isStrictMode, concurrentUpdatesByDefaultOverride, identifierPrefix, onRecoverableError, transitionCallbacks) {
    var root = new FiberRootNode(containerInfo, tag, hydrate, identifierPrefix, onRecoverableError);
    var uninitializedFiber = createHostRootFiber(tag, isStrictMode);
    root.current = uninitializedFiber;
    uninitializedFiber.stateNode = root;
    return root;
  }

上面代码的大致执行流程如下:

执行完成后,会创建一个FiberRootNode,保存在ReactDomRoot实例的this._internalRoot中,充当 React 的根节点,它保存着整个应用程序所需的元数据。其 current 属性指向实际的 Fiber 树结构,每次构建新的 Fiber 树时,它都会将 current 重新指向新的 HostRoot。生成如下FiberTree结构:

当前的root属性如下:

上图中的root为当前ReactDOMRoot的实例,内部_internalRoot为当前实例的FiberRootNode,其current指向当前页面的FiberNode节点。其中tag的值为3,表示FiberNode的节点类型为HostRoot

render

render过程中主要执行了下面内容:

jsx 复制代码
ReactDOMRoot.prototype.render = function (children) {
  var root = this._internalRoot;
  updateContainer(children, root, null, null);
};

function updateContainer(element, container, parentComponent, callback) {
  // some other code...
  var update = createUpdate(eventTime, lane);
  update.payload = {
    element: element
  };
  callback = callback === undefined ? null : callback;
  var root = enqueueUpdate(current$1, update, lane);

  if (root !== null) {
    // 进入调度器 schedule
    scheduleUpdateOnFiber(root, current$1, lane, eventTime);
  }
  return lane;
}

function scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
  if ((executionContext & RenderContext) !== NoLanes && root === workInProgressRoot) {
    // some code...
  } else {
    // 确保FiberRootNode被调度
    ensureRootIsScheduled(root, eventTime);
  }
}

function ensureRootIsScheduled(root, currentTime) {
  // some code...
  // 获取更新优先级,拿取最高级别的优先级任务进行执行
   scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}

function performSyncWorkOnRoot(root) {
    var exitStatus = renderRootSync(root, lanes);
    var finishedWork = root.current.alternate;
    root.finishedWork = finishedWork;
    root.finishedLanes = lanes;
    commitRoot(root, workInProgressRootRecoverableErrors, workInProgressTransitions);
    return null;
  }

上面的代码可以概括为下面的流程图:

其中scheduleUpdateOnFiberensureRootIsScheduled以及scheduleSyncCallback都是调度相关的函数,本章的重点是渲染,先暂时跳过这些内容,后期调度相关会详细讲解。渲染相关的核心函数为performSyncWorkOnRoot,以及performConcurrentWorkOnRoot函数,performSyncWorkOnRoot同步模式performConcurrentWorkOnRoot并发模式

同步模式
text 复制代码
performSyncWorkOnRoot
    ↓
renderRootSync  // 同步渲染根节点
    ↓
workLoopSync    // 同步工作循环(不可中断)
    ↓
completeUnitWork // 完成单元工作
    ↓
commitRoot      // 提交变更到DOM
并发模式
text 复制代码
performConcurrentWorkOnRoot
    ↓
renderRootConcurrent  // 并发渲染根节点
    ↓
workLoopConcurrent    // 并发工作循环(可中断)
    ↓
shouldYield? → 是 → 暂停并返回
    ↓
completeUnitWork
    ↓
commitRoot

上面的同步模式与并发模式的主要区别为渲染根节点的过程,同步模式 创建FiberTree的过程不可中断并发模式 则可以被高优先级任务中断 ,而commitRoot过程则是一致的,都是同步执行。

上面代码可以看到render函数中执行了renderRootSynccommitRoot两个函数,也是React中比较重要的两个部分,一个是创建FiberNode以及对应的stateNodeflagssubtreeFlags,并生成FiberTree,另一个是根据前面创建的FiberTree,获取对应节点的flags以及subtreeFlags来进行对应的DOM节点挂载操作。

📢 注意:由于commitRoot一次执行完成的挂载过程 ,为了避免浏览器产生闪烁以及重绘等,React内部进行了优化,也就是在rendercomplete阶段,就将子树的整个DOM树构建完成了,并将其对应的flags冒泡到父节点的subtreeFlags属性中,后续在commit阶段直接比较该属性进行相应的子树挂载即可。下面会进行详细的解析:

renderRootSync

renderRootSync函数中的核心为do...while(true)的执行workLoopSync函数

jsx 复制代码
function renderRootSync(root, lanes) {
  prepareFreshStack(root, lanes);
  do {
    try {
      workLoopSync();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
  workInProgressRoot = null;
  workInProgressRootRenderLanes = NoLanes;

  return workInProgressRootExitStatus;
}

function prepareFreshStack(root, lanes) {
  root.finishedWork = null;
  root.finishedLanes = NoLanes;

  workInProgressRoot = root;
  var rootWorkInProgress = createWorkInProgress(root.current, null);
  workInProgress = rootWorkInProgress;
  finishQueueingConcurrentUpdates();
  return rootWorkInProgress;
}

function createWorkInProgress(current, pendingProps) {
  var workInProgress = current.alternate;

  if (workInProgress === null) {
    // We use a double buffering pooling technique because we know that we'll
    // only ever need at most two versions of a tree. We pool the "other" unused
    // node that we're free to reuse. This is lazily created to avoid allocating
    // extra objects for things that are never updated. It also allow us to
    // reclaim the extra memory if needed.
    // 双缓存机制,创建 alternate
    workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
    workInProgress.elementType = current.elementType;
    workInProgress.type = current.type;
    workInProgress.stateNode = current.stateNode;

    workInProgress.alternate = current;
    current.alternate = workInProgress;
  } else {
    workInProgress.pendingProps = pendingProps;
    workInProgress.type = current.type;
    workInProgress.flags = NoFlags;
    workInProgress.subtreeFlags = NoFlags;
    workInProgress.deletions = null;
  }


  workInProgress.flags = current.flags & StaticMask;
  workInProgress.childLanes = current.childLanes;
  workInProgress.lanes = current.lanes;
  workInProgress.child = current.child;
  workInProgress.memoizedProps = current.memoizedProps;
  workInProgress.memoizedState = current.memoizedState;
  workInProgress.updateQueue = current.updateQueue;

  var currentDependencies = current.dependencies;
  workInProgress.sibling = current.sibling;
  workInProgress.index = current.index;
  workInProgress.ref = current.ref;

  return workInProgress;
}
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork) {
  var current = unitOfWork.alternate;
  setCurrentFiber(unitOfWork);
  var next;

  if ( (unitOfWork.mode & ProfileMode) !== NoMode) {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }

  resetCurrentFiber();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }

  ReactCurrentOwner$2.current = null;
}

上面的代码中,主要实现了两部分重要内容:

  • 1.创建workInProgressprepareFreshStack函数将当前FiberRootNode类型的节点传入,使用其root.current属性创建workInProgressroot.current为当前容器的FiberNode节点,也就是FiberNode(HostRoot)
  • 2.开启工作循环:workLoopSync以及performUnitOfWork函数实现了对FiberTree的创建,其中beginWork是创建FiberNodecompleteUnitOfWork为创建stateNode并构建以当前节点为根结点的DOM树的过程。

执行完上面的内容后,当前内存中的数据结构如下:

workLoopSync

下面是beginWork函数的主要内容,主要就是根据fiberNodetag值执行不同的方法

jsx 复制代码
function beginWork(current, workInProgress, renderLanes) {
  workInProgress.lanes = NoLanes;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
      {
        return mountIndeterminateComponent(current, workInProgress, workInProgress.type, renderLanes);
      }

    case FunctionComponent:
      {
        var Component = workInProgress.type;
        var unresolvedProps = workInProgress.pendingProps;
        var resolvedProps = workInProgress.elementType === Component ? unresolvedProps : resolveDefaultProps(Component, unresolvedProps);
        return updateFunctionComponent(current, workInProgress, Component, resolvedProps, renderLanes);
      }

    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);

    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);

    case HostText:
      return updateHostText(current, workInProgress);
  }
}

function updateHostRoot(current, workInProgress, renderLanes) {
  var nextProps = workInProgress.pendingProps;
  var prevState = workInProgress.memoizedState;
  var prevChildren = prevState.element;
  var nextState = workInProgress.memoizedState;
  var root = workInProgress.stateNode;
  var nextChildren = nextState.element;

  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

让我们从头进入该函数,了解一下执行过程,现在使用了如下的结构进行渲染:

jsx 复制代码
<App>
    <A>
        <div>
            <div>A</div>
            <div>B</div>
        </div>
    </A>
</App>
beginWork阶段
第一次 workLoop

函数执行过程如下图:

这是第一次workLoop循环,当前的workInProgress === FiberNode(HostRoot),进入beginWork函数,根据当前tag === 3,进入updateHostRoot函数,然后进入reconcileChildren函数中,此时,会将 root.render(<App />)代码中的<App />函数组件,经过babel编译后的ReactElement对象作为nextChildren传入reconcileChildFibers作为函数,该函数为执行ChildReconciler(true)后返回的函数,也就是ChildReconciler函数中的闭包reconcileChildFibers函数,其中shouldTrackSideEffects参数为true。后面继续进入reconcileSingleElement函数,该函数的主要作用是创建FiberNode(<App />),并将其return属性,指向当前FiberNode(HostRoot)。后续将当前新创建的FiberNode(<App />)作为参数传入placeSingleChild函数,添加flags,其shouldTrackSideEffects === true,则FiberNode(<App />).flags === Placement,执行到当前的内存数据接口如下:

然后从placeSingleChild依次从调用栈中进行返回,其中比较重要的就是在reconcileChildren函数中,将workInProgress.child属性指向了新创建的FiberNode(<App />)节点。此时结构如下:

然后在performUnitOfWork函数中,将workInProgress指向了FiberNode(<App />)节点。此时结构如下:

第二次workLoop

此时进入下一次workLoopSync循环,流程如下:

当前循环与上一次循环不同之处在于,此时的FiberNode(<App />).tag === IndeterminateComponent并且此时workInProgresscurrentnull,于是进入mountIndeterminateComponent函数,将该FiberNode<App />.tag修改为FunctionComponent,然后继续执行renderWithHooks函数,在该函数内,如果是函数组价,则获取其type,也就是函数来进行执行,执行完成后的返回值,传入reconcileChildren函数内,由于current === null,所以执行mountChildFibers函数,创建FiberNode(<A />),当前数据结构如下:

在函数返回后逐渐返回调用栈中的函数,并在reconcileChildren函数中将workInProgress.child指向当前新的结点FiberNode(<A />),并在performUnitOfWork函数中将当前新创建的结点指向workInProgress,当前数据结构如下:

第三次workLoop

继续执行第三次workLoop循环,流程如下:

当前循环与上次循环过程基本一致,除了生成的FiberNodetag === HostComponent类型,这是在createFiberFromTypeAndProps函数中根据当前的type值决定的,当前type === div,表示为宿主组件 ,则将tag = HostComponent,当前数据结构如下:

依次返回调用栈中的函数,本轮循环执行完成后数据结构如下:

第四次workLoop

第四次循环流程如上图👆🏻,当前workInProgress === FiberNode(div),则在beginWork函数中根据tag === HostComponent,会进入updateHostComponent函数,babel在编译时将HostComponent组件的子元素作为children属性放在了其element.props中,然后再创建FiberNode时,保存在了FiberNode(div).pendingProps属性中。如下代码:

jsx 复制代码
function createFiberFromElement(element, mode, lanes) {
  var type = element.type;
  var key = element.key;
  var pendingProps = element.props;
  var fiber = createFiberFromTypeAndProps(type, key, pendingProps, owner, mode, lanes);
  return fiber;
}

然后再updateHostComponent中执行下面代码,将其子元素传入了reconcileChildren函数

jsx 复制代码
var nextProps = workInProgress.pendingProps;
var prevProps = current !== null ? current.memoizedProps : null;
var nextChildren = nextProps.children;

执行到reconcileChildFibers函数内部,发现其isArray(newChild) === true,则执行了reconcileChildrenArray函数

jsx 复制代码
function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, lanes) {
  var resultingFirstChild = null;
  var previousNewFiber = null;
  var oldFiber = currentFirstChild;
  var lastPlacedIndex = 0;
  var newIdx = 0;
  var nextOldFiber = null;

  if (oldFiber === null) {
    for (; newIdx < newChildren.length; newIdx++) {
      var _newFiber = createChild(returnFiber, newChildren[newIdx], lanes);

      if (_newFiber === null) {
        continue;
      }

      lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);

      if (previousNewFiber === null) {
        resultingFirstChild = _newFiber;
      } else {
        previousNewFiber.sibling = _newFiber;
      }

      previousNewFiber = _newFiber;
    }

    return resultingFirstChild;
  }
  return resultingFirstChild;
}

当前函数执行完成后的内存模型如下:

如上图所示,生成了对应的FiberNode(div-A)节点,以及FiberNode(div-B)节点,并将其通过sibling属性进行连接。并且它们的return节点都指向了FiberNode(div)节点。

后续依次返回调用栈中的函数,本轮循环执行完成后数据结构如下:

第五次workLoop

进入第五次workLoop循环中的beginWork函数,当前workInProgress === FiberNode(div-A),流程如下: 大致流程与上次循环一致,不同点为没有其子元素,函数返回的为null,其没有子节点,于是进入performUnitOfWork函数中的completeUnitOfWork函数。当前内存中数据结构如下:

整体的beginWork流程如下图:

相关推荐
evle2 小时前
从 Recoil 的兴衰看前端状态管理的技术选型
前端·react.js
薛定e的猫咪4 小时前
Vibe Coding范式实战:用AI工具链(Stitch+Figma+ai studio+Trae)快速开发全栈APP
前端·人工智能·react.js·github·figma
追光少年33226 小时前
React学习:ES6
学习·react.js·es6
青青家的小灰灰7 小时前
深入解析 React 中的 useCallback:原理、场景与最佳实践
前端·react.js
用户600071819108 小时前
【翻译】元素与 Children 属性
前端·react.js
青青家的小灰灰8 小时前
深入解析 React 中的 useEffect:副作用管理的艺术与科学
前端·react.js
GISer_Jing9 小时前
Taro全栈学习路线与实战指南:从基础语法到工程化、性能优化深度进阶
前端·react.js·taro
GISer_Jing9 小时前
从零到架构师:Taro 全链路学习与实战指南
前端·react.js·taro
晓得迷路了9 小时前
栗子前端技术周刊第 117 期 - TypeScript 6.0 Beta、webpack 2026 年路线图、React 最新生态调查报告结果...
前端·javascript·react.js