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源码解析:组件的加载过程》。

相关推荐
m0_748255261 小时前
前端安全——敏感信息泄露
前端·安全
鑫~阳2 小时前
html + css 淘宝网实战
前端·css·html
Catherinemin2 小时前
CSS|14 z-index
前端·css
2401_882727574 小时前
低代码配置式组态软件-BY组态
前端·后端·物联网·低代码·前端框架
NoneCoder4 小时前
CSS系列(36)-- Containment详解
前端·css
anyup_前端梦工厂4 小时前
初始 ShellJS:一个 Node.js 命令行工具集合
前端·javascript·node.js
5hand4 小时前
Element-ui的使用教程 基于HBuilder X
前端·javascript·vue.js·elementui
GDAL5 小时前
vue3入门教程:ref能否完全替代reactive?
前端·javascript·vue.js
六卿5 小时前
react防止页面崩溃
前端·react.js·前端框架
z千鑫5 小时前
【前端】详解前端三大主流框架:React、Vue与Angular的比较与选择
前端·vue.js·react.js