【前端架和框架】react协调器reconciler工作原理

本篇我们看下协调器reconciler部分,本系列文章旨在分析react核心模块内部运行原理和源码实现,提升架构和编码能力。

一、协调器在react内部运行部分

协调器reconciler工作部分在react内部被称为render阶段,即class组件的render函数、函数组件本身的调用等,根据Scheduler调度结果的不同,render阶段可能开始于开始于performSyncWorkOnRootperformConcurrentWorkOnRoot方法的调用。这取决于本次更新是同步更新还是异步更新。

这两个方法会分别调用如下方法,可以看到,他们唯一的区别是是否调用shouldYield。如果当前浏览器帧没有剩余时间,shouldYield会中止循环,直到浏览器有空闲时间后再继续遍历,也就是说的并发模式可中断。

workInProgress代表当前已创建的workInProgress fiber。performUnitOfWork整个阶段最核心的部分,被称为工作单元,构建fiber树,每个fiber节点的工作可以分为两部分:"递"和"归"。 这里是最核心的部分。

js 复制代码
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

二、协调器整体工作流程

  1. 调用入口: 提供scheduleUpdateOnFiber函数, 在mount阶段或者update阶段去调用。
  2. 注册调度任务: 与调度中心(scheduler包)交互, 注册调度任务task, 等待任务回调.
  3. 执行任务回调: 在内存中构造出fiber树, 会调用渲染器(react-dom)的一些接口创建dom节点, 在内存中创建出与fiber对应的DOM节点.
  4. 输出: 带有副作用标记的fiber树,供Commit阶段使用。

调用入口

react-reconciler对外暴露的 api 函数中, 只要涉及到需要改变 fiber 的操作(无论是首次渲染后续更新操作), 详细见前面文章react工作两大循环里面的图,很详细的标出来首次渲染的时候updateContainer->scheduleUpdateOnFiber阶段。 最后都会间接调用scheduleUpdateOnFiber, 所以scheduleUpdateOnFiber函数是输入链路中的必经之路

scss 复制代码
// 首次渲染
export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  // ...省略与逻辑不相关代码

  // 创建update
  const update = createUpdate(eventTime, lane, suspenseConfig);
  
  // update.payload为需要挂载在根节点的组件
  update.payload = {element};

  // callback为ReactDOM.render的第三个参数 ------ 回调函数
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }

  // 将生成的update加入updateQueue
  enqueueUpdate(current, update);
  // 调度更新
  scheduleUpdateOnFiber(current, lane, eventTime);

  // ...省略与逻辑不相关代码
}

//再次更新阶段

function dispatchAction(fiber, queue, action) {
  // ...创建update
  var update = {
    ....
    next: null,
  };

  // ...将update加入queue.pending

  var alternate = fiber.alternate;

  if (
    fiber === currentlyRenderingFiber$1 ||
    (alternate !== null && alternate === currentlyRenderingFiber$1)
  ) {
    // render阶段触发的更新
    didScheduleRenderPhaseUpdateDuringThisPass =
      didScheduleRenderPhaseUpdate = true;
  } else {
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // ...fiber的updateQueue为空,优化路径
    }

    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }
}

// scheduleUpdateOnFiber统一入口函数
export function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  // ... 省略部分无关代码
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (lane === SyncLane) {
    if (
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      // 直接进行`fiber构造`
      performSyncWorkOnRoot(root);
    } else {
      // 注册调度任务, 经过`Scheduler`包的调度, 间接进行`fiber构造`
      ensureRootIsScheduled(root, eventTime);
    }
  } else {
    // 注册调度任务, 经过`Scheduler`包的调度, 间接进行`fiber构造`
    ensureRootIsScheduled(root, eventTime);
  }
}

逻辑进入到scheduleUpdateOnFiber之后, 后面有 2 种可能:

  1. 不经过调度, 直接进行fiber构造.
  2. 注册调度任务, 经过Scheduler包的调度, 间接进行fiber构造.

注册调度任务

scheduleUpdateOnFiber函数之后, 立即进入ensureRootIsScheduled函数调用

ini 复制代码
 
// ... 省略部分无关代码
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  
  const existingCallbackNode = root.callbackNode;
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );
  const newCallbackPriority = returnNextLanesPriority();
  if (nextLanes === NoLanes) {
    return;
  }
  if (existingCallbackNode !== null) {
    const existingCallbackPriority = root.callbackPriority;
    if (existingCallbackPriority === newCallbackPriority) {
      return;
    }
    cancelCallback(existingCallbackNode);
  }

  let newCallbackNode;
  if (newCallbackPriority === SyncLanePriority) {
    newCallbackNode = scheduleSyncCallback(
      performSyncWorkOnRoot.bind(null, root),
    );
  }else {
    const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
      newCallbackPriority,
    );
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

ensureRootIsScheduled的逻辑很清晰, 分为 2 部分:

  1. 判断是否需要注册新的调度(如果无需新的调度, 会退出函数)

  2. 根据任务优先级,判断同步任务直接调用scheduleSyncCallback,会进行微任务调度,在微任务中遍历执行performSyncWorkOnRoot。如果是其他优先级,会进入scheduleCallback,这个函数就是上一节调度器Scheduler中的unstable_scheduleCallback,这里就是去调度器的任务队列中注册一个新的调度任务,然后时间过去后去调度执行,也就是会执行performConcurrentWorkOnRoot

执行任务回调

任务回调, 实际上就是执行performSyncWorkOnRootperformConcurrentWorkOnRoot函数,即内部执行performUnitOfWork,两个部分beginWork"递"和completeWork"归"两个子阶段。

  • beginWork阶段

首先判断当前流程市属于mount阶段还是update阶段,判断的依据就是current === null(fiber是双缓存构架,首次渲染,不存在当前组件对应的Fiber节点在上一次更新时的Fiber节点,即mountcurrent === null),即mountcurrent === null。组件update时,由于之前已经mount过,所以current !== null

js 复制代码
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes
): Fiber | null {
  // update时:如果current存在可能存在优化路径,可以复用current(即上一次更新的Fiber节点)
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;

   if (
     oldProps !== newProps ||
     hasLegacyContextChanged() ||
     (__DEV__ ? workInProgress.type !== current.type : false)
   ) {
     didReceiveUpdate = true;
   } else if (!includesSomeLane(renderLanes, updateLanes)) {
     didReceiveUpdate = false;
     switch (workInProgress.tag) {
       ....
     }
    
     // 复用current
     return bailoutOnAlreadyFinishedWork(current, workInProgress,  renderLanes);
    } else {
       didReceiveUpdate = false;
     }
  } else {
    didReceiveUpdate = false;
  }

  // mount时:根据tag不同,创建不同的子Fiber节点
  switch (workInProgress.tag) {
    case FunctionComponent:
    // ...省略
    case ClassComponent:
    // ...省略
    case HostRoot:
    // ...省略
    case HostComponent:
    // ...省略
    case HostText:
    // ...省略
    // ...省略其他类型
  }
}

上面可以看出,mount阶段当不满足优化路径时,就会跳过上面的if逻辑,直接进入新建子Fiber。我们可以看到,根据fiber.tag不同,进入不同类型Fiber的创建逻辑。 update阶段时我们可以看到,满足如下情况时didReceiveUpdate === false(即可以直接复用前一次更新的子Fiber,不需要新建子Fiber) 对于我们常见的组件类型,如(FunctionComponent/ClassComponent/HostComponent),最终会进入reconcileChildren方法,如下:

js 复制代码
function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // 对于mount的组件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes
    );
  } else {
    // 对于update的组件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes
    );
  }
}

export const reconcileChildFibers = ChildReconciler(true); 
export const mountChildFibers = ChildReconciler(false); 

function ChildReconciler(shouldTrackSideEffects) { 
   ... 
   return reconcileChildFibers; 
}

上面mountChildFibersreconcileChildFibers这两个方法的逻辑基本一致。唯一的区别是:reconcileChildFibers会为生成的Fiber节点带上effectTag属性,而mountChildFibers不会。

  • 对于mount的组件,他会创建新的子Fiber节点
  • 对于update的组件,他会将当前组件与该组件在上次更新时对应的Fiber节点比较,就是Diff算法,将比较的结果生成新Fiber节点。这里面的副作用包含插入、删除、移动,不包含更新。更新是在completeWork阶段做的。

ChildReconciler函数传递shouldTrackSideEffects标识, 是否为Fiber对象添加 effectTag,对于初始渲染来说, 只有根组件需要添加, 其他元素不需要添加, 防止过多的 DOM操作(这是一个优化)。

  • completeWork阶段

与beginWork阶段类似,completeWork阶段也会根据wip.tag区分对待,进入不同的逻辑处理

js 复制代码
function completeWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  const newProps = workInProgress.pendingProps;

  switch (workInProgress.tag) {
    case IndeterminateComponent:
    case LazyComponent:
    case SimpleMemoComponent:
    case FunctionComponent:
    case ForwardRef:
    case Fragment:
    case Mode:
    case Profiler:
    case ContextConsumer:
    case MemoComponent:
      return null;
    case ClassComponent: {
      // ...省略
      return null;
    }
    case HostRoot: {
      // ...省略
      updateHostContainer(workInProgress);
      return null;
    }
    case HostComponent: {
      // ...省略
      return null;
    }
  // ...省略

completeWork阶段,主要是创建或者标记元素更新和flags冒泡。

mount时

  • Fiber节点生成对应的DOM节点
  • 将子孙DOM节点插入刚生成的DOM节点
  • update逻辑中的updateHostComponent类似的处理props的过程
  • 执行bubbleProperties完成flags冒泡

update时

  • onClickonChange等回调函数的注册
  • 处理style prop
  • 处理DANGEROUSLY_SET_INNER_HTML prop
  • 处理children prop
  • 执行bubbleProperties完成flags冒泡

三、总结

我们了解了Reconciler的工作流程,它主要是采用DFS的顺序构建fiber树,主要是可以划分为beginWork和completeWork阶段,beginWork会根据当前fiberNode创建下一级fiberNode,在update时通过diff算法标记Placement(新增、移动),ChildDeletion(删除)。completeWork在mount阶段会构建DOM Tree,在update时标记Update(属性更新),最终执行flags冒泡。当最终HostRootFiber完成completeWork时,Reconciler的工作流程结束,此时我们得到了一颗带flags的wip Fiber tree会传递给commit阶段去消费,完成页面的渲染。

相关推荐
星哥说事1 天前
开发者必备神器:阿里云 Qoder CLI 全面解析与上手指南
前端
Dcc1 天前
构建可维护的 React 应用:系统化思考 State 的分类与管理
前端·react.js
pepedd8641 天前
我用Kiro+Claude写了一个MCP Server,让AI真正"感知"真实环境
前端·javascript·trae
zhishidi1 天前
智能体面试题:ReAct框架 是什么
人工智能·面试
CrabXin1 天前
如何让你的前端应用像“永动机”一样保持登录状态?
前端·设计模式
San301 天前
JavaScript 标准库完全指南:从基础到实战
前端·javascript·node.js
Never_Satisfied1 天前
在JavaScript / Node.js中,Web服务器参数处理与编码指南
前端·javascript·node.js
进阶的鱼1 天前
React+ts+vite脚手架搭建(五)【规范篇】
前端·react.js·vite
盼哥PyAI实验室1 天前
序列的力量——Python 内置方法的魔法解密
java·前端·python