返回 blog
2023年12月07日
23 分钟阅读

React 渲染原理

React源码,启动!

React揭秘重点笔记(无上下文逻辑)

整个SchedulerReconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给RendererRenderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作。 如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历(继续遍历!因为打断了while循环,但记住了上一次中断的fiber节点,就可以等浏览器有时间的时候,重新while循环,从上一次的wipFiber开始) performUnitOfWork方法会创建下一个Fiber节点并赋值给workInProgress,并将workInProgress与已创建的Fiber节点连接起来构成Fiber树(performUnitOfWork里面做的事不少哦!要把ReactElement转成fiber,然后赋值给wipFiber,再把这个fiber跟已有的fiber连接起来。连接起来之后就会自然形成一颗Fiber树🌳

Q

  • scheduler 是怎么知道浏览器当前帧是否还有空闲时间的?
  • 每次的workloop中做了什么事?会创建dom吗(只是创建,不包含写到浏览器)?
  • scheduler把任务交给了reconciler?那scheduler的任务是从哪里来的?
  • scheduler 和 reconciler 有可能被中断,比如有更高优先级的任务来了。如何理解这个中断?中断的是什么?是怎么做到的中断?正在进行的task会被中断吗?
  • render(scheduler + reconciler)是一个递归的过程,是全部执行完了,再一次性全部commit吗?

React原理

最近看了哪些文章

说实话,经过这几天疯狂学习React,接收的输入不统一,导致我脑子很混乱,所以决定从头开始整理一下,把我对React的理解写下来,不一定对 我打算先从宏观入手,先搞清楚react渲染的流程,包括mount和update,在这个阶段,不出意外,我会有许多疑问 然后再带着疑问,去深入了解其中的原理

基于 React18.2.0 版本

启动

我们web开发,入口函数都是 ReactDOM.createRoot().render ,所以就从这里开始分析

export function createRoot(container, options) {
  if (!isValidContainer(container)) {
    throw new Error("createRoot(...): Target container is not a DOM element.");
  }

  // ...

  // createContainer是createRoot的核心函数
  // 其中,container 是我们传入的DOM,通常是:document.getElementById('root')
  // 这里的root,称为 FiberRootNode,在一个React应用中,只有一个 FiberRootNode
  // FiberRootNode 是最顶部的Fiber节点
  const root = createContainer(
    container,
    ConcurrentRoot,
    null,
    isStrictMode,
    concurrentUpdatesByDefaultOverride,
    identifierPrefix,
    onRecoverableError,
    transitionCallbacks
  );

  /**
    就是给container加了个内部属性[internalContainerInstanceKey],
    用来存储 FiberRootNode
    
    const internalContainerInstanceKey = "__reactContainer$" + randomKey
    export function markContainerAsRoot(hostRoot, node) {
   	 node[internalContainerInstanceKey] = hostRoot;
  	}
  */
  markContainerAsRoot(root.current, container);


  const rootContainerElement =
    container.nodeType === COMMENT_NODE ? container.parentNode : container;
  // 监听所有事件,看起来应该是React的事件绑定机制相关
  listenToAllSupportedEvents(rootContainerElement);

  /**
  	function ReactDOMRoot(internalRoot) {
      this._internalRoot = internalRoot;
    }

    ReactDOMRoot.prototype.render = function() {

      // 核心逻辑
      // 这里就是ReactDOM 跟 reconciler 的链接处
      updateContainer(children, root, null, null);
    }
  */
  // 所以返回的对象中有个render方法,也就是 createRoot().render()
  return new ReactDOMRoot(root);
}

进入到 createContainer ,看看 FiberRootNode 是怎么生成的

export function createFiberRoot(
  containerInfo,
  tag,
  hydrate,
  initialChildren,
  hydrationCallbacks,
  isStrictMode,
  concurrentUpdatesByDefaultOverride,
  identifierPrefix,
  onRecoverableError,
  transitionCallbacks
) {
  // 这里就是创建了唯一的FiberRootNode
  // 详情看下图
  const root = new FiberRootNode(
    containerInfo,
    tag,
    hydrate,
    identifierPrefix,
    onRecoverableError
  );

  // 这里是创建hostRootFiber
  // host指的是宿主
  // 这个其实就是Fiber树的根节点,因为之后会把这个fiber树渲染到宿主环境中
  const uninitializedFiber = createHostRootFiber(
    tag,
    isStrictMode,
    concurrentUpdatesByDefaultOverride
  );

  // 应用根节点中绑定了hostRootFiber
  root.current = uninitializedFiber;
  // hostRootFiber中也保存了FiberRootNode
  // 这两个步骤就把FiberRootNode 跟 hostRootFiber 绑定起来了
  uninitializedFiber.stateNode = root;

  if (enableCache) {
    // ...
    
    const initialState = {
      element: initialChildren,
      isDehydrated: hydrate,
      cache: initialCache,
    };
    // 初始化state
    uninitializedFiber.memoizedState = initialState;
  } else {
    // ...
  }

  // 初始化HostRootFiber的updateQueue
  initializeUpdateQueue(uninitializedFiber);

  // 返回FiberRootNode
  return root;
}

FiberRootNode中有一些比较重要的属性:

  • current:指向hostRootFiber,也就是当前渲染的Fiber
  • containerInfo:传入的 #root 的DOM信息
  • pendingLanes: 待更新任务优先级
  • expirationTimes:任务过期时间数组,初始化都是-1(已过期)

image.png

HostRootFiber,是一个普通的Fiber节点,也有一些比较重要的属性

  • tag:fiber的类型,根据 ReactElement 组件的type生成,有以下25种image.png
  • key:节点的key
  • mode: 二进制,继承至父节点,影响本 fiber 节点及其子树中所有节点. 与 react 应用的运行模式有关(有 ConcurrentMode, BlockingMode, NoMode 等选项).

image.png

  • child:直接子节点
  • return:直接父节点
  • sibling:兄弟节点
  • memoizedState:hook链表
  • memoizedProps:props
  • pendingProps:等待处理的props

image.pngimage.png

以上就是ReactDOM的启动的核心流程

ReactDOM render

我们通过现象来看一下整体的调用栈 image.png 从调用栈可以很直观看到,ReactDOMRoot.render 是起点,最后渲染的 FunctionComponent(业务代码)是终点 得益于React源码区分清晰,从文件名大致可以看出函数属于哪部分

updateContainer

render之前的流程已经梳理完毕,接下来,我们从 updateContainer讲起

ReactDOMRoot.prototype.render =
  function (children) {
    const root = this._internalRoot;
    // ...

    // render 调用 updateContainer
    // children 是用户传参,通常是一个jsx
    // root 是之前的 `#root` DOM
    updateContainer(children, root, null, null);
  };

image.png

export function updateContainer(element, container) {
	// element 是传入 render 的 ReactElement(经过babel,从jsx转成了ReactElement方法)
  
  // container是FiberRootNode
  // container.current 就是 HostFiberRoot
  const current = container.current;
  
  // 获取HostFiberRoot的更新优先级
  // 这里比较重要,涉及到了优先级相关的概念

  // 32,对应 DefaultLane
  const lane = requestUpdateLane(current);

  // ...

  // 设置fiber.updateQueue
  const update = createUpdate(lane);
  // Caution: React DevTools currently depends on this property
  // being called "element".
  update.payload = { element };

  // TODO:好像是向更新队列中推入了一个更新
  const root = enqueueUpdate(current, update, lane);
  
  if (root !== null) {
    const eventTime = requestEventTime();
    
    // 重点!每次更新一定会进入到这个方法中
    // 从上方的调用栈图中,也可以看到这个方法
    // 这个方法是开启Fiber的更新调度任务
    // 这里面应该就是开始构造fiber树了
    
    // 进入reconciler运作流程中的`输入`环节
    scheduleUpdateOnFiber(root, current, lane, eventTime);

    entangleTransitions(root, current, lane);
  }

  return lane;
}

初探优先级

分类

在React中有三类优先级

  • lane:fiber优先级,车道模型
    1. Lane 是二进制常量,利用位掩码的特性,在频繁运算时占用内存少,计算速度快
    2. Lane是单任务,Lanes是多任务
    3. 每个Lane都有其对应的优先级
export const TotalLanes = 31;

export const NoLanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane = /*                          */ 0b0000000000000000000000000000000;

export const SyncHydrationLane = /*               */ 0b0000000000000000000000000000001;
export const SyncLane = /*                        */ 0b0000000000000000000000000000010;

export const InputContinuousHydrationLane = /*    */ 0b0000000000000000000000000000100;
export const InputContinuousLane = /*             */ 0b0000000000000000000000000001000;

export const DefaultHydrationLane = /*            */ 0b0000000000000000000000000010000;
export const DefaultLane = /*                     */ 0b0000000000000000000000000100000;

export const SyncUpdateLanes = /*                */ 0b0000000000000000000000000101010;

const TransitionHydrationLane = /*                */ 0b0000000000000000000000001000000;
const TransitionLanes = /*                       */ 0b0000000011111111111111110000000;
const TransitionLane1 = /*                        */ 0b0000000000000000000000010000000;
const TransitionLane2 = /*                        */ 0b0000000000000000000000100000000;
const TransitionLane3 = /*                        */ 0b0000000000000000000001000000000;
const TransitionLane4 = /*                        */ 0b0000000000000000000010000000000;
const TransitionLane5 = /*                        */ 0b0000000000000000000100000000000;
const TransitionLane6 = /*                        */ 0b0000000000000000001000000000000;
const TransitionLane7 = /*                        */ 0b0000000000000000010000000000000;
const TransitionLane8 = /*                        */ 0b0000000000000000100000000000000;
const TransitionLane9 = /*                        */ 0b0000000000000001000000000000000;
const TransitionLane10 = /*                       */ 0b0000000000000010000000000000000;
const TransitionLane11 = /*                       */ 0b0000000000000100000000000000000;
const TransitionLane12 = /*                       */ 0b0000000000001000000000000000000;
const TransitionLane13 = /*                       */ 0b0000000000010000000000000000000;
const TransitionLane14 = /*                       */ 0b0000000000100000000000000000000;
const TransitionLane15 = /*                       */ 0b0000000001000000000000000000000;
const TransitionLane16 = /*                       */ 0b0000000010000000000000000000000;

const RetryLanes = /*                            */ 0b0000111100000000000000000000000;
const RetryLane1 = /*                             */ 0b0000000100000000000000000000000;
const RetryLane2 = /*                             */ 0b0000001000000000000000000000000;
const RetryLane3 = /*                             */ 0b0000010000000000000000000000000;
const RetryLane4 = /*                             */ 0b0000100000000000000000000000000;

export const SomeRetryLane = RetryLane1;

export const SelectiveHydrationLane = /*          */ 0b0001000000000000000000000000000;

const NonIdleLanes = /*                          */ 0b0001111111111111111111111111111;

export const IdleHydrationLane = /*               */ 0b0010000000000000000000000000000;
export const IdleLane = /*                        */ 0b0100000000000000000000000000000;

export const OffscreenLane = /*                   */ 0b1000000000000000000000000000000;
  • event:事件优先级
// 其实事件优先级就是Lane,只是为了更好的表达语意
export const DiscreteEventPriority = SyncLane;
export const ContinuousEventPriority = InputContinuousLane;
export const DefaultEventPriority = DefaultLane;
export const IdleEventPriority = IdleLane;
  • scheduler:调度优先级,独立包,可以不依赖 React 使用
export const NoPriority = 0;

// 优先级越高,对应数字越小
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

转化

优先级有个转化关系 image.png lane转成event,event再转成scheduler 优先级是React实现时间切片、中断渲染、suspense异步渲染的基础 正是因为每个任务有不同优先级,React才能hold住大型前端项目,紧急的任务先渲染,不紧急的后渲染,用户感知卡顿的几率极大减少

requestUpdateLane

export function requestUpdateLane(fiber) {
  // ...

  // 获取此次更新的优先级(默认是NoLane:0)
  const updateLane = getCurrentUpdatePriority();
  if (updateLane !== NoLane) {
    return updateLane;
  }

  // 没有显示设置优先级的话,会走到这里
  const eventLane = getCurrentEventPriority();
  // return 32
  return eventLane;
}
// 默认返回 DefaultEventPriority:32
export function getCurrentEventPriority() {
  const currentEvent = window.event;
  if (currentEvent === undefined) {
    return DefaultEventPriority;
  }
  return getEventPriority(currentEvent.type);
}

createUpdate

export function createUpdate(lane) {
  const update = {
    lane, // 优先级

    tag: UpdateState, // 0
    payload: null, // 更新内容, updateContainer会进行赋值操作
    callback: null, // 回调,updateContainer会进行赋值操作

    next: null, // 通过next指向下一个update对象形成链表
  };
  return update;
}

流程

reconciler阶段

此处先归纳一下react-reconciler包的主要作用, 将主要功能分为 4 个方面:

  1. 输入: 暴露api函数(如: scheduleUpdateOnFiber), 供给其他包(如react包)调用
  2. 注册调度任务: 与调度中心(scheduler包)交互, 注册调度任务task, 等待任务回调
  3. 执行任务回调: 在内存中构造出fiber树, 同时与与渲染器(react-dom)交互, 在内存中创建出与fiber对应的DOM节点
  4. 输出: 与渲染器(react-dom)交互, 渲染DOM节点

ReactFiberWorkLoop流程图

scheduleUpdateOnFiber

export function scheduleUpdateOnFiber(root, fiber, lane, eventTime) {
  // ...

  // Mark that the root has a pending update.
  // 给FiberRootNode标记pendingLanes
  markRootUpdated(root, lane, eventTime);

  // ...

  // 关键函数:注册调度任务
  ensureRootIsScheduled(root, eventTime);

  // ...
}

image.png

ensureRootIsScheduled

// 使用此功能可以为 FiberRootNode 调度任务(任务就是构造fiber树)
// 每个 FiberRootNode 只有一个任务;
// 如果任务已调度,我们将检查以确保 
// 现有任务的优先级 与 FiberRootNode 正在处理的下一个任务的 优先级相同
// 每次更新和退出任务之前都会调用此函数
function ensureRootIsScheduled(root, currentTime) {
  if (...) {
  } else {
    // ...
    
    // React 跟 Scheduler 交互的 入口
    // scheduleCallback 就是 Scheduler 包暴露的api
    // 传入的回调是 performConcurrentWorkOnRoot
    // 也就是说,需要被调度的任务是 performConcurrentWorkOnRoot
    newCallbackNode = scheduleCallback(
      // 调度优先级,为了简化逻辑,我们暂不考虑优先级
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }

  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

这里就是调度Fiber树构造的入口了,需要让 Scheduler执行调度任务是 performConcurrentWorkOnRoot 走到这里了,我们先暂停。但心里记住,Scheduler执行的回调是performConcurrentWorkOnRoot 为什么呢,因为 Scheduler 跟 React 是解耦的。接下来我们要知道,单纯的 Scheduler 内部做了什么

Scheduler

梳理了单纯的 Scheduler 的整体运行逻辑,我们先着重关注“执行任务回调”,所谓的任务回调,就是react传入给scheduler的一个任务,而这个任务就是 performConcurrentWorkOnRoot

function ensureRootIsScheduled() {
  // ...
  newCallbackNode = scheduleCallback(
  schedulerPriorityLevel,
  performConcurrentWorkOnRoot.bind(null, root),
	);
  // ...
}

可以看到 reconciler 把 performConcurrentWorkOnRoot传入了Scheduler,等待回调。

performXXXWorkOnRoot

这个方法是构造Fiber树的入口 performSyncWorkOnRoot的逻辑很清晰, 分为 3 部分:

  1. fiber 树构造
  2. 异常处理: 有可能 fiber 构造过程中出现异常
  3. 调用输出
function performConcurrentWorkOnRoot(root, didTimeout) {
const originalCallbackNode = root.callbackNode;

  const shouldTimeSlice =
    !includesBlockingLane(root, lanes) &&
    !includesExpiredLane(root, lanes) &&
    (disableSchedulerTimeoutInWorkLoop || !didTimeout);

  // 1. 构造fiber树
  // 可能是并发模式、也可能是同步模式
  // 如果触发了时间切片,就是并发模式
  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);

  // 如果是同步模式,走到这里的时候,fiber树就已经被构造好了

  if (
    includesSomeLane(
      workInProgressRootIncludedLanes,
      workInProgressRootUpdatedLanes,
    )
  ) {
    // 如果在render过程中产生了新的update, 且新update的优先级与最初render的优先级有交集
    // 那么最初render无效, 丢弃最初render的结果, 等待下一次调度
    // 刷新帧栈
    prepareFreshStack(root, NoLanes);
  } else if (exitStatus !== RootIncomplete) {
    // 2. 异常处理: 有可能fiber构造过程中出现异常
    if (exitStatus === RootErrored) {
      // ...
    }.
	
    // 3. 输出: 渲染fiber树()
    finishConcurrentRender(root, exitStatus, lanes);
  }

  // 退出前再次检测, 是否还有其他更新, 是否需要发起新调度
  ensureRootIsScheduled(root, now());
  if (root.callbackNode === originalCallbackNode) {
    // 渲染被阻断, 返回一个新的performConcurrentWorkOnRoot函数, 等待下一次调用
    return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}

一个performConcurrentWorkOnRoot,其实就是一个task 在一个task中,最核心的是构造fiber树(全部构造完毕后同步一次性commit) 我们通过之前的学习,知道了一个task是在workLoop中循环执行的,以实现时间切片和异步“可中断渲染” 而fiber树的构造也是需要中断,所以fiber树的构造多半也是一个workLoop循环,但本质跟scheduler不太一样

fiber树构造

fiber树的构造入口有两个

  • 并发构造 renderRootConcurrent
  • 同步构造 renderRootSync

这两个方法的本质相似,我们先从更简单的 renderRootSync 讲起

function renderRootSync(root: FiberRoot, lanes: Lanes) {
  const prevExecutionContext = executionContext;
  executionContext |= RenderContext;
  const prevDispatcher = pushDispatcher();
  // ...
  if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
    // 刷新帧栈,什么是帧栈?之后再说
    prepareFreshStack(root, lanes);
  }
  // 进入了一个循环,catch的时候继续重新循环
  do {
    try {
      // 重点
      // 同步的阻塞的workLoop
      workLoopSync();

      // 实际上正常执行完一次workLoop就会被break出去
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);
  
  resetContextDependencies(); // 重置上下文信息

  executionContext = prevExecutionContext;
  popDispatcher(prevDispatcher);
  ...
  // 置空标识当前render阶段结束, 没有正在执行的render过程
  workInProgressRoot = null;
  workInProgressRootRenderLanes = NoLanes;
  return workInProgressRootExitStatus;
}

自上而下渲染Root的时候,又进入到了一个workLoopSync循环 这是一个同步的循环,逻辑很简单,如果 workInProgress 不为空,就一直loop循环

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

image.png 可以看到,第一次循环的wip是rootFiber,也就是fiber树的根节点 image.png image.png 然后在循环过程中,通过 beginWork构造单个fiber节点

workLoopSync的过程是一个深度优先遍历(DFS)之 递归 递归递归,从名字上看来,递归分为 “递” 和 “归”。递 是向下探寻,归 是向上回溯

function Node() {
  this.name = '';
  this.children = [];
}

function dfs(node) {
  console.log('探寻阶段: ', node.name);
  node.children.forEach((child) => {
    dfs(child);
  });
  console.log('回溯阶段: ', node.name);
}

此处为了简明, 已经将源码中与 dfs 无关的旁支逻辑去掉

function workLoopSync() {
  // 1. 最外层循环, 保证每一个节点都能遍历, 不会遗漏
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function performUnitOfWork(unitOfWork: Fiber): void {
  const current = unitOfWork.alternate;
  let next;
  // 2. beginWork是向下探寻阶段
  next = beginWork(current, unitOfWork, subtreeRenderLanes);
  if (next === null) {
    // 3. completeUnitOfWork 是回溯阶段
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    let next;
    // 3.1 回溯并处理节点
    next = completeWork(current, completedWork, subtreeRenderLanes);
    if (next !== null) {
      // 判断在处理节点的过程中, 派生出新的节点
      workInProgress = next;
      return;
    }
    const siblingFiber = completedWork.sibling;
    // 3.2 判断是否有兄弟节点
    if (siblingFiber !== null) {
      workInProgress = siblingFiber;
      return;
    }
    // 3.3 没有兄弟节点 继续回溯
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
}

假设有以下的组件结构:

class App extends React.Component {
  render() {
    return (
      <div className="app">
        <header>header</header>
        <Content />
        <footer>footer</footer>
      </div>
    );
  }
}

class Content extends React.Component {
  render() {
    return (
      <React.Fragment>
        <p>1</p>
        <p>2</p>
        <p>3</p>
      </React.Fragment>
    );
  }
}

export default App;

则可以得出遍历路径: 注意⚠️

  • 每个fiber节点是最小工作单元,也是中断、恢复的边界
  • sibling、return 是伴随 第一次beginWork子节点的时候生成的,比如上图中的 3、6(带有括号的标记表示sibling生成阶段)
  • 优先遍历直接child,然后遍历sibling兄弟节点

接下来我们具体到一个Fiber,看看是如何构造出来的

单fiber构造

fiber是由ReactElement转化而来的,最后会输出为被renderer认识的东西(比如DOM) 我们先暂时放下 beginWork,等会再回来。先了解一下什么是ReactElement

jsx大家每天都在使用,为什么呢?因为很好用,像一颗糖,甜到心里 jsx就是一种语法糖,

const Item = <div>123</div>

这样的一个jsx,会被react-babel编译成:

这里不严格,现在的编译结果通常是 jsxRuntime,而非createElement(老版本), 但是为了便于理解,我们这里还是使用 createElement

React.createElement('div', { children: '123' })

createElement,顾名思义,就是创建一个ReactElement。这个方法很简单:

export function createElement(type, config, children) {
  // ...
  
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props
  );
}

const ReactElement = function (type, key, ref, self, source, owner, props) {
  const element = {
    // This tag allows us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type,
    key,
    ref,
    props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };
  
  return element;
};

image.png 相信大家都很熟悉这些字段了,如果不熟悉的话,你可以试试打印一个ReactComponent,看看结果

这样的一个Element,就是构造Fiber的原型 image.png

好的,我们接着回到 beginWork

// ... 省略部分无关代码
function performUnitOfWork(unitOfWork: Fiber): void {
  // unitOfWork就是被传入的workInProgress
  const current = unitOfWork.alternate;
  let next;
  // 这里入参current,涉及到双fiber,这里不细讲
  // current是目前渲染的fiber树,wip是正在构建的fiber树(在这里命名是unitOfWork)
  // 简单来说就是内存中有最多两个fiber树,为了在渲染的时候更快(不需要新建,只需要替换)
  next = beginWork(current, unitOfWork, subtreeRenderLanes);
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (next === null) {
    // 如果没有派生出新的节点, 则进入completeWork阶段, 传入的是当前unitOfWork
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

在上文中,我们讲到了,beginWork是下探阶段,completeUnitOfWork是回溯阶段 这两个阶段共同完成了一个fiber节点的构建,所有的fiber节点,则构成了一颗fiber树

探寻阶段 beginWork

主要做了:

  1. 根据ReactElement对象,创建出fiber对象,设置了return、sibling等
  2. 设置fiber.flags,标记fiber节点的增、删、改状态,在回溯阶段处理
  3. 给有状态的fiber设置stateNode,比如class组件的stateNode就是类实例。(无状态的如宿主组件(div、span等)在回溯阶段设置stateNode为DOM实例)
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const updateLanes = workInProgress.lanes;
  if (current !== null) {
    // update逻辑, 首次render不会进入
  } else {
    didReceiveUpdate = false;
  }
  // 1. 设置workInProgress优先级为NoLanes(最高优先级)
  workInProgress.lanes = NoLanes;
  // 2. 根据workInProgress节点的类型, 用不同的方法派生出子节点
  switch (
    workInProgress.tag // 只保留了本例使用到的case
  ) {
    case FunctionComponent: {
      child = updateFunctionComponent(null, workInProgress, Component, resolvedProps, renderLanes);
      return child;
    }
    case ClassComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText:
      return updateHostText(current, workInProgress);
    case Fragment:
      return updateFragment(current, workInProgress, renderLanes);
  }
}

updateXXX方法就是构建fiber的核心方法,它们的主要逻辑:

  1. 根据 fiber.pendingProps, fiber.updateQueue 这些输入状态,计算出输出状态 fiber.memoizedState(这里跟state相关了)
  2. 获取到ReactElement对象
    1. function 类型的fiber节点
      1. 传入正确的props状态,执行 function,获取到返回的ReactElement
    2. class 类型的fiber节点
      1. 创建class的实例,执行render方法,获取返回的ReactElement
    3. HostComponent 原生组件(div、span等 )的fiber节点
      1. 获取 pendingProps.children
  3. 根据2中的ReactElement对象,调用 reconcileChildren 生成fiber子节点。如果ReactElement是数组,就依次生成fiber节点(只会生成直接的子节点,一级),并且设置fiber节点之间的关系,sibling、return、child。BTW,这里面涉及到的东西特别多,很容易陷入源码出不来

举例,看看里面具体做了什么

  • fiber树的根节点 HostRootFiber节点
// 省略与本节无关代码
function updateHostRoot(current, workInProgress, renderLanes) {
  // 1. 状态计算, 更新整合到 workInProgress.memoizedState中来
  const updateQueue = workInProgress.updateQueue;
  const nextProps = workInProgress.pendingProps;
  const prevState = workInProgress.memoizedState;
  const prevChildren = prevState !== null ? prevState.element : null;
  cloneUpdateQueue(current, workInProgress);
  // 遍历updateQueue.shared.pending, 提取有足够优先级的update对象, 计算出最终的状态 workInProgress.memoizedState
  processUpdateQueue(workInProgress, nextProps, null, renderLanes);
  const nextState = workInProgress.memoizedState;
  // 2. 获取下级`ReactElement`对象
  const nextChildren = nextState.element;
  const root: FiberRoot = workInProgress.stateNode;
  if (root.hydrate && enterHydrationState(workInProgress)) {
    // ...服务端渲染相关, 此处省略
  } else {
    // 3. 根据`ReactElement`对象, 调用`reconcileChildren`生成`Fiber`子节点(只生成`次级子节点`)
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  }
  return workInProgress.child;
}
  • 普通DOM标签节点,如 div、span等
// ...省略部分无关代码
function updateHostComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
) {
  // 1. 状态计算, 由于HostComponent是无状态组件, 所以只需要收集 nextProps即可, 它没有 memoizedState
  const type = workInProgress.type;
  const nextProps = workInProgress.pendingProps;
  const prevProps = current !== null ? current.memoizedProps : null;
  // 2. 获取下级`ReactElement`对象
  let nextChildren = nextProps.children;
  const isDirectTextChild = shouldSetTextContent(type, nextProps);

  if (isDirectTextChild) {
    // 如果子节点只有一个文本节点, 不用再创建一个HostText类型的fiber
    nextChildren = null;
  } else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
    // 特殊操作需要设置fiber.flags
    workInProgress.flags |= ContentReset;
  }
  // 特殊操作需要设置fiber.flags
  markRef(current, workInProgress);
  // 3. 根据`ReactElement`对象, 调用`reconcileChildren`生成`Fiber`子节点(只生成`次级子节点`)
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

image.png image.png image.png image.png image.png image.png image.png image.png 至此,完成了根据ReactElement构建fiber对象 最后返回到createChild,我想说一下这段代码,这里的

let resultingFirstChild = null;
let previousNewFiber = null;

let oldFiber = currentFirstChild;

if (oldFiber === null) {
  // mount阶段
  for (; newIdx < newChildren.length; newIdx++) {
    // 根据ReactElement创建fiber
    const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
    if (newFiber === null) {
      continue;
    }
    lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
    if (previousNewFiber === null) {
      // 这是第一次循环,说明该节点是子节点中的第一个节点
      resultingFirstChild = newFiber;
    } else {
      // 此时是子节点的非首节点,当前节点是上一个节点的兄弟
      // 给上一个节点设置兄弟节点
      previousNewFiber.sibling = newFiber;
    }
    // 指针右移
    previousNewFiber = newFiber;
  }
	// 返回第一个子节点
	// 这个节点中是链表的头指针,其中包含了所有兄弟fiber节点的信息
	return resultingFirstChild;
}

image.png 可以看出来,sibling指针指向下一个兄弟fiber节点 至此,实际上把children都构造成了fiber image.png 最后返回了直接子节点,用于下一次 performUnitOfWork performUnitOfWork是最小的执行单元,无法中断,也就是说,每次时间切片中,只要走到了 performUnitOfWork方法,就至少会构造一个fiber节点

回溯阶段 completeUnitOfWork

在beginWork探寻阶段,已经生成了fiber节点。completeUnitOfWork阶段,主要是处理fiber节点 核心逻辑如下:

  1. 调用 completeWork
    1. 给原生组件的 fiber节点(HostComponent、HostText)创建DOM实例,设置 fiber.stateNode 局部状态。比如 tag=HostComponent, HostText节点: fiber.stateNode 指向这个 DOM 实例
    2. 给DOM节点设置属性、绑定事件
    3. 设置 fiber.flags 标记(增删改)
  2. 把当前fiber对象的副作用队列(firstEffect、lastEffect)添加到父节点的副作用队列中
  3. 识别beginWork阶段设置的fiber.flags,判断当前fiber是否有副作用(增删改),如果有,就将当前fiber加入到父节点的effects队列,commit阶段处理
function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  // 外层循环控制并移动指针(`workInProgress`,`completedWork`等)
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    if ((completedWork.flags & Incomplete) === NoFlags) {
      let next;
      // 1. 处理Fiber节点, 会调用渲染器(调用react-dom包, 关联Fiber节点和dom对象, 绑定事件等)
      next = completeWork(current, completedWork, subtreeRenderLanes); // 处理单个节点
      if (next !== null) {
        // 如果派生出其他的子节点, 则回到`beginWork`阶段进行处理
        workInProgress = next;
        return;
      }
      // 重置子节点的优先级
      resetChildLanes(completedWork);
      if (
        returnFiber !== null &&
        (returnFiber.flags & Incomplete) === NoFlags
      ) {
        // 2. 收集当前Fiber节点以及其子树的副作用effects
        // 2.1 把子节点的副作用队列添加到父节点上
        if (returnFiber.firstEffect === null) {
          returnFiber.firstEffect = completedWork.firstEffect;
        }
        if (completedWork.lastEffect !== null) {
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
          }
          returnFiber.lastEffect = completedWork.lastEffect;
        }
        // 2.2 如果当前fiber节点有副作用, 将其添加到子节点的副作用队列之后.
        const flags = completedWork.flags;
        if (flags > PerformedWork) {
          // PerformedWork是提供给 React DevTools读取的, 所以略过PerformedWork
          if (returnFiber.lastEffect !== null) {
            returnFiber.lastEffect.nextEffect = completedWork;
          } else {
            returnFiber.firstEffect = completedWork;
          }
          returnFiber.lastEffect = completedWork;
        }
      }
    } else {
      // 异常处理, 本节不讨论
    }

    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // 如果有兄弟节点, 返回之后再次进入`beginWork`阶段
      workInProgress = siblingFiber;
      return;
    }
    // 移动指针, 指向下一个节点
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
  // 已回溯到根节点, 设置workInProgressRootExitStatus = RootCompleted
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
}
completeWork
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;
  popTreeContext(workInProgress);
  // 这里跟beiginWork一致的,都是通过判断Fiber的tag标签去判断执行什么逻辑
  switch (workInProgress.tag) {
    case IndeterminateComponent: ...
    case LazyComponent: ...
    case SimpleMemoComponent: ...
    case FunctionComponent: ...
    case ForwardRef: ...
    case Fragment: ...
    case Mode: ...
    case Profiler: ...
    case ContextConsumer: ...
    case MemoComponent: ...
    case ClassComponent: ....
    case HostRoot: ...
    case HostComponent: ....
    case HostText: ...
    case SuspenseComponent: ....
    case HostPortal: ...
    case ContextProvider: ...
    case IncompleteClassComponent: ...
    case SuspenseListComponent: ....
    case OffscreenComponent:
    case LegacyHiddenComponent: ....
    case CacheComponent: ...
  }
}
case HostComponent: {
  popHostContext(workInProgress);
  // 拿到根节点的DOM,见下图
  const rootContainerInstance = getRootHostContainer();
  // 拿到类型
  const type = workInProgress.type; 

  if (current !== null && workInProgress.stateNode != null) {
   // ...
  } else {
   // ... 
   // mount
   const currentHostContext = getHostContext();  // 拿到上下文信息
   const wasHydrated = popHydrationState(workInProgress);
   if (wasHydrated) { ... } else {
     // 根据Fiber创建对应的DOM结构信息
     const instance = createInstance(
       type,
       newProps,
       rootContainerInstance,
       currentHostContext,
       workInProgress,
     );
     // 把子树中的DOM对象append到本节点的DOM对象之后
     appendAllChildren(instance, workInProgress, false, false);
     // 设置stateNode属性,指向DOM对象
     workInProgress.stateNode = instance;  
     // ...

     if (
        // 设置DOM对象的属性, 绑定事件等
        finalizeInitialChildren(
          instance,
          type,
          newProps,
          rootContainerInstance,
          currentHostContext,
        )
      ) {
        // 设置fiber.flags标记(Update)
        markUpdate(workInProgress);
      }
      if (workInProgress.ref !== null) {
        // 设置fiber.flags标记(Ref)
        markRef(workInProgress);
      }
      return null;
   }
}

一个ReactElement经过beginWork和completeUnitOfWork后,就完成了单个fiber的构造 回溯全部完成后,一颗fiber树也就构造好了

代码很干涩,接下来图示整个fiber树构建的过程

图解mount过程

什么是帧栈? 在React源码中, 每一次执行fiber树构造(也就是调用performSyncWorkOnRoot或者performConcurrentWorkOnRoot函数)的过程, 都需要一些全局变量来保存状态(比如workInProgresworkInProgressRoot等 如果从单个变量来看, 它们就是一个个的全局变量. 如果将这些全局变量组合起来, 它们代表了当前fiber树构造的活动记录. 通过这一组全局变量, 可以还原fiber树构造过程(比如时间切片的实现过程,fiber树构造过程被打断之后需要还原进度, 全靠这一组全局变量). 所以每次fiber树构造是一个独立的过程, 需要独立的一组全局变量, 在React内部把这一个独立的过程封装为一个栈帧stack(简单来说就是每次构造都需要独立的空间)

待构造的示例代码

class App extends React.Component {
  componentDidMount() {
    console.log(`App Mount`);
    console.log(`App 组件对应的fiber节点: `, this._reactInternals);
  }
  render() {
    return (
      <div className="app">
        <header>header</header>
        <Content />
      </div>
    );
  }
}

class Content extends React.Component {
  componentDidMount() {
    console.log(`Content Mount`);
    console.log(`Content 组件对应的fiber节点: `, this._reactInternals);
  }
  render() {
    return (
      <>
        <p>1</p>
        <p>2</p>
      </>
    );
  }
}
export default App;

构造前: 在上文已经说明, 进入循环构造前会调用prepareFreshStack刷新栈帧, 在进入fiber树构造循环之前, 保持这这个初始化状态: image.png

performUnitOfWork第 1 次下探(只执行beginWork):

  • 执行前: workInProgress指针指向HostRootFiber.alternate对象, 此时current = workInProgress.alternate指向fiberRoot.current是非空的(初次构造, 只在根节点时, current非空).
  • 执行过程: 调用updateHostRoot
    • reconcileChildren阶段, 向下构造次级子节点fiber(<App/>), 同时设置子节点(fiber(<App/>))fiber.flags |= Placement
  • 执行后: 返回下级节点fiber(<App/>), 移动workInProgress指针指向子节点fiber(<App/>)

image.png

performUnitOfWork第 2 次下探(只执行beginWork):

  • 执行前: workInProgress指针指向fiber(<App/>)节点, 此时current = null
  • 执行过程: 调用updateClassComponent
    • 本示例中, class 实例存在生命周期函数componentDidMount, 所以会设置fiber(<App/>)节点workInProgress.flags |= Update
    • 需要注意classInstance.render()在本步骤执行后, 虽然返回了render方法中所有的ReactElement对象, 但是随后reconcileChildren只构造次级子节点
    • reconcileChildren阶段, 向下构造次级子节点div
  • 执行后: 返回下级节点fiber(div), 移动workInProgress指针指向子节点fiber(div)

image.png

performUnitOfWork第 3 次下探 (只执行beginWork):

  • 执行前: workInProgress指针指向fiber(div)节点, 此时current = null
  • 执行过程: 因为tag是div,所以调用updateHostComponent
    • reconcileChildren阶段, 向下构造次级子节点(本示例中, div有 2 个次级子节点)
      • 构建所有同级子节点fiber,并设置相邻关系(sibling、return、child)
  • 执行后: 返回下级节点fiber(header), 移动workInProgress指针指向子节点fiber(header)

image.png

performUnitOfWork第 4 次 下探(执行beginWorkcompleteUnitOfWork):

  • beginWork执行前: workInProgress指针指向fiber(header)节点, 此时current = null
  • beginWork执行过程: 调用updateHostComponent
    • 本示例中header的子节点是一个直接文本节点,设置nextChildren = null(直接文本节点并不会被当成具体的fiber节点进行处理, 而是在宿主环境(父组件)中通过属性进行设置. 所以无需创建HostText类型的 fiber 节点, 同时节省了向下遍历开销.).
    • 由于nextChildren = null, 经过reconcileChildren阶段处理后, 返回值也是null
  • beginWork执行后: 由于下级节点为null, 所以进入completeUnitOfWork(unitOfWork)函数, 传入的参数unitOfWork实际上就是workInProgress(此时指向fiber(header)节点)

image.png 第 1 次回溯:

  1. 执行completeWork函数
    • 创建fiber(header)节点对应的DOM实例, 并append子节点的DOM实例
    • 设置DOM属性, 绑定事件等(本示例中, 节点fiber(header)没有事件绑定)
  2. 上移副作用队列: 由于本节点fiber(header)没有副作用(fiber.flags = 0), 所以执行之后副作用队列没有实质变化(目前为空).
  3. 向上回溯: 由于还有兄弟节点, 把workInProgress指针指向下一个兄弟节点fiber(<Content/>), 退出completeUnitOfWork.

image.png

performUnitOfWork第 5 次 下探(执行beginWork):

  • 执行前:workInProgress指针指向fiber(<Content/>)节点.
  • 执行过程: 这是一个class类型的节点, 与第 2 次调用逻辑一致.
  • 执行后: 返回下级节点fiber(p), 移动workInProgress指针指向子节点fiber(p)

image.png

performUnitOfWork第 6 次下探 (执行beginWorkcompleteUnitOfWork

  • 与第 4 次调用中创建fiber(header)节点的逻辑一致. 先后会执行beginWorkcompleteUnitOfWork, 最后构造 DOM 实例, 并将把workInProgress指针指向下一个兄弟节点fiber(p)

第2次回溯 image.png

performUnitOfWork第 7 次下探(执行beginWorkcompleteUnitOfWork):

  • beginWork执行过程: 与上次调用中创建fiber(p)节点的逻辑一致
  • completeUnitOfWork执行过程: 以fiber(p)为起点, 向上回溯

第3次回溯

  1. 执行completeWork函数: 创建fiber(p)节点对应的DOM实例, 并append子树节点的DOM实例
  2. 上移副作用队列: 由于本节点fiber(p)没有副作用, 所以执行之后副作用队列没有实质变化(目前为空).
  3. 向上回溯: 由于没有兄弟节点, 把workInProgress指针指向父节点fiber(<Content/>)

image.png

第4次回溯

  1. 执行completeWork函数: class 类型的节点不做处理
  2. 上移副作用队列:
    • 本节点fiber(<Content/>)flags标志位有改动(completedWork.flags > PerformedWork), 将本节点添加到父节点(fiber(div))的副作用队列之后(firstEffectlastEffect属性分别指向副作用队列的首部和尾部).
  3. 向上回溯: 把workInProgress指针指向父节点fiber(div)

image.png

第5次回溯

  1. 执行completeWork函数: 创建fiber(div)节点对应的DOM实例, 并append子树节点的DOM实例
  2. 上移副作用队列:
    • 本节点fiber(div)的副作用队列不为空, 将其拼接到父节点fiber<App/>的副作用队列后面.
  3. 向上回溯: 把workInProgress指针指向父节点fiber(<App/>)

image.png

第6次回溯:

  1. 执行completeWork函数: class 类型的节点不做处理
  2. 上移副作用队列:
    • 本节点fiber(<App/>)的副作用队列不为空, 将其拼接到父节点fiber(HostRootFiber)的副作用队列上.
    • 本节点fiber(<App/>)flags标志位有改动(completedWork.flags > PerformedWork), 将本节点添加到父节点fiber(HostRootFiber)的副作用队列之后.
    • 最后队列的顺序是子节点在前, 本节点在后
  3. 向上回溯: 把workInProgress指针指向父节点fiber(HostRootFiber)

image.png

第7次回溯:

  1. 执行completeWork函数: 对于HostRoot类型的节点, 初次构造时设置workInProgress.flags |= Snapshot
  2. 向上回溯: 由于父节点为空, 无需进入处理副作用队列的逻辑. 最后设置workInProgress=null, 并退出completeUnitOfWork

image.png

到此整个fiber树构造循环已经执行完毕, 拥有一棵完整的fiber树, 并且在fiber树的根节点上挂载了副作用队列, 副作用队列的顺序是层级越深子节点越靠前。这也是为什么子组件的生命周期更先执行

renderRootSync函数退出之前, 会重置workInProgressRoot = null, 表明没有正在进行中的render. 且把最新的fiber树挂载到fiberRoot.finishedWork上. 这时整个 fiber 树的内存结构如下(注意fiberRoot.finishedWorkfiberRoot.current指针,在commitRoot阶段会进行处理): image.png

至此,一颗fiber的mount构建过程就全部执行完毕了。但这只是mount,还有update更新的流程 update相对mount稍微复杂些,我们进入更复杂的流程之前,先思考一下

  1. fiber的可中断渲染如何实现?
  2. 如果某个element有1000万个兄弟节点,会导致fiber构造卡顿吗?

fiber update

update 涉及到了 双fiber缓冲,这里讲一下什么是双fiber缓冲 缓冲是一种经典的空间换时间的优化方式,为什么React采用了双fiber,其中肯定有个理由是 「为了渲染更丝滑」。至于其他原因,我们后面再探究 在React项目中,内存中最多存在两颗fiber树,分别是

  • 正在内存中构建的fiber树 —— workInProgress
  • 渲染在页面上的fiber树 —— fiberRootNode.current

在以上的内容中,我们只涉及到了一颗workInProgress树,因为mount的时候,页面还未渲染fiber image.png 可以看到,此时页面上的fiber树还是空的。经过了commit render(暂时不用理解这个)后,就变成了这样: image.png 可以看到,

注意:

  • mount或update之前,有个创建wip的阶段
    • 建立了wip和current之间的联系(相互引用)

image.png

  • 这时候还没有child、sibling那些,因为还没构造过fiber树

image.png

  • 如果是update,wip指向current.alternate(经历了mount的相互绑定,可以在current中取到wip了,当然,也可以在wip中取到current)
  • 因为经历了一次构造了,current已经是一颗fiber树了,有fiber之间的关系了

image.png React应用的根节点(FiberRootNode)通过使current指针在不同Fiber树的rootFiber间切换来完成current Fiber树指向的切换

即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。(提问:那么此时的WIP树是什么呢?)

每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新

image.png 这是React技术揭秘中的一段话,我并不苟同。 我认为,update的时候,不需要重新构建一颗新的workInProgress树,而是复用current.alternate,这是一颗已经构建好的树,不过是老的而已

讲完双fiber,我们再说fiber的update update的示例代码

import React from 'react';

class App extends React.Component {
  state = {
    list: ['A', 'B', 'C'],
  };
  onChange = () => {
    this.setState({ list: ['C', 'A', 'X'] });
  };
  componentDidMount() {
    console.log(`App Mount`);
  }
  render() {
    return (
      <>
        <Header />
        <button onClick={this.onChange}>change</button>
        <div className="content">
          {this.state.list.map((item) => (
            <p key={item}>{item}</p>
          ))}
        </div>
      </>
    );
  }
}

class Header extends React.PureComponent {
  render() {
    return (
      <>
        <h1>title</h1>
        <h2>title2</h2>
      </>
    );
  }
}
export default App;

初次渲染完成之后, 与fiber相关的内存结构如下(后文以此图为基础, 演示对比更新过程): image.png

一次update,还是会走一次reconciler的流程。reconciler流程的入口是 scheduleUpdateOnFiber,所以我们在debug的时候可以把断点打在这里 image.png

React有哪些更新方式呢?

3 种更新方式

如要主动发起更新, 有 3 种常见方式:

  1. Class组件中调用setState.
  2. Function组件中调用hook对象暴露出的dispatchAction(useState、useReducer都会返回dispatchAction)
  3. container节点上重复调用ReactDOM的render(官网示例),这种方式很少见

我觉得React关于状态更新的API,设计得很好。尽量减少用户入口,减轻了用户的开发心智负担,排查问题也很方便

setState
Component.prototype.setState = function (partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

在fiber的beginWork阶段,class组件的初始化就完成之后,this.update如下:

const classComponentUpdater = {
  isMounted,
  enqueueSetState(inst, payload, callback) {
    // 1. 获取class实例对应的fiber节点,见下图
    const fiber = getInstance(inst);
    const lane = requestUpdateLane(fiber);

    // 2. 根据优先级,创建update对象
    const update = createUpdate(lane);
    update.payload = payload;

    // 3. 将update对象添加到当前Fiber节点的updateQueue队列当中
    const root = enqueueUpdate(fiber, update, lane);
    
    // 4. 又见 scheduleUpdateOnFiber
    // 进入reconciler流程(输入环节)
    scheduleUpdateOnFiber(root, fiber, lane, eventTime);
    }
  },
  enqueueReplaceState(inst, payload, callback) {
    // ...
  },
  enqueueForceUpdate(inst, callback) {
    // ...
  },
};

获取当前组件的fiber image.png 创建update对象 image.png

dispatchAction

此处只是为了对比dispatchActionsetState. 有关hook原理的深入分析, 在hook 原理章节中详细讨论.

function类型组件中, 如果使用hook(useState), 则可以通过hook api暴露出的dispatchAction来更新

function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  // 1. 创建update对象
  const eventTime = requestEventTime();
  const lane = requestUpdateLane(fiber); // 确定当前update对象的优先级
  const update: Update<S, A> = {
    lane,
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };
  // 2. 将update对象添加到当前Hook对象的updateQueue队列当中
  const pending = queue.pending;
  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
  // 3. 请求调度, 进入reconciler运作流程中的`输入`环节.
  scheduleUpdateOnFiber(fiber, lane, eventTime); // 传入的lane是update优先级
}

跟setState差不多

重复调用 render
import ReactDOM from 'react-dom';
function tick() {
  const element = (
    <div>
      <h1>Hello, world!</h1>
      <h2>It is {new Date().toLocaleTimeString()}.</h2>
    </div>
  );
  ReactDOM.render(element, document.getElementById('root'));
}
setInterval(tick, 1000);

这种方式,每次都会重新启动React应用,调用路径包含了:updateContainer-->scheduleUpdateOnFiber

所以以上三种方式,都一定会进到scheduleUpdateOnFiber中,那我们接下来就讲 scheduleUpdateOnFiber 是如何处理update的

update

function scheduleUpdateOnFiber(root) {
  // ...
	ensureRootIsScheduled(root)
  // ...
}
function ensureRootIsScheduled(root, currentTime) {
  const existingCallbackNode = root.callbackNode;

  // 饥饿问题(不用管)
  markStarvedLanesAsExpired(root, currentTime);

  // 获取优先级,Demo里面是个点击,得到的结果是 2
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

  const newCallbackPriority = getHighestPriorityLane(nextLanes);
  const existingCallbackPriority = root.callbackPriority;
  
  let newCallbackNode;
  if (includesSyncLane(newCallbackPriority)) {
    if (root.tag === LegacyRoot) {
    } else {
      // performSyncWorkOnRoot 很眼熟了吧,就是构造fiber树的入口
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
  } 

  // ...
}

performSyncWorkOnRoot -> renderRootSync -> workLoopSync -> performUnitOfWork -> beginWork -> completeUnitOfWor 看起来跟mount阶段的调用栈差不多的,在update阶段,最关键的是找出diff,更新fiber diff的核心代码 reconcileChildFibers 在beginWork中执行, image.png 是不是也很眼熟,就是上文说的 updateXXX,遇到遇到class 组件、或function组件这种带有内部state的,reconciler会执行,然后在这个过程中根据updateQueue更新组件,然后在 reconcilerChildFibers 里面做diff diff算法这里不提了,可以之后单独讲 image.png image.png image.pngimage.png image.png image.png diff完了之后,返回到 performUnitOfWork 执行下一次fiber构造 update fiber 跟 mount 时初始化fiber,有一个很重要的相同点,就是在遍历到某个节点的子节点是数组时,会把这个子节点的所有兄弟节点都一起处理了。 所以debug的时候可以看到,当wip是button,其相邻的兄弟节点中的pendingProps就已经是新的props了 image.png

构造完了fiber,剩下的就交给 commit 阶段了

我们还是看看图,更直观

图解update过程

待update的代码:

import React from 'react';

class App extends React.Component {
  state = {
    list: ['A', 'B', 'C'],
  };
  onChange = () => {
    this.setState({ list: ['C', 'A', 'X'] });
  };
  componentDidMount() {
    console.log(`App Mount`);
  }
  render() {
    return (
      <>
        <Header />
        <button onClick={this.onChange}>change</button>
        <div className="content">
          {this.state.list.map((item) => (
            <p key={item}>{item}</p>
          ))}
        </div>
      </>
    );
  }
}

class Header extends React.PureComponent {
  render() {
    return (
      <>
        <h1>title</h1>
        <h2>title2</h2>
      </>
    );
  }
}
export default App;

mount之后,已经构造好了一颗fiber树了,这棵树是 current 树。current树的 alternate 指向 wip。 如图: image.png

performUnitOfWork第 1 次调用(只执行beginWork下探):

  • 执行前: workInProgress指向HostRootFiber.alternate对象, 此时current = workInProgress.alternate指向当前页面对应的fiber树.
  • 执行后: 返回被clone的下级节点fiber(<App/>), 移动workInProgress指向子节点fiber(<App/>)

image.png

performUnitOfWork第 2 次调用(只执行beginWork下探):

  • 执行前: workInProgress指向fiber(<App/>)节点, 且current = workInProgress.alternate有值
  • 执行过程:
    • 调用updateClassComponent()函数中, 调用reconcileChildren()生成下级子节点.
  • 执行后: 返回下级节点fiber(<Header/>), 移动workInProgress指向子节点fiber(<Header/>)

image.png

performUnitOfWork第 3 次调用(执行beginWork下探 和completeUnitOfWork回溯):

beginWork下探阶段:

  • beginWork执行前: workInProgress指向fiber(<Header/>), 且current = workInProgress.alternate有值
  • beginWork执行后: 因为没有子节点了,所以进入completeUnitOfWork(unitOfWork)函数

image.png

completeUnitOfWork 回溯阶段:

  • completeUnitOfWork执行前: workInProgress指向fiber(<Header/>)
  • completeUnitOfWork执行过程: 以fiber(<Header/>)为起点, 向上回溯

completeUnitOfWork第 1 次 回溯:

  1. 执行completeWork函数: class类型的组件无需处理.
  2. 上移副作用队列: 由于本节点fiber(header)没有副作用(fiber.flags = 0), 所以执行之后副作用队列没有实质变化(目前为空).
  3. 向上回溯: 由于还有兄弟节点, 把workInProgress指向下一个兄弟节点fiber(button), 退出completeUnitOfWork.

image.png

performUnitOfWork第 4 次调用(执行beginWork下探和completeUnitOfWork回溯):

beginWork下探阶段:

  • beginWork执行过程: 调用updateHostComponent
    • 本示例中button的子节点是一个直接文本节点,设置nextChildren = null(源码注释的解释是不用在开辟内存去创建一个文本节点, 同时还能减少向下遍历).
    • 由于nextChildren = null, 经过reconcileChildren阶段处理后, 返回值也是null
  • beginWork执行后: 由于下级节点为null, 所以进入completeUnitOfWork(unitOfWork)函数, 传入的参数unitOfWork实际上就是workInProgress(此时指向fiber(button)节点)
  • completeUnitOfWork执行过程: 以fiber(button)为起点, 向上回溯

completeUnitOfWork第 2 次 回溯:

  1. 执行completeWork函数
    • 因为fiber(button).stateNode != null, 所以无需再次创建 DOM 对象. 只需要进一步调用updateHostComponent()记录 DOM 属性改动情况
    • updateHostComponent()函数中, 又因为oldProps === newProps, 所以无需记录改动情况, 直接返回
  2. 上移副作用队列: 由于本节点fiber(button)没有副作用(fiber.flags = 0), 所以执行之后副作用队列没有实质变化(目前为空).
  3. 向上回溯: 由于还有兄弟节点, 把workInProgress指向下一个兄弟节点fiber(div), 退出completeUnitOfWork.

image.png

performUnitOfWork第 5 次调用(执行beginWork下探):

  • 执行前: workInProgress指向fiber(div)节点, 且current = workInProgress.alternate有值
  • 执行过程:
    • updateHostComponent()函数中, 调用reconcileChildren()生成下级子节点.
    • 需要注意的是, 下级子节点是一个可迭代数组, 会把fiber.child.sibling一起构造出来, 同时根据需要设置fiber.flags. 在本例中, 下级节点有被删除的情况, 被删除的节点会被添加到父节点的副作用队列中(具体实现方式请参考React diff算法).
  • 执行后: 返回下级节点fiber(p), 移动workInProgress指向子节点fiber(p)

image.png

performUnitOfWork第 6 次调用(执行beginWork下探和completeUnitOfWork回溯):

  • beginWork执行过程: 与第 4 次调用中构建fiber(button)的逻辑完全一致, 因为都是直接文本节点, reconcileChildren()返回的下级子节点为 null.
  • beginWork执行后: 由于下级节点为null, 所以进入completeUnitOfWork(unitOfWork)函数
  • completeUnitOfWork执行过程: 以fiber(p)为起点, 向上回溯

completeUnitOfWork第 3 次回溯:

  1. 执行completeWork函数
    • 因为fiber(p).stateNode != null, 所以无需再次创建 DOM 对象. 在updateHostComponent()函数中, 又因为节点属性没有变动, 所以无需打标记
  2. 上移副作用队列: 本节点fiber(p)没有副作用(fiber.flags = 0).
  3. 向上回溯: 由于还有兄弟节点, 把workInProgress指向下一个兄弟节点fiber(p), 退出completeUnitOfWork.

image.png

performUnitOfWork第 7 次调用(执行beginWorkcompleteUnitOfWork):

  • beginWork执行过程: 与第 4 次调用中构建fiber(button)的逻辑完全一致, 因为都是直接文本节点, reconcileChildren()返回的下级子节点为 null.
  • beginWork执行后: 由于下级节点为null, 所以进入completeUnitOfWork(unitOfWork)函数
  • completeUnitOfWork执行过程: 以fiber(p)为起点, 向上回溯

completeUnitOfWork第 4 次回溯:

  1. 执行completeWork函数:
  • 因为fiber(p).stateNode != null, 所以无需再次创建 DOM 对象. 在updateHostComponent()函数中, 又因为节点属性没有变动, 所以无需打标记
  1. 上移副作用队列: 本节点fiber(p)有副作用(fiber.flags = Placement), 需要将其添加到父节点的副作用队列之后.
  2. 向上回溯: 由于还有兄弟节点, 把workInProgress指向下一个兄弟节点fiber(p), 退出completeUnitOfWork.

image.png

performUnitOfWork第 8 次调用(执行beginWorkcompleteUnitOfWork):

  • beginWork执行过程: 本节点fiber(p)是一个新增节点, 其current === null, 会进入updateHostComponent()函数. 因为是直接文本节点, reconcileChildren()返回的下级子节点为 null.
  • beginWork执行后: 由于下级节点为null, 所以进入completeUnitOfWork(unitOfWork)函数
  • completeUnitOfWork执行过程: 以fiber(p)为起点, 向上回溯

completeUnitOfWork第 5 次回溯:

  1. 执行completeWork函数: 由于本节点是一个新增节点,且fiber(p).stateNode === null, 所以创建fiber(p)节点对应的DOM实例, 挂载到fiber.stateNode之上.
  2. 上移副作用队列: 本节点fiber(p)有副作用(fiber.flags = Placement), 需要将其添加到父节点的副作用队列之后.
  3. 向上回溯: 由于没有兄弟节点, 把workInProgress指针指向父节点fiber(div).

image.png

至此,下探阶段全部走完了,一直向上回溯,把副作用队列上移

最后: image.png

在流程上,update 和 mount 基本上没有区别,它们主要是在 performUnitOfWork 中处理方式不同。 update 阶段要考虑复用、diff、副作用等等,但最后,它们两个都是会构造出一颗fiber树,剩下的就交由commit阶段了

commit阶段

image.png fiber树构造好了,接下来就是渲染了(4标) 我们思路回到调用栈的第一个,performXXXWorkOnRoot

function performConcurrentWorkOnRoot(root, didTimeout) {
	// ...
  // 现在走到这一步了
  // 输出: 渲染fiber树
  finishConcurrentRender(root, exitStatus, lanes);

 	// ...
}

finishConcurrentRender主要是根据exitStatus的不同情况,判断如何commitRoot

function finishConcurrentRender(root, exitStatus, lanes) {
  switch (exitStatus) {
    case RootInProgress: ...
    case RootFatalErrored: ...
    case RootErrored: ...
    case RootSuspended: ...
    case RootSuspendedWithDelay: ...
    case RootCompleted: {
      // The work completed. Ready to commit.
      commitRoot(
        root,
        workInProgressRootRecoverableErrors,
        workInProgressTransitions,
      );
      break;
    }
  }
}

commitRoot的核心方法是 commitRootImpl。其整体分为3个阶段

  1. commit准备阶段
  2. commit阶段:这个阶段会把之前计算出的fiber,应用到DOM上,又可以分成3个子阶段
    1. before mutation:操作DOM之前
    2. mutation:进行DOM操作
    3. layout:DOM操作之后
  3. commit结束阶段

commit准备阶段

// 开启do while循环去处理副作用
do {
  flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);

// ...
// 拿到reconciler阶段结束后的成果,也就是内存中的fiber树根节点HostRootFiber
const finishedWork = root.finishedWork;

// ...

// 如果存在挂起的副作用,就通过scheduleCallback生成一个task任务去处理
if (
  (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
  (finishedWork.flags & PassiveMask) !== NoFlags
) {
  // Passive标记只在使用了useEffect才会出现,此处是专门针对hook对象做处理
  if (!rootDoesHavePassiveEffects) {
    // 开启一个宏任务调度flushPassiveEffects
    scheduleCallback(NormalSchedulerPriority, () => {
      flushPassiveEffects();
      return null;  
    });
  }
}

这里我们暂时只关心一个事

  1. 使用 flushPssiveEffects 清除掉所有的副作用

flushPassiveEffects 中主要是处理带有 Passive标记的fiber。Passive标记只会在使用了hook对象的function类型的节点上存在

commit阶段

如果没有副作用的话,commit阶段就简单的切换了fiber树

// 检查构造好的Fiber的子孙节点是否存在副作用需要操作
const subtreeHasEffects =
  (finishedWork.subtreeFlags &
    (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
  NoFlags;

// 检查hostRootFiber本身是否存放副作用需要进行操作
const rootHasEffect =
  (finishedWork.flags &
    (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
  NoFlags;

// 只要存在副作用, 那么则进入commit阶段
if (subtreeHasEffects || rootHasEffect) {
    ...
} else {
  // 没有副作用的话直接切换树了
  root.current = finishedWork;
}

进入真正的commit阶段后,就进入了上文提到的3个子阶段

  1. beforeMutation —— commitBeforeMutationEffects
  2. mutation —— commitMutationEffects
  3. layout —— commitLayoutEffects
// beforeMutation阶段
commitBeforeMutationEffects(root, finishedWork);


// mutation阶段
commitMutationEffects(root, finishedWork, lanes);
// 这里重置了containerInfo相关信息
resetAfterCommit(root.containerInfo);

root.current = finishedWork;

// layout阶段
commitLayoutEffects(finishedWork, root, lanes);

// 暂停scheduler,让浏览器绘制
// 但其实这个方法什么都没做,因为每一帧都会让出一些时间给浏览器绘制
requestPaint();

before mutation

beforeMutation主要处理带有 BeforeMutationMask标记的fiber节点

export const BeforeMutationMask =
  Update |
  Snapshot

commitBeforeMutationEffects 的核心方法是 commitBeforeMutationEffectsOnFiber

function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    try {
      commitBeforeMutationEffectsOnFiber(fiber);
    } catch (error) {
      // ...
    }

    // 处理兄弟节点
    const sibling = fiber.sibling;
    if (sibling !== null) {
      sibling.return = fiber.return;
      nextEffect = sibling;
      return;
    }

    // 回溯
    nextEffect = fiber.return;
  }
}

commitBeforeMutationEffectsOnFiber 这个方法,主要是处理flags存在Snapshot的节点,什么是Snapshot,class组件有个生命周期方法:getSnapshotBeforeUpdate,这个生命周期的执行时机就是在DOM提交之前调用

function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate; 
  const flags = finishedWork.flags; // 当前节点的flags标志
  // 只要标志为Snapshot才会进行处理
  if ((flags & Snapshot) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        break;
      }
      // 对于类组件
      case ClassComponent: {
        if (current !== null) {
          const prevProps = current.memoizedProps;  // 拿到之前的props
          const prevState = current.memoizedState;  // 拿到之前的state
          const instance = finishedWork.stateNode;  // 拿到组件实例
          // 在这里调用了类组件的getSnapshotBeforeUpdate, 返回值赋值给snapshot
          const snapshot = instance.getSnapshotBeforeUpdate( 
            finishedWork.elementType === finishedWork.type
              ? prevProps
              : resolveDefaultProps(finishedWork.type, prevProps),
            prevState,
          );
          // 然后赋值给instance.__reactInternalSnapshotBeforeUpdate进行保存
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
        break;
      }
      // 对于hostFiberRoot
      case HostRoot: {
       if (supportsMutation) {
         const root = finishedWork.stateNode;  // 拿到Root根应用节点
         // root.containerInfo执行根DOM节点, 此处调用clearContainer进行清空处理
         clearContainer(root.containerInfo);
       }
       break;
      }
      // ...
  }
}

mutation

mutation阶段主要是做DOM的更改,处理副作用队列中带有 MutationMask标记的fiber节点

export const MutationMask =
  Placement |
  Update |
  ChildDeletion |
  ContentReset |
  Ref |
  Hydrating |
  Visibility;

mutation的入口是commitMutationEffects,内部核心方法是commitMutationEffectsOnFiber

function commitMutationEffectsOnFiber(
  finishedWork: Fiber,
  root: FiberRoot,
  lanes: Lanes,
) {
  const current = finishedWork.alternate; // 拿到当前页面使用的Fiber结构
  const flags = finishedWork.flags; // 拿到当前节点的标签

  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: ...
    case ClassComponent: ...
    case HostComponent: ...
    case HostText: ...
    case HostRoot: ...
    case HostPortal: ...
    case SuspenseComponent: ...
    case OffscreenComponent:...
    case SuspenseListComponent: ...
    case ScopeComponent: ...
    default: {
      // 无论哪种情况都会执行这两个函数
      recursivelyTraverseMutationEffects(root, finishedWork, lanes);
      commitReconciliationEffects(finishedWork);

      return;
    }
  }
}

简单来说,这个阶段最后会调用 react-dom的api,对DOM进行

  • 新增 commitPlacement -> insertOrAppendPlacementNode-> insertBefore | appendChild(react-dom)
  • 删除 commitDeletionEffects -> commitDeletionEffectsOnFiber-> removeChildFromContainer | removeChild(react-dom)
  • 更新 commitUpdate(react-dom)

react-dom的这些方法执行完之后,DOM所在的界面也会更新

layout

layout阶段是在DOM变更后,处理副作用队列中带有 LayoutMask标记的fiber节点

export const LayoutMask = Update | Callback | Ref | Visibility;

layout的入口函数是 commitLayoutEffects, 其内部核心方法是 commitLayoutEffectOnFiber

function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  if ((finishedWork.flags & LayoutMask) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
           .....
         // 对于函数组件来说, 同步去执行useLayoutEffect的回调
        commitHookEffectListMount(
                HookLayout | HookHasEffect,
                finishedWork,
              );
             .....
        break;
      }
      case ClassComponent: {
        const instance = finishedWork.stateNode;
        if (finishedWork.flags & Update) {
          if (!offscreenSubtreeWasHidden) {
            if (current === null) {
              ...
              // 如果是初次挂载的话, 调用componentDidMount
              instance.componentDidMount();
            } else {
            // 如果是更新的话, 那么则调用componentDidUpdate
            // 这里传入了instance.__reactInternalSnapshotBeforeUpdate
            // 看commitBeforeMutationEffectsOnFiber。 我们在遇到getSnapshotBeforeUpdate的时候处理的
              const prevProps =
                finishedWork.elementType === finishedWork.type
                  ? current.memoizedProps
                  : resolveDefaultProps(
                      finishedWork.type,
                      current.memoizedProps,
                    );
              const prevState = current.memoizedState;
              ....
              instance.componentDidUpdate(
                    prevProps,
                    prevState,
                    instance.__reactInternalSnapshotBeforeUpdate,
               );
            }
          }
        }
        // 调用setState的回调
        const updateQueue = (finishedWork.updateQueue: any);
  
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
        break;
      }
      case HostRoot: ...
      case HostComponent: ...
      case HostText: {

        break;
      }
      case HostPortal: {
   
        break;
      }
      case Profiler: ...
      case SuspenseComponent: ...
      case SuspenseListComponent:
      case IncompleteClassComponent:
      case ScopeComponent:
      case OffscreenComponent:
      case LegacyHiddenComponent:
      case TracingMarkerComponent: {
        break;
      }

      default:
        throw new Error(
          'This unit of work tag should not have side-effects. This error is ' +
            'likely caused by a bug in React. Please file an issue.',
        );
    }
  }
   // 进行Ref的绑定逻辑
  if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
    if (enableScopeAPI) {
      if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
        commitAttachRef(finishedWork);
      }
    } else {
      if (finishedWork.flags & Ref) {
        commitAttachRef(finishedWork);
      }
    }
  }
}

layout阶段做了什么呢

  • 针对函数组件, 调用了useLayoutEffect的回调
  • 针对类组件, 初次挂载的情况下调用componentDidMount, 更新的情况下调用componentDidUpdate。 以及处理了setState的回调
  • 处理了Ref对象的绑定
  • 对于HostComponent节点, 如有Update标记, 需要设置一些原生状态(如: focus等)

至此,渲染任务就完成了

commit结束阶段

执行完了上述步骤后,最后是

  1. 检测更新。渲染过程中可能会派生出新的更新,渲染完毕后需要调用 ensureRootIsScheduled添加任务(如果有任务的话)

image.png 至此,react的整个渲染流程就完成了,最后我们再看看这张图 image.png

总结:从宏观上看fiber 树渲染位于reconciler 运作流程中的输出阶段, 是整个reconciler 运作流程的链路中最后一环(从输入到输出)。上文从渲染前, 渲染, 渲染后三个方面分解了commitRootImpl函数。 其中最核心的渲染逻辑又分为了 3 个函数, 这 3 个函数共同处理了有副作用fiber节点, 并通过渲染器react-dom把最新的 DOM 对象渲染到界面上