深入React(2)初始渲染中的Fiber树构建过程

本节主要关注React初始渲染中Fiber树的构建过程,将忽略一些不相关内容。

调试用的源码如下,推荐实践调试。这里的例子很简单,但具备了Fiber树的结构要素:child、return与sibling。

源码调试可参考这篇文章:深入React(1)调试React源码

jsx 复制代码
const container = document.getElementById('app');

debugger; // 从这里开始断点调试

const root = ReactDOM.createRoot(container);

root.render(
  <div>
    <h1>Hello World</h1>
    <a href='https://react.dev'>Welcome to React</a>
  </div>
);

总体的核心流程如下:

createRoot

下面截取了源码中的一些核心逻辑。

jsx 复制代码
function createRoot(container){
  var root = createContainer(container);

  // ReactDOMRoot构造函数返回 {_internalRoot: root}
  return new ReactDOMRoot(root);
}

createContainer是个套壳,里面只执行了createFiberRoot的调用。

jsx 复制代码
function createFiberRoot(containerInfo){
  var root = new FiberRootNode(containerInfo);

  var uninitializedFiber = createHostRootFiber();
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  return root;
}

这里创建了一个FiberRootNode与HostRootFiber,并通过current与stateNode相互引用。HostRootFiber是整个Fiber树构建的起点 ,FiberRootNode的containerInfo属性记录了React绑定的container信息(一个DOM元素)

这里的tag记录了FiberNode的类型,后面会提到,React会根据tag做不同的处理。

render

jsx 复制代码
root.render(
  <div>
    <h1>Hello World</h1>
    <a href='https://react.dev'>Welcome to React</a>
  </div>
);

上述代码中的JSX会先经过Babel编译为下面的内容:

jsx 复制代码
root.render(React.createElement(
  'div',
  null,
  React.createElement(
    'h1',
    null,
    'Hello World'
  ),
  React.createElement(
    'a',
    { href: 'https://react.dev' },
    'Welcome to React'
  )
));

可以理解为JSX就是ReactElement,它的内容如下。ReactElement中记录了元素标签、属性以及子元素信息,通过它可以还原出真实DOM

render()的核心逻辑如下。它将传入的ReactElement保存到update.payload中,然后插入一个异步的更新队列。

jsx 复制代码
function updateContainer(){
  var update = createUpdate(lane);
  update.payload = {
    element: element
  };

  var root = enqueueUpdate(current$1, update, lane);

  if (root !== null) {
    scheduleUpdateOnFiber(root, current$1, lane);
    entangleTransitions(root, current$1, lane);
  }

  return lane;
}

这里忽略什么时候开始进行更新,直接跳到正式的更新入口performConcurrentWorkOnRoot。调试时直接找到performConcurrentWorkOnRoot函数,在该处打断点并跳转即可。

performConcurrentWorkOnRoot

虽然叫performConcurrentWorkOnRoot,但首次渲染会走renderRootSync(),即同步渲染。

jsx 复制代码
// shouldTimeSlice为false
shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);

renderRootSync的核心逻辑是workLoopSync(),其源码如下:

jsx 复制代码
function workLoopSync() {
  // Perform work without checking if we need to yield between fiber.
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

容易看出来这是一个同步迭代的过程。workInProgress即当前正在处理的FiberNode ,它的初始状态为前面提到的HostRootFiber

performUnitOfWork

performUnitOfWork的核心逻辑如下。这里重点关注beginWorkcompleteUnitOfWrok两个函数。

beginWork中会创建并返回下一个Fiber节点,如果存在,则继续处理返回的节点,反之则执行对当前节点执行completeUnitOfWork。

jsx 复制代码
function performUnitOfWork(unitOfWork) {
  var current = unitOfWork.alternate;
  setCurrentFiber(unitOfWork);

  // 初次渲染中,所以current与unitOfWork相同,暂时不用管这个逻辑
  var next = beginWork(current, unitOfWork);
  
  unitOfWork.memoizedProps = unitOfWork.pendingProps;

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

beginWork

beginWork会根据 workInProgress.tag来做不同处理 。前面提到初始化时workInProgress为HostRootFiber,因此将首先执行updateHostRoot。当后续workInProgress更新为div等节点时,则将执行updateHostComponent$1。

jsx 复制代码
switch (workInProgress.tag) {
	// ...
  case HostRoot:
    return updateHostRoot(current, workInProgress, renderLanes);
    
  case HostComponent:
    return updateHostComponent$1(current, workInProgress, renderLanes);
  // ...
}

这里需要注意下两者的区别:它们最终都会执行reconcileChildren(),并返回workInProgress.child ,不同的是children从何处获取

jsx 复制代码
// updateHostRoot
function updateHostRoot(){
  // 对于hostRoot,会从前面提到的update.payload中取出element,作为hostRoot的children
  // 并存储在workInProgress.memoizedState.element中
  processUpdateQueue(workInProgress, nextProps, null, renderLanes);
  var nextState = workInProgress.memoizedState;
  var nextChildren = nextState.element; // 即root.render()中传入的ReactElement
  
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

// updateHostComponent$1
function updateHostComponent$1(){
  // 对于hostComponent,直接从workInProgress.pendingProps.children中获取子元素
  var nextProps = workInProgress.pendingProps;
  var nextChildren = nextProps.children;
  
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

对于hostRoot,会从前面提到的update.payload中取出element,作为hostRoot的children。而对于普通元素,则可以直接从fiber节点的pendingProps中取出children。后面会提到普通元素的fiber节点是如何构造出来的。

beginWork中的重点就是reconcileChildren,它是构建Fiber树的关键逻辑。

reconcileChildren

reconcileChildren的源码如下。

jsx 复制代码
function reconcileChildren(current, workInProgress, nextChildren, renderLanes) {
  if (current === null) {
    // If this is a fresh new component that hasn't been rendered yet, we
    // won't update its child set by applying minimal side-effects. Instead,
    // we will add them all to the child before it gets rendered. That means
    // we can optimize this reconciliation pass by not tracking side-effects.
    workInProgress.child = mountChildFibers(workInProgress, null, nextChildren, renderLanes);
  } else {
    // If the current child is the same as the work in progress, it means that
    // we haven't yet started any work on these children. Therefore, we use
    // the clone algorithm to create a copy of all the current children.
    // If we had any progressed work already, that is invalid at this point so
    // let's throw it out.
    workInProgress.child = reconcileChildFibers(workInProgress, current.child, nextChildren, renderLanes);
  }
}

mountChildFibers与reconcileChildFibers仅仅是一个参数的区别,这个参数名为shouldTrackSideEffects,后面会提到它的作用所在。

jsx 复制代码
var reconcileChildFibers = createChildReconciler(true); // shouldTrackSideEffects为true
var mountChildFibers = createChildReconciler(false); // shouldTrackSideEffects为false

mountChildFibers与reconcileChildFibers最终都会调用reconcileChildFibersImpl。其中会根据子元素是单个元素还是数组来决定是否调用reconcileChildrenArray

jsx 复制代码
function reconcileChildFibersImpl(returnFiber, currentFirstChild, newChild, lanes){
  if (typeof newChild === 'object' && newChild !== null) {
    switch (newChild.$$typeof) {
      // 子节点是一个React Element,比如示例中的div
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(reconcileSingleElement(returnFiber, currentFirstChild, newChild, lanes));
      // ...
    }

    // 子节点是一个数组,比如示例中的div的子元素[h1, a]
    if (isArray(newChild)) {
      return reconcileChildrenArray(returnFiber, currentFirstChild, newChild, lanes);
    }
  }
}

比如hostRoot的子元素为div,则将执行placeSingleChild(reconcileSingleElement());而div的子元素为[h1, a],则将执行reconcileChildrenArray()

reconcileSingleElement

reconcileSingleElement的核心逻辑如下。它根据element创建一个新的Fiber节点,并将其return属性指向returnFiber

jsx 复制代码
var _created4 = createFiberFromElement(element, returnFiber.mode, lanes);

_created4.ref = coerceRef(returnFiber, currentFirstChild, element);
_created4.return = returnFiber;

根据示例中的div元素创建的fiber结构如下:

jsx 复制代码
var _created4 = {
  type: 'div',
  elementType: 'div',
  pendingProps: {
    children: [h1, a]
  },
  return: FiberNode, // HostRoot
  child: null,
  sibling: null,
  flags: 0
}

它包含了element的类型与属性,并有一个return属性指向它的父节点。

同时执行的逻辑还有placeSingleChild,其源码如下。设置fiber.flags属性为Placement意味着这个节点是需要插入的

jsx 复制代码
function placeSingleChild(newFiber) {
  // This is simpler for the single child case. We only need to do a
  // placement for inserting new children.
  if (shouldTrackSideEffects && newFiber.alternate === null) {
    newFiber.flags |= Placement | PlacementDEV;
  }

  return newFiber;
}

这里出现了shouldTrackSideEffects,它的作用是什么呢?设想一下,对于<div><h1 /><a /></div>这样一个结构,是否每个节点都要标记为"插入"呢?显然不是,我们只需要插入div这个根节点到页面上即可 。所以在初始化渲染过程中,只有hostRoot会调用reconcileChildFibers(shouldTrackSideEffects为true) ,即div节点会被标记为Placement。而其他子节点只会调用mountChildFibers(shouldTrackSideEffects为false),不会被标记(后面会看到<h1><a>会被添加为<div>的的子节点,所以最后只插入<div>就能显示全部内容)。

reconcileChildrenArray

这个函数里包含了子元素diff的逻辑,但是对于初始化渲染只需执行下面的部分代码。

jsx 复制代码
if (oldFiber === null) {
  // 遍历children
  for (; newIdx < newChildren.length; newIdx++) {
    // 根据element创建fiber,并设置return属性指向父节点
    var _newFiber = createChild(returnFiber, newChildren[newIdx], lanes);

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

    // 示例中此时shouldTrackSideEffects为true,所以placeChild不会添加flags更新
    lastPlacedIndex = placeChild(_newFiber, lastPlacedIndex, newIdx);

    if (previousNewFiber === null) {
      resultingFirstChild = _newFiber;
    } else {
      // 设置sibling为下一个子节点
      previousNewFiber.sibling = _newFiber;
    }

    previousNewFiber = _newFiber;
  }

  return resultingFirstChild;
}

这里创建子元素的fiber节点时,除了设置return指向父节点,还会设置sibling指向同级的下一个子节点,形成下面的结构。最后将第一个子节点返回作为下一个处理节点。

completeUnitOfWork

beginWork()的返回值为null,即当前fiber节点不存在子节点时,将执行 completeUnitOfWork。比如示例中的h1与a元素,它们是不存在子节点的。

jsx 复制代码
function performUnitOfWork(unitOfWork) {
  // ...
  var next = beginWork(current, unitOfWork, entangledRenderLanes);

  // 示例中beginWork总是返回unitOfWork.child,如果不存在则执行completeUnitOfWork
  if (next === null) {
    // If this doesn't spawn new work, complete the current work.
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

completeUnitOfWork的核心逻辑如下,它也是执行一个循环逻辑。

  1. 首先处理当前节点completeWork()
  2. 然后判断当前节点的sibling是否存在,存在则跳出循环,后续将执行performUnitOfWork(sibling)
  3. 不存在sibling则设置completedWork为returnFiber,继续执行completeUnitOfWork(returnFiber)
  4. 直到当前节点的returnFiber为空(最终是HostRootFiber,其return为空)。
jsx 复制代码
function completeUnitOfWork(unitOfWork) {
  var completedWork = unitOfWork;
  var returnFiber = completedWork.return;
  
  do {
    next = completeWork(current, completedWork, entangledRenderLanes);

    if (next !== null) {
      workInProgress = next;
      return;
    }

    var siblingFiber = completedWork.sibling;

    if (siblingFiber !== null) {
      // 如果sibling存在则后续将执行performUnitOfWork(sibling)
      workInProgress = siblingFiber;
      return;
    } 

    // 如果sibling不存在则执行completeUnitOfWork(returnFiber)
    completedWork = returnFiber;

    workInProgress = completedWork;
  }while (completedWork !== null); 
}

completeWork()的处理逻辑如下,其主要作用是根据fiber结构创建出真实DOM 。如果存在子节点,则将子节点的DOM添加到当前节点的DOM上。但需要注意,这里虽然构建了真实DOM,但并未插入到页面上,因此此时页面上还不会显示出内容(这里照应了前面所说的shouldTrackSideEffects内容,最后只需将div这个根元素插入到页面上即可)。

jsx 复制代码
function completeWork(current, workInProgress, renderLanes) {
  var newProps = workInProgress.pendingProps;
  switch (workInProgress.tag) {
    case HostComponent:
      {
        var _type2 = workInProgress.type;
        // div#app
        var _rootContainerInstance = getRootHostContainer();

        // 调用document.createElement(type)创建DOM元素
        var _instance3 = createInstance(_type2, newProps);

        // 调用domNode.appendChild(child)添加子元素到当前的DOM元素中
        appendAllChildren(_instance3, workInProgress);

        // 将fiber的stateNode属性指向创建的真实DOM
        workInProgress.stateNode = _instance3;

        // 将props属性设置到DOM元素上
        if (finalizeInitialChildren(_instance3, _type2, newProps)) {
          markUpdate(workInProgress);
        }
      }
    case HostRoot:
      {
      	// 处理HostRootFiber
      }
  }
}

beginWork是从HostRootFiber开始,而最终completeUnitOfWork也是以HostRootFiber结束。至此整个workLoopSync循环就结束了。完整流程示意图如下:

整体而言,renderRootSync(workLoopSync)做了哪些事情呢?

  • 针对root.render()传入的ReactElement同步构建了一棵完整的Fiber树,这些fiber节点通过return、child、sibling相连。
  • 每个fiber节点都保存了对应的ReactElement的信息,通过createElementAPI创建出真实的DOM节点,并保存在fiber的stateNode属性上。div的子节点h1与a则通过appendChildAPI被添加到子元素中。
  • 只有div这个根元素的fiber节点被标记了Placement(插入)。

commitRoot

在前面的render过程中已经构建了一棵完整的Fiber树,并且进行了标记(flags)。最后还有一个commit的过程来将这些标记更新到页面上,成为我们可见的内容

比如示例中只有div元素标记了Placement,最后将执行下面的代码做更新:

jsx 复制代码
appendChildToContainer(parent, stateNode);

// 最终将调用appendChild API将div添加到container元素(div#app)上
parentNode.appendChild(child);

总结一下,这篇文章重点梳理了初始化渲染中有关Fiber树构建的部分内容,但很多React的核心内容还没有涉及到,可以把它当成是一个起点,后面慢慢拓展开来。

相关推荐
帅夫帅夫1 分钟前
深入理解 JavaScript 的 const:从基础到内存原理
前端
用户名1232 分钟前
我写了个脚本,让前端彻底告别 Swagger 手动搬砖
前端
爱编程的喵4 分钟前
深入理解JavaScript节流函数:从原理到实战应用
前端·javascript·html
尧木晓晓4 分钟前
开发避坑指南:Whistle 代理失效背后,localhost和 127.0.0.1 的 “爱恨情仇” 与终极解决方案
前端·javascript
风无雨36 分钟前
GO启动一个视频下载接口 前端可以边下边放
前端·golang·音视频
Java技术小馆1 小时前
langChain开发你的第一个 Agent
java·面试·架构
aha-凯心1 小时前
前端学习 vben 之 axios interceptors
前端·学习
熊出没2 小时前
Vue前端导出页面为PDF文件
前端·vue.js·pdf
VOLUN2 小时前
Vue3项目中优雅封装API基础接口:getBaseApi设计解析
前端·vue.js·api
用户99045017780092 小时前
告别广告干扰,体验极简 JSON 格式化——这款工具让你专注代码本身
前端