React18.2x源码解析(三)render阶段之reconciler协调流程

本章节我们主要讲解Fiber Reconciler协调流程,即FiberTree的具体创建过程。

回到之前的renderRootXXX方法:

js 复制代码
let exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);

下面的讲解我们将renderRootSync同步渲染模式为例,因为常规的情况下react应用都是同步渲染模式创建的FiberTree,并且并发渲染模式和同步渲染模式实际差异并不大,后续我们会讲解它们具体的差异,下面我们开始学习具体的创建过程。

1,renderRootSync

查看renderRootSync源码:

js 复制代码
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
​
# 同步渲染模式
function renderRootSync(root: FiberRoot, lanes: Lanes) {
  const prevExecutionContext = executionContext;
  executionContext |= RenderContext;
  // 派发器:包含了所有hook方法
  const prevDispatcher = pushDispatcher();
  
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    workInProgressTransitions = getTransitionsForLanes(root, lanes);
    # 根据初始的【或者叫上一次的】hostFiber对象内容,来新建一个本次工作中的hostFiber,即WorkInProgress【非克隆】
    // 这个函数调用之后,初始的root.current即hostFiber对象,它的alternate属性就有了内容,
    // wrokInProgress就是工作中的hostFiber,是计算中的组件树根节点
    prepareFreshStack(root, lanes);
  }
​
  # 开始执行工作循环
  do {
    try {
      workLoopSync();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
  
  // 重置上下文依赖
  resetContextDependencies();
​
  executionContext = prevExecutionContext;
  popDispatcher(prevDispatcher);
​
  // 重置work,表示没有在进行中的工作内容
  workInProgressRoot = null;
  workInProgressRootRenderLanes = NoLanes;
  
  # 返回最后的渲染状态
  return workInProgressRootExitStatus;
}

renderRootSync方法的核心是一个do while循环体,并且它的循环条件永远为真,代表为无限循环,只有workLoopSync方法执行完成后,才能触发break关键字,退出循环。

renderRootSync方法每次执行时,都会在workLoop工作循环开始之前,调用一个prepareFreshStack方法,这个方法的作用是根据上一次的【旧的】HostFiber对象内容【FiberTree的根节点】,创建一个新的HostFiber对象,作为本次创建FiberTree的根节点。

也就是说每次创建具体FiberTree之前,都会首先创建【或者说确定】用于本次FiberTree的根节点HostFiber

prepareFreshStack

查看prepareFreshStack方法:

js 复制代码
function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
  // 重置当前的应用根节点的Fiber树
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
​
  workInProgressRoot = root;
  # 使用旧的hostFiber对象,来创建一个本次工作的hostFiber,即WorkInProgress
  const rootWorkInProgress = createWorkInProgress(root.current, null);
  # 并且更新全局变量workInProgress的值为 本次参与计算的hostFiber
  workInProgress = rootWorkInProgress;
  // 设置子节点的更新lanes值: 为本次更新的lanes
  workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
  workInProgressRootExitStatus = RootInProgress;
  workInProgressRootFatalError = null;
  workInProgressRootSkippedLanes = NoLanes;
  workInProgressRootInterleavedUpdatedLanes = NoLanes;
  workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
  workInProgressRootPingedLanes = NoLanes;
  workInProgressRootConcurrentErrors = null;
  workInProgressRootRecoverableErrors = null;
​
  finishQueueingConcurrentUpdates();
​
  return rootWorkInProgress;
}

首先重置了root应用根节点上的Fiber树和更新lanes,然后调用createWorkInProgress方法创建参与本次更新的HostFiber,然后将创建结果赋值给全局变量workInProgress,然后下面还初始化了一些其他的全局变量,都是为了本次的更新做准备工作。

注意:workInProgress是一个全局变量,代表当前正在工作中的Fiber节点【动态更新的】,它并非固定指某一个Fiber节点,只是在当前函数中,它的值被赋值为新建的HostFiber,代表本次参与计算的FiberTree根节点。

下面我们深入看一下createWorkInProgress方法:

js 复制代码
// packages\react-reconciler\src\ReactFiber.new.js
​
export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
  let workInProgress = current.alternate;
   
  # 首次应用加载
  if (workInProgress === null) {
    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;
    // Needed because Blocks store data on type.
    workInProgress.type = current.type;
    workInProgress.flags = NoFlags;
    workInProgress.subtreeFlags = NoFlags;
    workInProgress.deletions = null;
  }
    
  # 复用上一次HostFiber的内容
  // Reset all effects except static ones.
  // Static effects are not specific to a render.
  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;
  const currentDependencies = current.dependencies;
  workInProgress.dependencies =
    currentDependencies === null
      ? null
      : {
          lanes: currentDependencies.lanes,
          firstContext: currentDependencies.firstContext,
        };
​
  // These will be overridden during the parent's reconciliation
  workInProgress.sibling = current.sibling;
  workInProgress.index = current.index;
  workInProgress.ref = current.ref;
  ...
​
  return workInProgress;
}

createWorkInProgress方法看着代码很多,实际上它的逻辑很简单,在讲解之前,再次贴出currentworkInProgress的关系图:

首先从通过current.alternate属性中取出对应的workInProgress,然后下面就是一个if else的条件语句:

js 复制代码
let workInProgress = current.alternate;
​
if (workInProgress === null) {
    // 应用首次加载,此时workInProgress一定时null,需要调用createFiber来新建参与计算的`HostFiber`
    workInProgress = createFiber();
} else {
    // 应用更新,无需新建workInProgress,复用current的相关内容即可
}
  • 在react应用首次加载的时候,current.alternate一定是null,所以需要调用createFiber来新建参与计算的HostFiber
  • 如果是应用更新阶段,这里的current.alternate指向就是上一次的【旧的】HostFiber

无论是新建的,还是取出旧的workInProgress,它的后续内容都是复用current的内容,所以说createWorkInProgress方法它的作用就是确定本次参与计算的workInProgress,在当前的内容中就是确定本次创建FiberTree的根节点HostFiber

回到renderRootSync方法,在讲解完prepareFreshStack方法逻辑后,下面的内容就比较简单了。

剩下的内容主要就是一个do while循环,最后返回一个渲染状态。并且我们可以发现do while的判断条件永远为真,所以这是一个无限循环,只有workLoopSync方法执行完成,才会触发break关键字,跳出循环。

注意:renderRootConcurrent并发渲染模式,在这里也是一样的do while循环逻辑,它们的差异在下面。

workLoopSync

继续查看workLoopSync方法:

js 复制代码
# 同步渲染模式工作循环
function workLoopSync() {
​
  // while循环,只要workInProgress有值,就会一直循环执行performUnitOfWork,直到workInProgress === null
  while (workInProgress !== null) {
    // 执行装置函数
    // current是上一次的Fiber树,旧的;  workInProgress是正在计算中的Fiber树,新的
    performUnitOfWork(workInProgress);
  }
}

workLoopSync方法内只有一个while循环,它的判断条件是workInProgress不为null,表示还有进行中的工作,则需要继续调用performUnitOfWork方法,创建未完成的FiberTree。所以同步渲染模式,一旦开始这个Fiber Reconciler协调流程,就会进入一直循环的逻辑,直到创建出完整的FiberTree,这个过程无法中断。

下面我们来看看并发渲染模式的方法。

workLoopConcurrent

查看workLoopConcurrent方法:

js 复制代码
# 并发渲染模式 工作循环
function workLoopConcurrent() {
  // workInProgress === null 代表FiberTree的构建工作结束
  // shouldYield可中断构建
  while (workInProgress !== null && !shouldYield()) {
    // 这个方法会创建下一个fiberNode赋值给 workInProgress,并将已创建的FiberNode链接起来构成FiberTree
    performUnitOfWork(workInProgress);
  }
}

可以看见和同步渲染模式基本一样,唯一的区别就是while循环的多了一个判断条件:

scss 复制代码
!shouldYield()

这个方法就是我们在第二章节遇见过shouldYieldToHost,它的作用就是判断:当前程序运行时间是否小于帧间隔时间frameInterval【默认5ms】。

  • 如果小于:则返回false,代表还有剩余可执行时间。取反后为true,表示可以继续创建FiberTree的工作循环。
  • 如果大于:则返回true,代表没有剩余可执行时间。取反后为false,则while循环的条件将不再满足,会暂停创建FiberTree的工作,结束本次宏任务,剩下的工作会留待下一次宏任务再处理。

所以并发渲染模式和同步渲染模式的主要区别 就是在这里:它可以中断FiberTree的创建过程,而同步渲染模式是无法中断这个过程的,它只能从开始到创建完成。

并且在这里我们也可以看出,它们的while循环里面都是执行了一个相同的performUnitOfWork方法,所以后面的逻辑也就没有什么模式的区分了,都是执行的一样的内容。

在继续讲解performUnitOfWork方法内容之前,这里要补充说明一下暂停的工作是如何恢复执行的?

暂停工作与恢复执行
js 复制代码
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
​
// 并发渲染模式
function renderRootConcurrent() {
  do {
    try {
      workLoopConcurrent();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
  
  // 检查tree工作是否完成
  if (workInProgress !== null) {
    // Still work remaining.
    return RootInProgress;
  }
}

workLoopConcurrent并发渲染的工作被中断后,就会退出do while循环,然后就会检查当前的workInProgress是否为null,很明显被暂停的情况,workInProgress是一定有值的,它的值为下一个即将处理的Fiber节点。

此时workInProgress不为null,就会返回RootInProgress的渲染状态,表示还有剩下的工作。

这里return之后,就会回到performConcurrentWorkOnRoot方法之中。

js 复制代码
function performConcurrentWorkOnRoot(root, didTimeout) {
  // 返回未完成的渲染状态
  let exitStatus = shouldTimeSlice ? renderRootConcurrent(root, lanes) : renderRootSync(root, lanes);
​
  // 根据渲染状态 继续执行逻辑 
  if (exitStatus !== RootInProgress) {
      ...
  }
  
  // 渲染未完成的情况:
  if (root.callbackNode === originalCallbackNode) {
    // 渲染暂停的情况:继续返回performConcurrentWorkOnRoot,下一次宏任务继续处理
    return performConcurrentWorkOnRoot.bind(null, root);
  }
}
​

此时返回未完成的渲染状态,就不满足下面的条件判断,直接来后最后判断当前的回调任务是否还是原来的任务:

js 复制代码
root.callbackNode === originalCallbackNode

很明显这里还是相同的回调任务,对应的就是渲染暂停的情况,这里就会继续返回这个performConcurrentWorkOnRoot函数。

这里return就会回到workLoop方法之中:

js 复制代码
function workLoop() {
    ...
    while (currentTask !== null) {
​
      if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost()) ) {
        // This currentTask hasn't expired, and we've reached the deadline.
        break;
      }
      ...
      
        const continuationCallback = callback(didUserCallbackTimeout);
        // performConcurrentWorkOnRoot函数
        if (typeof continuationCallback === 'function') {
          // 说明任务还未完成,将任务继续设置未当前任务的callback,等待下次继续执行
          // 这里没有删除这个任务,则下次取出的第一个任务,还是这个任务,
          currentTask.callback = continuationCallback;
        }
    }
    
    if (currentTask !== null) {
    // 还有工作,则会生成一个新的宏任务,在下次的宏任务中继续执行剩下的任务
    return true;
    else {
        ...
    }
}

workLoop方法之中,就会将返回的内容再次赋值给当前任务currentTaskcallback属性,表示还有未完成的工作。workLoop方法中的while循环就会被中断,原因同样是shouldYieldToHost方法。

workLoop方法最后就会判断当前任务currentTask是否为null,很明显被中断的情况,currentTask是有值的,所以就会返回true,这里会一直向上return

ini 复制代码
=>  flushWork  => performWorkUntilDeadline

最后会将结果返回performWorkUntilDeadline方法之中:

js 复制代码
const performWorkUntilDeadline = () => {
    ...
    hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
    // 此时hasMoreWork为true
    if (hasMoreWork) {
      // 如果还有任务,则又触发MessageChannel事件,生成新的宏任务,即在下一个消息事件继续执行任务
      schedulePerformWorkUntilDeadline();
    } else {
        ...
    }
​
}

此时hasMoreWorktrue,就会调用schedulePerformWorkUntilDeadline方法,生成一个新的宏任务,在下个宏任务中继续执行剩下的任务。

关于上面这部分调度的逻辑可以查看《React18.2x源码解析(二)scheduler调度程序》。

恢复执行的原理也很简单:在下次的宏任务中继续创建FiberTree的时候,因为workInProgress是一个全局变量,一直保存在内存之中,并且它内容为下一个即将处理的Fiber节点,所以下次再回到这里时,就会自动从这个Fiber节点开始继续执行剩下的创建工作。

js 复制代码
function workLoopConcurrent() {
  // workInProgress === null 代表FiberTree的构建工作结束
  // shouldYield可中断构建
  while (workInProgress !== null && !shouldYield()) {
    // 这个方法会创建下一个fiberNode赋值给 workInProgress,并将已创建的FiberNode链接起来构成FiberTree
    performUnitOfWork(workInProgress);
  }
}

注意: 这里的下次并不一定是指的下一个宏任务,因为有可能会遇到更高优先级的任务排在前面执行,比如浏览器执行渲染相关的宏任务,这就是react框架使用调度器与时间切片功能的意义所在。

2,performUnitOfWork

案例准备

在讲解具体的FiberTree创建过程之前,我们准备一个基础的案例,配合案例可以帮助我们更好的理解创建的流程。

js 复制代码
// index.js
​
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
​
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
js 复制代码
// App.js
​
import MyFun from './views/MyFun';
import MyClass from './views/MyClass';
​
export default function App() {
  console.log('App组件运行了')
  return (
    <div className="App">
      <div>react源码调试</div>
      <MyFun name='MyFun'></MyFun>
      <MyClass name='MyClass'></MyClass>
    </div>
  );
}
js 复制代码
//  MyFun.js
import React from 'react'
import { useState } from 'react'
​
export default function MyFun(props) {
  console.log('MyFun组件运行了')
  const [count, setCount] = useState(1)
  return (
    <div className='MyFun'>
      <div>MyFun组件</div>
      <div>state: {count}</div>
      <div>name: {props.name}</div>
    </div>
  )
}
js 复制代码
// MyClass.js
import React, { Component } from 'react';
​
export default class MyClass extends Component {
  constructor(props) {
    super(props)
    console.log('MyClass组件运行了')
    this.state = {
      count: 1
    }
  }
  componentDidMount() {
    console.log('MyClass组件mount完成')
  }
  render() {
    return (
      <div className='MyClass'>
        <div>MyClass组件</div>
        <div>state: {this.state.count}</div>
        <div>name: {this.props.name}</div>
      </div>
    );
  }
}
Fiber树结构

下图是案例对应的FiberTree树结构:

上图是根据案例绘制的对应的FiberTree结构,方便大家理解,后面阅读beginWrokcompleteWork的执行逻辑时可以根据此图印证。每一个单元格都是一个Fiber对象【FiberNode实例】,Fiber对象的child属性指向它的第一个子节点【FirstChild】,Fiber对象的return属性指向它的父级节点。

这里有一个注意的点:

js 复制代码
// 非纯静态节点
<div>state: {count}</div>

即节点的内容中包含了动态的数据内容{count},react在创建FiberTree时,会进行分离,创建成独立的Fiber节点,也就是如上图所示一样,它还会继续分成两个子节点。

所以图中的name: {props.name}以及后面class组件中的对应内容都是一样处理逻辑,它们也还会有自己的子节点,图中没有展示完全是因为会增加图片的复杂度【影响阅读】,所以没有补全这几个内容的后续节点,只补充了state: {count}这个节点的子节点内容,这一点细节大家只要了解即可,同时在这里我们也可以知道DOM结构和FiberTree并不是一一对应的。

下面正式进入FiberTree的创建过程。

performUnitOfWork

查看performUnitOfWork方法:

js 复制代码
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
​
function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate; // 上一次,旧的节点
​
  // 开始循环构建
  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    // 开始工作
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
​
  /**
   * 注意:当遍历到叶子元素,即没有子FiberNode时,performUnitOfWork会进入归的阶段。
   * 这个阶段会调用completeUnitOfWork处理FiberNode,当某个FiberNode阶段执行completeUnitOfWork方法后,
   * 如果存在其他的兄弟节点【FiberNode.sibling !== null】,会进入兄弟节点的归阶段,
   * 如果不存在其他兄弟节点,则进入父节点的归阶段,递阶段和归阶段会交替进行直到HostFiber的归阶段
   * 至此,render工作结束,Fiber树创建完成。
   * 
   */
  if (next === null) {
    // 这个阶段会调用completeUnitOfWork处理FiberNode,
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

首先从workInProgress.alternate属性中取出current对象,对于这个current对象,你可以理解为这个节点上一次对应的内容【因为在参与计算时,workInProgress一定是新的节点,current则变成了旧的节点】,这里在调用beginWork时,同时传入了currentworkInProgress,它的作用就是方便后续区分这个节点是更新还是初始加载,以及为更重要的diff过程做准备。

关于currentworkInProgress,对比Vue源码,就是patch过程中的patch( oldVNode,VNode )

3,beginWork

继续查看beginWork方法:

js 复制代码
// packages\react-reconciler\src\ReactFiberBeginWork.new.js
​
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
​
  // update更新流程
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    ...
​
  } else {
    didReceiveUpdate = false;
​
    // mount加载流程
​
    if (getIsHydrating() && isForkedChild(workInProgress)) {
      const slotIndex = workInProgress.index;
      const numberOfForks = getForksAtLevel(workInProgress);
      pushTreeId(workInProgress, numberOfForks, slotIndex);
    }
  }
  workInProgress.lanes = NoLanes;
​
  # 根据不同的tag,进入不同的处理逻辑:
  switch (workInProgress.tag) {
    // 函数组件 mount
    case IndeterminateComponent: {
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderLanes,
      );
    }
    // 函数组件 update
    case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    // 类组件分支
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      // 处理class App组件【此时App组件对应的FiberNode已经创建】
      return updateClassComponent(
        current,
        workInProgress, // 存储的App组件对应的FiberNode
        Component, // FiberNode.type
        resolvedProps,
        renderLanes,
      );
    }
    // 根节点的处理【第一次都会走这里】
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:  // 代表原生Element元素,div,span
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText: // 文本类型
      return updateHostText(current, workInProgress);
    case SuspenseComponent:
      return updateSuspenseComponent(current, workInProgress, renderLanes);
    case HostPortal:
      return updatePortalComponent(current, workInProgress, renderLanes);
    // 处理ForwardRef组件 11
    case ForwardRef: {
      const type = workInProgress.type;
        ...
    }
    case Fragment:
      return updateFragment(current, workInProgress, renderLanes);
    case Mode:
      return updateMode(current, workInProgress, renderLanes);
    case Profiler:
      return updateProfiler(current, workInProgress, renderLanes);
    case ContextProvider:
      return updateContextProvider(current, workInProgress, renderLanes);
    case ContextConsumer:
      return updateContextConsumer(current, workInProgress, renderLanes);
    
    // memo组件处理 14
    // 简单MemoComponent在首次创建时也会走这里,更新时会走下面updateSimpleMemoComponent
    // 因为它会在进入MemoComponent后,进入另外一个分支,走updateSimpleMemoComponent
    case MemoComponent: {
      const type = workInProgress.type;
        ...
    }
​
    // 简单memo组件:15
    case SimpleMemoComponent: {
        ...
    }
    case IncompleteClassComponent: {
      return mountIncompleteClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    
    // 省略
    ...
  throw new Error(
    `Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
      'React. Please file an issue.',
  );
}

beginWork方法的内容比较多,下面我们按顺序进行解析:

首先是一个current的判断:

csharp 复制代码
if (current !== null) {
    // 更新流程
} else {
    // 加载流程
}

根据前面的performUnitOfWork方法可以知道:current取之于workInProgress.alternate属性,代表着旧的节点。

js 复制代码
function performUnitOfWork() {
    const current = workInProgress.alternate;
    beginWork(current, workInProgress, subtreeRenderLanes);
}

这里我们要注意: 第一次进入beginWork工作的节点一定是HostFiber,也就是FiberTree的根节点。

js 复制代码
// packages\react-reconciler\src\ReactWorkTags.js
export const HostRoot = 3; // 根节点

tag3代表着此节点为HostFiber

当前的workInProgressHostFiber,则current一定是有值的,因为它的current在最开始的createRoot方法中就已经创建了,而workInProgress是在进入workLoopSync工作循环之前创建的,所以它在beginWork方法中永远都不会为null,它会进入更新的流程。

然后beginWork方法后续的内容就是根据workInProgress.tag的值,进入不同的分支处理逻辑。

js 复制代码
switch (workInProgress.tag) {
    case FunctionComponent: {},
    case ClassComponent: {},
    case HostRoot: {},
    ...
}

注意这个switch case结构【根据tag值执行不同的组件逻辑】,它在react源码中应用非常广泛,无论是在render阶段还是commit阶段中都有它的身影,唯一的区别是执行的细节不同而已。

当前为HostFiber则会进入HostRoot分支,进行根节点的处理。

js 复制代码
case HostRoot:
  return updateHostRoot(current, workInProgress, renderLanes);
updateHostRoot

继续查看updateHostRoot方法:

js 复制代码
// packages\react-reconciler\src\ReactFiberBeginWork.new.js
​
function updateHostRoot(current, workInProgress, renderLanes) {
  const nextProps = workInProgress.pendingProps;
  // 之前的state
  const prevState = workInProgress.memoizedState;
  const prevChildren = prevState.element;
  cloneUpdateQueue(current, workInProgress);
  processUpdateQueue(workInProgress, nextProps, null, renderLanes);
  // 处理后的state
  const nextState: RootState = workInProgress.memoizedState;
  const nextChildren = nextState.element; // 存储的组件的内容
  ...
  // 如果新旧children相同,则会进入优化的逻辑,跳过本次reconcile协调流程,复用原来的节点内容
  if (nextChildren === prevChildren) {
    return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
  }
  # 不相同,则进入diff过程,创建新的子节点内容
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  }
  // 返回新建的子节点
  return workInProgress.child;
}

进入updateHostRoot方法后,主要是根据新旧children是否相同来进入不同的逻辑处理:

  • bailoutOnAlreadyFinishedWork:代表新旧children相同,满足优化策略条件,可以进入react内部的优化逻辑,如果满足内部优化逻辑,则可以跳过本节点的reconcile协调流程,复用原来的节点内容。
  • reconcileChildren:不满足优化策略,代表子节点需要更新,进入本节点的reconcile协调流程,创建新的子节点内容。

最后返回新的子节点内容。

reconcileChildren

这里的HostFiber节点,会进入reconcileChildren方法:

js 复制代码
// packages\react-reconciler\src\ReactFiberBeginWork.new.js
​
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  if (current === null) {
    # 加载流程
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    # 更新流程, 更新当前节点的child内容
    workInProgress.child = reconcileChildFibers(
      workInProgress, // 父节点
      current.child, // 旧的child, null
      nextChildren, // 新的child
      renderLanes, // 本次更新的lanes
    );
  }
}

根据前面已经知道,HostFiber对应的current一定是有值的【FiberTree的根节点一个特殊的节点,它的加载和普通的节点有所不同】,所以这里它会进入reconcileChildFibers分支,开始更新HostFiberchild内容【也就是App根组件】。

注意: 这里的mountChildFibersreconcileChildFibers其实都是ChildReconciler方法调用的返回值,代表着它们其实是同一个方法。它们的区别在于ChildReconciler调用时传入了一个不同的状态,即shouldTrackSideEffects参数,这个参数表示是否追踪副作用。

js 复制代码
// packages\react-reconciler\src\ReactChildFiber.new.js
​
// 更新子元素
export const reconcileChildFibers = ChildReconciler(true);
// 创建子元素
export const mountChildFibers = ChildReconciler(false);
​
function ChildReconciler(shouldTrackSideEffects) {
    ...
    
    function reconcileChildFibers() {}
    
    return reconcileChildFibers;
}
reconcileChildFibers

继续查看reconcileChildFibers方法:

js 复制代码
// packages\react-reconciler\src\ReactChildFiber.new.js
​
function reconcileChildFibers(
  returnFiber: Fiber, // 父节点
  currentFirstChild: Fiber | null, // 子节点
  newChild: any, // 新的child内容
  lanes: Lanes,
): Fiber | null {
​
  // 根据新的child类型,进行不同的处理
  if (typeof newChild === 'object' && newChild !== null) {
      
    // 1,单个子节点处理
    switch (newChild.$$typeof) {
      # 最常见的react-element元素类型:react中类组件,函数组件,普通dom组件都属于此类型
      case REACT_ELEMENT_TYPE:
        return placeSingleChild(
          reconcileSingleElement(
            returnFiber,
            currentFirstChild,
            newChild,
            lanes,
          ),
        );   
      ... // 省略
    }
      
    // 2,数组节点的处理,即多个子节点,比如div.App下面有三个子节点
    // 循环创建多个子节点,最后返回第一个子节点,firstChild
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }
​
  // 3,处理文本子节点
  if (
    (typeof newChild === 'string' && newChild !== '') ||
    typeof newChild === 'number'
  ) {
    return placeSingleChild(
      reconcileSingleTextNode(
        returnFiber,
        currentFirstChild,
        '' + newChild,
        lanes,
      ),
    );
  }
  // newChild为null时,表示没有子节点了,会返回一个null,由此一组beginWork工作执行完成,
  return deleteRemainingChildren(returnFiber, currentFirstChild);     
}

reconcileChildFibers方法就是根据新的子节点类型,来创建新的child内容,HostFiber的子节点为App根组件,如图所示:

进入第一个分支,开始单个子节点的处理过程:

js 复制代码
return placeSingleChild(reconcileSingleElement())

它的处理是两个函数的调用,返回最后的调用结果,这里我们先看里面的函数调用。

reconcileSingleElement

查看reconcileSingleElement方法:

js 复制代码
// packages\react-reconciler\src\ReactChildFiber.new.js
​
function reconcileSingleElement(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  element: ReactElement,
  lanes: Lanes,
): Fiber {
  const key = element.key;
  let child = currentFirstChild;
  # 更新处理 【单节点diff】
  while (child !== null) {
    if (child.key === key) {
      const elementType = element.type;
      if (elementType === REACT_FRAGMENT_TYPE) {
        if (child.tag === Fragment) {
          deleteRemainingChildren(returnFiber, child.sibling);
          const existing = useFiber(child, element.props.children);
          existing.return = returnFiber;
          return existing;
        }
      } else {
        if (
          child.elementType === elementType ||
          // Keep this check inline so it only runs on the false path:
          (__DEV__
            ? isCompatibleFamilyForHotReloading(child, element)
            : false) ||
          (typeof elementType === 'object' &&
            elementType !== null &&
            elementType.$$typeof === REACT_LAZY_TYPE &&
            resolveLazy(elementType) === child.type)
        ) {
          deleteRemainingChildren(returnFiber, child.sibling);
          const existing = useFiber(child, element.props);
          existing.ref = coerceRef(returnFiber, child, element);
          existing.return = returnFiber;
          return existing;
        }
      }
      // Didn't match.
      deleteRemainingChildren(returnFiber, child);
      break;
    } else {
      deleteChild(returnFiber, child);
    }
    // 更新为兄弟节点:再次处理更新逻辑
    child = child.sibling;
  }
 
  # 创建处理
  if (element.type === REACT_FRAGMENT_TYPE) { // 片段类型
    const created = createFiberFromFragment(
      element.props.children,
      returnFiber.mode,
      lanes,
      element.key,
    );
    created.return = returnFiber;
    return created;
  } else {
     
    // 其他类型
    // 根据ReactElement元素对象,创建FiberNode节点
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    // 设置App组件的FiberNode.return 指向hostFiberRoot根节点
    created.return = returnFiber;
    return created;
  }
}

这里的child是前面传递的current.child,这是首次加载,所以currenthostFiber】肯定没有子节点内容,所以为null,直接进入下面的创建过程:

App组件为react-element元素类型,不属于fragment类型,进入else分支的创建逻辑。

下面的内容比较重要,正式进入组件节点的创建过程。

createFiberFromElement

查看createFiberFromElement方法:

js 复制代码
// packages\react-reconciler\src\ReactFiber.new.js
​
export function createFiberFromElement(
  element: ReactElement,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
  let owner = null;
  const type = element.type; // 存储原始的组件内容,比如fun,class
  const key = element.key;
  const pendingProps = element.props; // 等待处理的,最新的props
    
  // 创建Fiber节点: 根据元素的type ,针对组件来说:type值一般为class类 或者Fun函数
  const fiber = createFiberFromTypeAndProps(
    type,
    key,
    pendingProps,
    owner,
    mode,
    lanes,
  );
    
  # 返回新建的Fiber节点
  return fiber;
}

createFiberFromElement方法很重要,react所有reactElement元素都是通过这个方法来创建对应的Fiber节点。比如常见的class组件,fun组件,原生dom元素等,我们在项目中定义的组件和元素最终都会通过createElement方法转换为一个reactElement元素对象。

js 复制代码
// react.createElement(<App />) 
{
    $$typeof: Symbol(react.element),
    key: null,
    props: {},
    ref: null,
    type: ƒ App() // class组件就是 type: class App{}
}
​
// react.createElement(<div />) 
{
    $$typeof: Symbol(react.element),
    key: null,
    props: {},
    ref: null,
    type: 'div'
}

所以这里createFiberFromElement方法就是通过这个reactElement对象来创建对应的Fiber节点,最后返回新建的Fiber节点。

createFiberFromTypeAndProps

继续查看createFiberFromTypeAndProps方法:

js 复制代码
// packages\react-reconciler\src\ReactFiber.new.js
​
export function createFiberFromTypeAndProps(
  type: any, // React$ElementType
  key: null | string,
  pendingProps: any,
  owner: null | Fiber,
  mode: TypeOfMode,
  lanes: Lanes,
): Fiber {
  # 设置初始的fiber节点tag
  let fiberTag = IndeterminateComponent;
  let resolvedType = type;
​
  # 根据元素的type进行不同的逻辑处理,class也是函数
  if (typeof type === 'function') {
    // 针对类组件的特殊处理
    if (shouldConstruct(type)) {
      // 根据type 设置fiberTag 为class组件
      fiberTag = ClassComponent;
    } else {
        # 普通函数组件,没有更新tag【重要,伏笔】
    }
  } else if (typeof type === 'string') {
    fiberTag = HostComponent; // 原生dom的处理
  } else {
      
    // 其他type类型的处理
    ...
  }
​
  # 创建Fiber
  const fiber = createFiber(fiberTag, pendingProps, key, mode);
  fiber.elementType = type;
  fiber.type = resolvedType;
  fiber.lanes = lanes;
​
  return fiber;
}

createFiberFromTypeAndProps方法看着代码虽然多,但实际上主要是两部分逻辑的处理:

  • 首先定义初始的fiberTag值,然后根据元素对象的type类型来更新fiberTag值。
js 复制代码
let fiberTag = IndeterminateComponent;

这里的IndeterminateComponent表示待定的组件类型,也就是没有确定的组件类型。

下面列出react源码中常见的tag值对应的组件类型:

js 复制代码
// packages\react-reconciler\src\ReactWorkTags.js
export const FunctionComponent = 0; // 函数组件
export const ClassComponent = 1; // 类组件
export const IndeterminateComponent = 2; // 待定的类型 
export const HostRoot = 3; // 根节点类型
export const HostPortal = 4; 
export const HostComponent = 5; // 指的是原生dom 标签元素
export const HostText = 6; // 文本类型
export const Fragment = 7; // 片段
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
...

然后根据type类型更新fiberTag,这里我们主要关注类组件,函数组件以及普通的dom元素处理即可。

js 复制代码
if (typeof type === 'function') {
  // 针对类组件的特殊处理
  if (shouldConstruct(type)) {
    // 根据type 设置fiberTag 为class组件
     fiberTag = ClassComponent;
    } else {
      # 普通函数组件,没有更新tag【重要,伏笔】
  }
} else if (typeof type === 'string') {
  fiberTag = HostComponent; // 原生dom的处理
} else {
    ...
}

这里要注意,class类本质是构造函数,所以它也是function类型:

js 复制代码
class App{}
typeof App // 'function'

所以类组件和函数组件都会进入function进行处理,然后这里又通过shouldConstruct方法对这两个组件进行了区分:

js 复制代码
function shouldConstruct(Component: Function) {
  const prototype = Component.prototype;
  return !!(prototype && prototype.isReactComponent);
}

在react应用中我们定义的class组件,都是通过继承react的内部组件Component来定义的,所以通过它原型上的isReactComponent属性可以来区分当前组件是不是类组件,返回true则是类组件,返回false则是函数组件。

关于Component内置组件原理可以查看《React18.2x源码解析:React常用API原理》

如果是类组件就会更新fiberTagClassComponent的值。

注意: 如果是函数组件,这里并没有更新fiberTag,所以函数组件的fiberTag还是为待定的IndeterminateComponent的值。

函数组件在创建Fiber节点时,tag值还是待定的组件类型。该节点会在首次执行beginWork工作时,在mountIndeterminateComponent方法中更新tag值为正确的函数类型。

最后是针对普通的dom元素的处理,更新为HostComponent的值。

js 复制代码
else if (typeof type === 'string') {
    fiberTag = HostComponent; // 原生dom的处理
}
  • 然后根据前面准备的信息,调用createFiber创建新的Fiber节点,最后返回新创建的Fiber

关于createFiber方法创建Fiber节点的过程可查看《React18.2x源码解析(一)react应用加载》。

最后在创建完成后,返回新建的Fiber对象。

查看当前创建完成的App组件对应的Fiber对象:

createFiberFromTypeAndProps方法执行完成后,就会return回到createFiberFromElement方法中。

然后继续return回到reconcileSingleElement方法:

js 复制代码
function reconcileSingleElement() {
    ...
    // 根据ReactElement元素对象,创建FiberNode节点
    const created = createFiberFromElement(element, returnFiber.mode, lanes);
    created.ref = coerceRef(returnFiber, currentFirstChild, element);
    # 设置Fiber对象return属性 指向它的父级节点
    created.return = returnFiber; // 当前则为hostFiber
    return created;
}

在根据ReactElement元素对象,创建完成对应的Fiber节点之后,下一步是针对ref的处理,因为我们这里的案例并没有使用ref,所以ref还是null,然后设置Fiber节点的return属性,这个return属性指向的是它的父级节点【当前新建的Fiber属于App组件,所以它的父级节点就是HostFiber】。

最后返回创建完成的Fiber节点,继续向上return,则会来到placeSingleChild方法中:

js 复制代码
// place插入操作
function placeSingleChild(newFiber) {
  # 新建的Fiber节点alternate都是null
  if (shouldTrackSideEffects && newFiber.alternate === null) {
    newFiber.flags |= Placement;
  }
  return newFiber;
}

这个方法只有一个作用,就是给新建的Fiber节点设置flags属性值为Placement【插入的标识,用于commit阶段的逻辑执行】。

注意:因为之前HostFiber是走的reconcileChildFibers方法,这里的shouldTrackSideEffectstrue,可以回到前面查看关于这部分内容的解析。所以当前App组件对应的Fiber节点满足这里的判断条件,它的flags属性就会被打上Placement插入的标记。但是其他节点都是走的mountChildFibers,所以都无法进入判断,最终react应用首次创建FiberTree时,只有App根组件这一个Fiber节点被打上了Placement【插入标记】。它的作用就是在commit阶段,只需要执行一次插入操作,即可将一个完整的离屏DOM树立即渲染到页面【首屏渲染的优化】。

后面的内容就是一直向上return,返回新建Fiber节点,直到回到performUnitOfWork方法中:

js 复制代码
function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate; // 上一次
​
  // 开始循环构建
  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    # 此时next的值就是 新建的App组件对应的Fiber对象
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // 这个阶段会调用completeUnitOfWork处理FiberNode,
    completeUnitOfWork(unitOfWork);
  } else {
    # next有值,开启下一个节点的beginWork工作
    workInProgress = next;
  }
}

到此时,HostFiber节点的beginWork的工作就已完成,next就等于新建的Fiber节点【App组件对应的】。

然后进行判断,此时next有值,则更新workInProgress,将它设置为新的Fiber节点。

js 复制代码
workInProgress = next;

workInProgress之前是hostFiber,此时就已经变成了App组件对应的Fiber节点。

performUnitOfWork方法执行完成,再次回到workLoopSync方法中,此时就代表一个Fiber节点的beginWork工作执行完成。

js 复制代码
function workLoopSync() {
​
  // while循环,只要workInProgress有值,就会一直循环执行performUnitOfWork,直到workInProgress===null
  while (workInProgress !== null) {
    // 执行装置
    performUnitOfWork(workInProgress);
  }
}

此时workInProgress依然有效的Fiber节点,所以满足while循环的条件,就会再次进入performUnitOfWork方法,开启下一个的 Fiber节点【App组件】的beginWork工作。

如果当前是并发渲染模式workLoopConcurrent,react就有机会可以中断这个循环过程,暂停FiberTree的创建,释放主线程,让浏览器执行更高优先级的任务,剩下的FiberTree创建任务等待下一次的宏任务再执行。

到此为止,一个Fiber节点的beginWork工作就此结束。

多节点的创建

下面我们再来了解一下多子节点的创建:

比如案例中的【div.app】Fiber节点,它有三个子节点:

css 复制代码
div 源码调试
myFun
myClass

当传入的newChild为数组时,就会进入多子节点的创建过程:

js 复制代码
// packages\react-reconciler\src\ReactChildFiber.new.js
​
function reconcileChildFibers(..., newChild, ...) {
​
  // 根据新的child类型,进行不同的处理
  if (typeof newChild === 'object' && newChild !== null) {
      
    // 1,单个子节点处理
    ...
      
    // 2,数组节点的处理,即多个子节点,比如div.App下面有三个子节点
    // 循环创建多个子节点,最后返回第一个子节点,firstChild
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }
​
  // 3,处理文本子节点
  ...
​
  // newChild为null时,表示没有子节点了,会返回一个null,由此一组beginWork工作执行完成,
  return deleteRemainingChildren(returnFiber, currentFirstChild);     
}

进入reconcileChildrenArray方法:

js 复制代码
// packages\react-reconciler\src\ReactChildFiber.new.js
​
// 多个子节点创建
function reconcileChildrenArray(
  returnFiber: Fiber,
  currentFirstChild: Fiber | null,
  newChildren: Array<*>,
  lanes: Lanes,
): Fiber | null {
​
​
  // 结果child
  let resultingFirstChild: Fiber | null = null;
  // 上一个新建的child
  let previousNewFiber: Fiber | null = null;
​
  // 旧的Fiber节点
  let oldFiber = currentFirstChild;
  let lastPlacedIndex = 0;
  let newIdx = 0;
  let nextOldFiber = null;
​
  // 初次加载的情况:
  if (oldFiber === null) {
    // 多子节点的 创建:创建第一个的child,完成之后退出创建,返回resultingFirstChild结果为第一个child
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
      if (newFiber === null) {
        continue;
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      // previousNewFiber === null,表示为第一次循环:
      if (previousNewFiber === null) {
        // TODO: Move out of the loop. This only happens for the first run.
        // 设置第一个新建的Fiber节点, 为最后返回的child内容
        resultingFirstChild = newFiber;
      } else {
        // 否则表示不是第一次循环,将新建的节点设置为上一个节点的兄弟节点
        previousNewFiber.sibling = newFiber;
      }
      // 更新上一个新的节点
      previousNewFiber = newFiber;
    }
​
    // 退出当前函数,返回创建完成的第一个child
    return resultingFirstChild;
  }
  
  ...
​
  return resultingFirstChild;
}

创建多子节点的逻辑主要是一个for循环结构:循环newChildren数组,调用createChild方法创建子节点。在第一个子节点创建完成之后,就会将它赋值为返回的child节点,循环结构里只会赋值一次,所以reconcileChildrenArray方法返回值就是第一个子节点。

js 复制代码
resultingFirstChild = newFiber;

在后续的子节点创建过程中,会将新建的子节点设置为上一个节点的兄弟节点:

js 复制代码
// 设置兄弟节点
previousNewFiber.sibling = newFiber;

处理完成后,从第一个子节点开始,就可以通过每个节点的sibling属性指向下一个兄弟节点,一直到最后一个兄弟节点。

总结(案例)

上面我们以HostFiber节点为例,解析了它的beginWork工作流程。下面以前面的案例为目标,加快一组Fiber节点的beginWork工作:

js 复制代码
// 从hostFiber根节点开始,【一组beginWork工作】
hostFiber => 
fun App()  => 
div.App => 
div react源码调试 =>
null

hostFiber根节点开始,react的深度优先遍历算法,会一直创建到再无Child子节点。

js 复制代码
<div>react源码调试</div>

当【div react源码调试】所对应的Fiber节点创建完成后,它会继续开启属于它的beginWork工作,来创建它的子节点,但是因为它已经没有子节点了,所以在reconcileChildFibers方法中,它最终会返回一个null

js 复制代码
function reconcileChildFibers() {
   ...
    
  // newChild为null时,表示没有子节点了,会返回一个null
  return deleteRemainingChildren(returnFiber, currentFirstChild); 
}

然后一直向上return,直到回到performUnitOfWork方法中:

js 复制代码
function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate; // 上一次
​
  // 开始循环构建
  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    // 此next为null
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }
​
  if (next === null) {
    // next为null, 则会进入此节点对应的completeWork工作【归的阶段】
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

此时,nextnull时,将进入此Fiber节点的completeWork工作【归的阶段】。

关于FiberTree的具体执行流程,也就是beginWorkcompleteWork的交替执行,可以查看4小节中的【执行流程图】。

4,completeWork

completeUnitOfWork

completeWork代表一个Fiber节点的结束工作,completeUnitOfWorkcompleteWork阶段的入口函数。

查看completeUnitOfWork源码:

js 复制代码
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
​
function completeUnitOfWork(unitOfWork: Fiber): void {
  # 取出当前的Fiber节点
  let completedWork = unitOfWork;
    
  // do while循环,先执行一次循环体,再判断
  do {
​
    // 取出节点 对应的current
    const current = completedWork.alternate;
    // 取出节点的父级节点
    const returnFiber = completedWork.return;
    if ((completedWork.flags & Incomplete) === NoFlags) {
      # 正常的completeWork工作
      let next = completeWork(current, completedWork, subtreeRenderLanes);
      // 归阶段,是自下而上的收缩,从div react源码解析 到fun App() 到 hostFiber节点
      if (next !== null) {
        // Completing this fiber spawned new work. Work on that next.
        workInProgress = next;
        return;
      }
    } else {
      # 异常处理
      // 因为某些异常原因返回了null,但是Fiber工作还没有完成,
      const next = unwindWork(current, completedWork, subtreeRenderLanes);
      // fiber树还没有完成,不能重置lanes
      if (next !== null) {
        next.flags &= HostEffectMask;
        workInProgress = next;
        return;
      }
      ...
    }
​
    # 一个节点的completeWork工作完成后,寻找是否存在兄弟节点
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      // 如果兄弟节点存在,则设置为新的workInProgress,跳出当前函数,回到performUnitOfWork方法中
      // 这将开始sibling兄弟节点的beginWork工作【自上而下】
      workInProgress = siblingFiber;
      return;
    }
​
    // 【自下而上的处理】
    // 如果不存在兄弟节点,则将父级节点设置为completedWork,开始父级节点的completedWork工作
    completedWork = returnFiber;
    // Update the next thing we're working on in case something throws.
    // 更新workInProgress,可能会开启新的beginWork工作
    workInProgress = completedWork;
​
    // 最后当completedWork为null时,就会跳出循环,这时候workInProgress也为null,代表整颗FiberTree就创建完成了
  } while (completedWork !== null);
​
  // We've reached the root.
  if (workInProgressRootExitStatus === RootInProgress) {
    // 等于完成的状态
    workInProgressRootExitStatus = RootCompleted;
  }
}

completeUnitOfWork方法中重点就是一个do while循环:

js 复制代码
completedWork !== null

这个循环的条件是completedWork不等于null,即它是一个有效的Fiber节点。

接着上一节的执行结果,当前的completedWork为【div react源码调试】所对应的Fiber节点:

取出当前Fiber节点的current和父级节点,进入当前的节点的completedWork工作。

关于completeUnitOfWork方法中剩下的逻辑等待后续讲解。

completedWork

查看completedWork方法:

js 复制代码
// packages\react-reconciler\src\ReactFiberCompleteWork.new.js
​
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
​
  // 取出新的props
  const newProps = workInProgress.pendingProps;
  popTreeContext(workInProgress);
  # 根据tag值,进行不同的逻辑处理
  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      bubbleProperties(workInProgress);
      return null;
​
    // 类组件
    case ClassComponent: {
      const Component = workInProgress.type;
      if (isLegacyContextProvider(Component)) {
        popLegacyContext(workInProgress);
      }
      bubbleProperties(workInProgress);
      return null;
    }
          
    // 根节点的处理
    case HostRoot: {}
    # 原生dom节点处理
    case HostComponent: {}
    
    ... 
  }   
}

completedWork方法源码量非常大,但是它的逻辑依然是熟悉的switch case结构,根据Fiber节点的tag值进行不同的组件处理。

不同的tag值对应不同组件生成的Fiber节点,前面已经讲解过。

当前Fiber节点的tag值对应着HostComponent

注意:这里completeWork阶段的工作我们以HostComponent类型【dom元素】为案例,因为beginWork阶段的重点是常规组件的加载逻辑,而completeWork阶段的重点是HostComponent类型组件的处理【创建真实的DOM结构,初始化属性及样式】。

js 复制代码
case HostComponent: {
  popHostContext(workInProgress);
  // 获取#root 根元素
  const rootContainerInstance = getRootHostContainer();
  // 获取dom元素类型 'div'
  const type = workInProgress.type;
  if (current !== null && workInProgress.stateNode != null) {
    # 更新元素
    updateHostComponent(
      current,
      workInProgress,
      type,
      newProps,
      rootContainerInstance,
    );
​
    if (current.ref !== workInProgress.ref) {
      markRef(workInProgress);
    }
  } else {
      
    # 初次加载
    if (!newProps) {
      if (workInProgress.stateNode === null) {
        throw new Error(
          'We must have new props for new mounts. This error is likely ' +
            'caused by a bug in React. Please file an issue.',
        );
      }
​
      // This can happen when we abort work.
      // props冒泡
      bubbleProperties(workInProgress);
      return null;
    }
​
    # 创建元素流程:
    // 创建HostComponent类型的FIber节点 对应的真实dom元素
    const instance = createInstance(
      type,
      newProps,
      rootContainerInstance,
      currentHostContext,
      workInProgress,
    );
    // 添加子元素:将下一级的dom元素挂载到instance上面
    // 【由于appendAllChildren方法的存在,当completeWork到hostFiber时,已经形成了一颗离屏的真实DOM树】
    appendAllChildren(instance, workInProgress, false, false);
    // 在【div react源码解析】节点上: 存储它对应的真实dom内容
    workInProgress.stateNode = instance;
    // 执行finalizeInitialChildren方法,完成属性的初始化:styles,innerHTML, 文本类型的children
    if (finalizeInitialChildren(instance,type,newProps,rootContainerInstance,currentHostContext,)) {
      markUpdate(workInProgress);
    }
​
    if (workInProgress.ref !== null) {
      // If there is a ref on a host node we need to schedule a callback
      markRef(workInProgress);
    }
  }
​
  // props冒泡,将flags冒泡
  bubbleProperties(workInProgress);
  return null;
}

HostComponent组件的处理逻辑主要有两部分内容:

  • 根据是否存在current判断为更新元素或者创建元素。
  • flags冒泡。

当前我们的Fiber节点为初次加载,currentnull,所以会进入else分支,进行真实dom元素的创建。

js 复制代码
const newProps = workInProgress.pendingProps;

newProps取之于节点的pendingProps属性,所以当前节点存在有效的props,并且这里我们也可以看出本文内容被存储到了children属性中。

继续下面的内容,调用createInstance创建真实的DOM元素。

createInstance

查看createInstance方法:

js 复制代码
// packages\react-dom\src\client\ReactDOMHostConfig.js
​
// 创建真实的dom元素
export function createInstance(
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
  internalInstanceHandle: Object,
): Instance {
​
  const parentNamespace = ((hostContext: any): HostContextProd);
​
  # 创建dom元素实例
  const domElement: Instance = createElement(
    type,
    props,
    rootContainerInstance,
    parentNamespace,
  );
  precacheFiberNode(internalInstanceHandle, domElement);
  // 更新fiber节点的props
  updateFiberProps(domElement, props);
  # 返回dom元素
  return domElement;
}

createInstance方法的作用是创建真实的DOM元素,最后返回创建完成的DOM元素。

继续查看createElement方法:

js 复制代码
// packages\react-dom\src\client\ReactDOMComponent.js
​
export function createElement(
  type: string, 
  props: Object,
  rootContainerElement: Element | Document | DocumentFragment,
  parentNamespace: string,
): Element {
  // 获取Document对象
  const ownerDocument: Document = getOwnerDocumentFromRootContainer(
    rootContainerElement,
  );
  let domElement: Element;
  let namespaceURI = parentNamespace;
  if (namespaceURI === HTML_NAMESPACE) {
    namespaceURI = getIntrinsicNamespace(type);
  }
  if (namespaceURI === HTML_NAMESPACE) {
    if (type === 'script') {
        ...
    } else if (type.is === 'string') {
        ...
    } else {
        # 常规的dom元素
        domElement = ownerDocument.createElement(type);
    }
  } else {
      // svg/xml等元素的创建
      domElement = ownerDocument.createElementNS(namespaceURI, type);
  }
  
  # 返回创建完成的真实dom元素
  return domElement;
}

createElement方法主要是根据type参数,使用原生方法document.createElement(type)创建真实的DOM元素。

如果namespaceURI不等于HTML命名空间,则会使用document.createElementNS()创建svg/xml等元素。

当前传递typediv类型:

js 复制代码
const domElement = document.createElement('div')

最后返回创建完成的div元素。

继续回到前面的createInstance方法:

js 复制代码
export function createInstance() {
  
  ...
​
  # 创建的dom元素实例
  const domElement: Instance = createElement();
  // 将当前的Fiber节点存储到domElement元素上
  precacheFiberNode(internalInstanceHandle, domElement);
  updateFiberProps(domElement, props);
  # 返回dom元素
  return domElement;
}

在创建完成真实的dom元素之后,做了两个挂载操作:

  • 调用precacheFiberNode方法,在domElement元素上定义一个react内部属性存储当前的Fiber节点。
  • 调用updateFiberProps方法,在domElement元素上定义一个react内部属性存储当前Fiber节点的pendingProps

最后返回domElement

回到HostComponent处理逻辑中:

js 复制代码
case HostComponent: {
    ...
    # 真实的dom元素创建完成
    const instance = createInstance();
    # 添加子元素:将下一级的dom元素挂载到instance上面
    // 【由于appendAllChildren方法的存在,当completeWork到hostFiber时,已经形成了一颗离屏的真实DOM树】
    appendAllChildren(instance, workInProgress, false, false);
    // 在div react源码解析 节点上 存储它对应的真实dom元素
    workInProgress.stateNode = instance;
    // 执行finalizeInitialChildren方法,完成属性的初始化:styles,innerHTML, 文本类型的children,
    if (finalizeInitialChildren(instance,type,newProps,rootContainerInstance,currentHostContext,)) {
      markUpdate(workInProgress);
    }
​
    if (workInProgress.ref !== null) {
      // If there is a ref on a host node we need to schedule a callback
      markRef(workInProgress);
    }
  }
​
  # props冒泡,将flags冒泡
  bubbleProperties(workInProgress);
  return null;
}

在真实的dom创建完成之后,继续调用appendAllChildren,它的作用是将下一级的dom内容添加到当前的DOM元素之中。

appendAllChildren

查看appendAllChildren方法:

js 复制代码
appendAllChildren = function(
  parent: Instance,
  workInProgress: Fiber,
  needsVisibilityToggle: boolean,
  isHidden: boolean,
) {
    
  let node = workInProgress.child;
  // 有child才会处理,无child,说明自身就已经最下面的节点了
  while (node !== null) {
    if (node.tag === HostComponent || node.tag === HostText) {
      appendInitialChild(parent, node.stateNode);
    } else if (node.tag === HostPortal) {
    
    } else if (node.child !== null) {
      node.child.return = node;
      node = node.child;
      continue;
    }
    if (node === workInProgress) {
      return;
    }
    while (node.sibling === null) {
      if (node.return === null || node.return === workInProgress) {
        return;
      }
      node = node.return;
    }
    node.sibling.return = node.return;
    node = node.sibling;
  }
};

根据appendAllChildren方法名称其实就已经知道它的作用了,将当前FIber节点下的所有子节点对应的dom内容添加到当前的DOM元素之中。

js 复制代码
let node = workInProgress.child;

取出当前FIber节点的子节点【fisrtChild】,只有存在子节点才会开启while循环,当前workInProgress为【div react源码调试】所对应的Fiber节点,它已经是最下面的节点【即不存在子节点】,所以这里的appendAllChildren方法没有处理内容。

案例解读

举例: 假如当前的节点为【state: {count}】所对应的FIber节点:

前面我们已经知道,因为这个节点内容存在动态数据,所以它还会继续创建两个子节点。

当这个FIber节点进入appendAllChildren方法时,它存在child就可以正常的进入while循环:

【state:】所对应的Fiber节点tag值为6,代表它是文本节点内容,满足条件判断:

js 复制代码
appendInitialChild(parent, node.stateNode);
// 即是原始dom方法
div.appendChild(text)

执行原生DOM方法,将文本子节点添加到父级节点之下。当这行代码执行完成之后,【state: {count}】所对应的FIber节点,它所对应的DOM元素就已经发生了变化:

js 复制代码
<div></div>
// 变成了
<div>state: </div>

继续回到while循环中:

js 复制代码
  // 有child才会处理,无child,说明自身就已经最下面的节点了
  while (node !== null) {
    if (node.tag === HostComponent || node.tag === HostText) {
      // 添加子节点
      appendInitialChild(parent, node.stateNode);
    } else if (node.tag === HostPortal) {
    
    } else if (node.child !== null) {
      node.child.return = node;
      node = node.child;
      continue;
    }
    if (node === workInProgress) {
      return;
    }
    while (node.sibling === null) {
      if (node.return === null || node.return === workInProgress) {
        return;
      }
      node = node.return;
    }
    node.sibling.return = node.return;
    # 更新node 为兄弟节点
    node = node.sibling;
  }

在第一个子节点处理完成后,更新node对象:

js 复制代码
node = node.sibling;

将下一个兄弟节点设置为最新的node,如果node存在,说明当前还存在其他子节点,继续循环添加子节点。

开启第二次循环,继续执行原生DOM方法添加子节点:

当这行代码执行完成之后,【state: {count}】所对应的FIber节点,它所对应的DOM元素就再次更新:

js 复制代码
<div>state: </div>
// 变成了
<div>state: 1</div>

到此,该FIber节点对应DOM元素的更新就已经处理完成,因为它只有两个子节点。

js 复制代码
while (node.sibling === null) {
  if (node.return === null || node.return === workInProgress) {
    return;
  }
}

再遇到此判断就会触发return关键字,跳出appendAllChildren方法,表示执行完成。

当这个方法执行完成后,在HostComponent处理逻辑中,就会将最新的DOM内容存储到当前Fiber节点的stateNode属性中。

上面通过一个案例,帮助我们更好的理解了appendAllChildren方法的内容。

下面我们再次回到之前HostComponent处理逻辑中:

js 复制代码
case HostComponent: {
    ...
    const instance = createInstance();
    appendAllChildren(instance, workInProgress, false, false);
    workInProgress.stateNode = instance;
    # 完成DOM元素属性的初始化,以及原生事件的绑定
    if (finalizeInitialChildren(instance,type,newProps,rootContainerInstance,currentHostContext,)) {
      markUpdate(workInProgress);
    }
​
    if (workInProgress.ref !== null) {
      // If there is a ref on a host node we need to schedule a callback
      markRef(workInProgress);
    }
  }
​
  # props冒泡,将flags冒泡
  bubbleProperties(workInProgress);
  return null;
}

在DOM内容更新完成后,然后调用了一个finalizeInitialChildren方法,这个方法有两个作用:

  • 初始化当前DOM元素的原生属性。
  • 添加原生事件绑定。
finalizeInitialChildren

查看finalizeInitialChildren方法:

js 复制代码
// packages\react-dom\src\client\ReactDOMHostConfig.js
​
export function finalizeInitialChildren(
  domElement: Instance,
  type: string,
  props: Props,
  rootContainerInstance: Container,
  hostContext: HostContext,
): boolean {
    
  // 初始化dom属性【重点】
  setInitialProperties(domElement, type, props, rootContainerInstance);
  switch (type) {
    case 'button':
    case 'input':
    case 'select':
    case 'textarea':
      return !!props.autoFocus;
    case 'img':
      return true;
    default:
      return false;
  }
}

finalizeInitialChildren方法中最重要的逻辑就是setInitialProperties方法的调用,而下面的switch case结构只是针对几个特殊的DOM元素,打上一个更新的标记,用于在commit阶段之中对特殊的元素进行预加载处理,比如输入聚焦或者图片加载。

这里还是回到之前的【div react源码调试】所对应的Fiber节点的执行逻辑。

上一小节展示【state: {count}】所对应的FIber节点案例,只是为了方便解读appendAllChildren方法内容。

setInitialProperties方法传递了四个参数:

  • 当前DOM元素。
  • 元素类型div
  • 定义的props
  • #root应用根节点元素。

继续查看setInitialProperties方法:

js 复制代码
// packages\react-dom\src\client\ReactDOMComponent.js
​
export function setInitialProperties(
  domElement: Element,
  tag: string,
  rawProps: Object,
  rootContainerElement: Element | Document | DocumentFragment,
): void {
  const isCustomComponentTag = isCustomComponent(tag, rawProps);
​
  let props: Object;
  // 针对一些特殊的DOM元素,添加默认事件绑定
  switch (tag) {
    case 'dialog':
      listenToNonDelegatedEvent('cancel', domElement);
      listenToNonDelegatedEvent('close', domElement);
      props = rawProps;
      break;
    case 'iframe':
    case 'object':
    case 'embed':
      listenToNonDelegatedEvent('load', domElement);
      props = rawProps;
      break;
    case 'video':
    case 'audio':
      for (let i = 0; i < mediaEventTypes.length; i++) {
        listenToNonDelegatedEvent(mediaEventTypes[i], domElement);
      }
      props = rawProps;
      break;
    case 'source':  // ...
      break;
    case 'img':
    case 'image':
    case 'link': // ...
      break;
    case 'details': // ...
      break;
    case 'input': // ...
      break;
    case 'option': // ...
      break;
    case 'select': // ...
      break;
    case 'textarea': // ...
      break;
    default:
      props = rawProps;
  }
  // props合法校验
  assertValidProps(tag, props);
  # 绑定真实的属性和事件
  setInitialDOMProperties(
    tag,
    domElement,
    rootContainerElement,
    props,
    isCustomComponentTag,
  );
}

setInitialProperties方法内容主要有三个逻辑步骤:

  • 针对一些特殊的DOM元素,添加必要的事件绑定。
  • 对传入的props进行合法性校验。
  • 调用setInitialDOMProperties方法,根据props中的内容,对当前的DOM元素进行真实的属性和事件绑定。

setInitialDOMProperties方法具体的内容就不再展开了,这部分逻辑涉及到了React合成事件系统的原理,里面的内容比较复杂,感兴趣的可以自行了解。

finalizeInitialChildren方法做一个总结:根据props中的内容,对当前的DOM元素进行真实的属性和事件绑定。

根据前面的图例展示:当前的Fiber节点对应的DOM元素为div,且只有一个children属性,并没有其他属性和事件绑定,所以

这里仅仅是设置了元素的textContent文本属性之后,就没有其他逻辑执行了。

上面的内容执行完成后,再次回到HostComponent处理逻辑中,最后执行了一个bubbleProperties方法,这个方法的作用是执行flags冒泡,让子节点的flags向上冒泡一层。经过冒泡处理后,访问任意一个Fiber节点的subTreeFlags属性,都可以获取它的子节点所需要执行的副作用操作。

flags标记对应不同的副作用操作,用于commit阶段的renderer渲染器执行对应的操作逻辑。

总结(案例)

上面HostComponent相关的一系列逻辑执行完成后,即代表了一个Fiber节点的completedWork工作就此完成,这里以HostComponent为例,主要是处理DOM的相关逻辑才是completedWork工作的主要内容,其他的组件类型基本都只是处理flags冒泡。

在一个Fiber节点的completedWork工作完成后,最终都是回到completeUnitOfWork方法中。

下面我们再次回到此方法,解析后续的逻辑执行:

js 复制代码
// packages\react-reconciler\src\ReactFiberWorkLoop.new.js
​
// 完成当前的Fiber节点,然后移动到兄弟节点,如果没有,则返回到父级节点
function completeUnitOfWork(unitOfWork: Fiber): void {
  # 取出当前的Fiber节点
  let completedWork = unitOfWork;
    
  # do while循环,先执行一次循环体,再判断
  do {
​
    // 取出节点 对应的current
    const current = completedWork.alternate;
    // 取出节点的父级节点
    const returnFiber = completedWork.return;
    if ((completedWork.flags & Incomplete) === NoFlags) {
      # 正常的completeWork工作
      let next = completeWork(current, completedWork, subtreeRenderLanes);
      // 归阶段,是自下而上的收缩,从div react源码解析 到fun App() 到 hostFiber节点
      if (next !== null) {
        // Completing this fiber spawned new work. Work on that next.
        workInProgress = next;
        return;
      }
    } else {
      # 异常处理
      // 因为某些异常原因返回了null,但是Fiber工作还没有完成,
      const next = unwindWork(current, completedWork, subtreeRenderLanes);
      // fiber树还没有完成,不能重置lanes
      if (next !== null) {
        next.flags &= HostEffectMask;
        workInProgress = next;
        return;
      }
      ...
    }
​
    # 一个节点的completeWork工作完成后,寻找是否存在兄弟节点
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // 如果兄弟节点存在,则设置为新的workInProgress,跳出当前函数,回到performUnitOfWork方法中
      // 这将开始sibling兄弟节点的beginWork工作【自上而下】
      workInProgress = siblingFiber;
      return;
    }
​
    // 【自下而上的处理】
    // 如果不存在兄弟节点,则将父级节点设置为新的completedWork,开始父级节点的completedWork工作
    completedWork = returnFiber;
    // Update the next thing we're working on in case something throws.
    // 更新workInProgress,可能会开启新的beginWork工作
    workInProgress = completedWork;
​
    // 最后当completedWork为null时,就会跳出循环,这时候workInProgress也为null,代表整颗FiberTree就创建完成了
  } while (completedWork !== null);
​
  // We've reached the root.
  if (workInProgressRootExitStatus === RootInProgress) {
    // 等于完成的状态
    workInProgressRootExitStatus = RootCompleted;
  }
}

当前Fiber完成completedWork工作后,返回了一个null

js 复制代码
let next = completeWork(current, completedWork, subtreeRenderLanes); // null

然后继续进入下面的程序,首先判断当前节点是否存在兄弟节点sibling

js 复制代码
const siblingFiber = completedWork.sibling;

根据我们准备的案例,很明显它是存在兄弟节点的:

存在兄弟节点,则将兄弟节点设置为最新的workInProgress,触发return关键字,退出completeUnitOfWork函数。

这将会回到workLoopSync方法中,此时workInProgress为有效的内容,将开始【MyFun函数组件】Fiber节点的benginWork工作。

到处,关于一个Fiber节点的beginWorkcompletedWork工作流程就基本解析完成了。

最后再对react应用创建FiberTree的过程做一个总结:react中FiberTree的创建过程是采用DFS深度优先遍历,从beginWork方法开始深度探索阶段【自上而下】,也就是处理每层第一个childfirstChild】,直到没有子节点。然后进入completeWork方法【归的阶段,自下而上】,在这里继续处理一些该Fiber节点的工作,在这个过程中两个工作会交替执行beginWork工作从HostFiber开始,最终由completeWork回到HostFiber,因为HostFiberFiberTree的根节点,它没有父级节点,所以在completeWork方法最后会返回一个null,赋值给workInProgress。上面所有的工作执行完成后,最后会回到workLoopSync方法中,此时workInProgressnull,就会跳出循环代表本次FiberTree已经创建完成。

执行流程图

下面是案例对应的完整执行流程图【可与前面的FiberTree结构图对比】:

5,结束语

上面我们已经学习了FiberTree整体的创建过程,下一章节我们将进入react的commit阶段,开始真正的页面渲染以及相关副作用的执行过程。

关于类组件和函数组件的具体加载过程,可以查看《React18.2x源码解析:组件的加载过程》。

相关推荐
GISer_Jing2 小时前
前端面试通关:Cesium+Three+React优化+TypeScript实战+ECharts性能方案
前端·react.js·面试
落霞的思绪3 小时前
CSS复习
前端·css
咖啡の猫5 小时前
Shell脚本-for循环应用案例
前端·chrome
百万蹄蹄向前冲8 小时前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5818 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路8 小时前
GeoTools 读取影像元数据
前端
ssshooter9 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友9 小时前
【Node.js】什么是Node.js
javascript·后端·node.js
Jerry9 小时前
Jetpack Compose 中的状态
前端
dae bal10 小时前
关于RSA和AES加密
前端·vue.js