react源码分析 setStatae究竟是同步任务还是异步任务

useState

源码:https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js

useState对应了两个函数,分别是组件刚刚挂载时的mountState,和组件更新时的updateState 我们这里直接用montState来做例子 它被调用时会通过mountStateImpl这个方法创建一个hook对象并添加到workInProgressHook链表上,workInProgressHook其实就是当前工作的fiber节点的hooks链表

然后让赋值我们初始化传进来的initialStatehook.memoizedState,同时也会创建一个队列对象绑定到hook上,这个队列对象是用于合并多次setState的,后面我们会提到

最后我们将这个hook的队列对象作为参数传给方法dispatchSetState,同时将hook.memoizedState和封装好的dipatch返回出去,也就是[state,setState]

那么这么一套流程下来我们就知道,setState的关键在于dispatchSetState,我们跑去看看这个函数是啥就好了

dispatchSetState

源码地址,github.com/facebook/re... dispatchSetStateInternal

首先如果调用setState时已经是在渲染阶段,那么本次的setState会被推入等待队列,等待渲染结束被调用 现在我们再细看看,两次state相同时和不同时,update对象是怎么被处理的,其实它们都被推入了队列,只是优先级不同 ,像后面相同的update对象会被enqueueConcurrentHookUpdateAndEagerlyBailout标记上低优先级 hook的队列对象queuepending的值其实是一个循环链表,每次update对象的推入都是接入到这个循环链表上,之所以设计成循环链表

  • 其一是因为它的插入效率为O(1)
  • 其次是从链表的任何一个节点都可以遍历整个链表,这样在处理掉所有更新的时候直接从queue.pending出发就可以遍历链表

但是在推入队列后,由于优先级不同,被处理的逻辑也不同 。 像低优先级(其实也就是无优先级)的updateupdateStatewhile循环中会被跳过,也就是不会被处理,所以多次的setState只有第一次setState会生效

总结来说就是,初始化useState时创建一个hook对象在当前fiber节点上,并在该hook对象上添加一个queue对象,每次调用setState后,都会创建一个update对象并添加到queue对象的pending链表上,记录完以后,react会比较前后两次的state是否有变化,如果有变化调用scheduleUpdateOnFiber开始整个的更新逻辑,否则跳过。

调度scheduleUpdateOnFiber最后会让fiber节点重新render,对于hooks组件来说,就是把整个函数再重新运行一遍,这个时候函数组件中的useState底层就是在调用updateState

返回state之前,它会判断当前是否有update链表

  • 如果没有就返回上一次的hook.memoizedState
  • 如果有就遍历update链表,跳过低优先级(NoLane)update对象,最后拿到有效的update对象获取到newState,赋值给hook.memoizedState(也就是state),返回出去即可

setStaste自始至终都是一个地址不变方法

所以setState是个异步方法还是个同步方法完全取决于fiber节点是怎么被调度的

接下来我们看看fiber是怎么被调度的

scheduleUpdateOnFiber

scheduleUpdateOnFiber 的关键在于调用了ensureRootIsScheduled,而ensureRootIsScheduled会从当前节点一直找到根节点,然后从根节点 开始遍历整个fiber树 ,但遍历的过程中,如果节点没有状态、props等等的更新 ,会通过attemptEarlyBailoutIfNoScheduledUpdate这个方法直接bailout,跳过它去遍历它的子节点或其它节点

react18以后

而现在ensureRootIsScheduled我们可以看到其实全部都是异步任务调度 了,这是react18与18之后的差异之处,感兴趣的可以看下一部分,我们继续讲18以后

ensureRootIsScheduled会把所有的任务都以异步任务包裹 ,具体使用微任务(Promise)还是宏任务(setTimeout)则取决于当前环境

而被包裹的scheduleImmediateRootScheduleTask最后会调用Scheduler_scheduleCallback实际就是调度器中的unstable_scheduleCallback来调度任务processRootScheduleInImmediateTask

调度器调度任务时会分为紧急任务和延时任务,对于延时任务会直接使用setTimeout调用,而其它任务会被推入任务队列,最后使用requestHostCallback中的schedulePerformWorkUntilDeadline一次性全部循环遍历调用

经过一系列的嵌套,最后所有的任务都会在workLoop中被循环遍历调用 ,而任务的每次循环调用前都会判断当前运行js是否超时,如果超时则会停止处理,让给主线程,等待恢复调用

react18以前

此前ensureRootIsScheduled会分为同步任务和异步任务调度:

scss 复制代码
js
 代码解读
复制代码
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  const existingCallbackNode = root.callbackNode;

  // 标记其他优先级为过期,以确定处理哪个任务
  markStarvedLanesAsExpired(root, currentTime);

  // 确定下一个优先级是多少,优先级也是过期时间
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

  // 开始调度任务.
  let newCallbackNode;
  if (newCallbackPriority === SyncLane) {
    // 同步任务通过scheduleSyncCallback调度进同步任务队列
    if (root.tag === LegacyRoot) {
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
    // 如果支持微任务就调用这个api并处理同步任务,调用queueMicrotask或者new Promise都可以实现微任务
    if (supportsMicrotasks) {
      scheduleMicrotask(() => {
          flushSyncCallbacks();
       });
    } else {
      // 不然就给个高优先级让他异步调度并处理同步任务,注意scheduleCallback 本质是个宏任务调用
      scheduleCallback(ImmediateSchedulerPriority, flushSyncCallbacks);
    }
    // =========== 以上这个`flushSyncCallbacks`其实就是while循环调用掉所有的同步任务 =======
    newCallbackNode = null;
  } else {
    // ======================= 这里就不是同步任务了 =============================
    let schedulerPriorityLevel;
    // 根据lanes来判断优先级
    switch (lanesToEventPriority(nextLanes)) {
      // ========================= 省略 ====================================
    }
    // 调度并发更新  这个就是异步更新了 scheduleCallback 本质是个宏任务调用
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }
  // 更新节点信息
  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

简单总结,ensureRootIsScheduled会根据当前节点的优先级来判断调用同步任务还是异步任务,但是react到目前为止还是默认同步任务,随后调用scheduleTaskForRootDuringMicrotask,同步任务调用performSyncWorkOnRoot来触发节点的更新,否则就是performConcurrentWorkOnRoot

而这两的主要区别就是是否要进行时间切片的判断,因为代码太长,只保留主要内容:

ini 复制代码
js
 代码解读
复制代码
function performConcurrentWorkOnRoot(root, didTimeout) {
  const originalCallbackNode = root.callbackNode;
  // 处理掉所有之前还在等待的useEffect的回调,最终调用的是flushPassiveEffectsImpl
  const didFlushPassiveEffects = flushPassiveEffects();

  // 如果工作已经在CPU上绑定了太长时间会暂时禁用时间切片.
  const shouldTimeSlice =
    !includesBlockingLane(root, lanes) &&
    !includesExpiredLane(root, lanes) &&
    (disableSchedulerTimeoutInWorkLoop || !didTimeout);
  // 根据是否要时间切片拿到现在节点的状态,从这里开始渲染fiber树更新dom
  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);
  if (exitStatus !== RootInProgress) {
    // ============================   这里省略了很多  ==========================
    if (// 省略) {
    } else {
      // 因为有可能上一次已经加载了一部分,从这里拿到上次的状态已经加载好的fiber树,将它和现在的树一致化
      const finishedWork: Fiber = (root.current.alternate: any);

      // 把上次加载好的提交了,会根据加载的状态执行不同的逻辑,这里最终调用的是commitRoot
      root.finishedWork = finishedWork;
      root.finishedLanes = lanes;
      finishConcurrentRender(root, exitStatus, lanes);
    }
  }
  // 再确认一遍是否任务都被调用了
  ensureRootIsScheduled(root, now());
  if (root.callbackNode === originalCallbackNode) {
    // 如果节点被挂起,先不处理
    if (
      workInProgressSuspendedReason === SuspendedOnData &&
      workInProgressRoot === root
    ) {
      root.callbackPriority = NoLane;
      root.callbackNode = null;
      return null;
    }

    return performConcurrentWorkOnRoot.bind(null, root);
  }
  return null;
}

以上代码比较重要的就是useEffect的执行时机和协调阶段提交阶段的入口,这个函数会根据是否要进行时间切片来选择执行renderRootConcurrent还是renderRootSync,这是协调阶段入口

processRootScheduleInImmediateTask(协调与提交阶段)

processRootScheduleInImmediateTask在标记完所有root(也就是fiber)的优先级后,最终会调用flushSyncWorkAcrossRoots_impl来开启协调阶段

等协调阶段结束以后,会调用finishConcurrentRender来进行提交阶段,会根据fiber的构建情况来确定是否要一起提交了还是挂起等下一帧的构建再提交,对于performConcurrentWorkOnRoot来说,这里会在协调阶段后直接调用commitRoot

最后会再调用ensureRootIsScheduled确认一遍是否fiber上所有的任务都完成了,这个过程中就调用flushPassiveEffectsuseEffect的回调处理了

接下来我们进入renderRootSync看看

renderRootSync : 进入协调阶段

renderRootSyncrenderRootConcurrent的主要区别就是分别循环调用了workLoopSyncworkLoopConcurrent

scss 复制代码
js
 代码解读
复制代码
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    // $FlowFixMe[incompatible-call] found when upgrading Flow
    performUnitOfWork(workInProgress);
  }
}

它们之间的区别就是每次调用前是否要进行shouldYield()的判断,也就是是否要将线程让给GUI线程,我们看看这个函数,它实际是\react-main\packages\scheduler\src\forks\Scheduler.js下的shouldYieldToHost

arduino 复制代码
js
 代码解读
复制代码
function shouldYieldToHost(): boolean {
  // 用当前时间减去开始调度的时间,底层是performance.now()
  const timeElapsed = getCurrentTime() - startTime;
  // 当前任务调度执行的时间是否小于每一帧使用的时间,react定义为5ms,也就是每一帧允许使用5ms调度执行任务
  if (timeElapsed < frameInterval) {
    return false;
  }
  // 亦或者如果主线程被占用太久了,也会让出来给绘制和用户事件执行
  if (enableIsInputPending) {
    // ====================== 省略 ================================
  }

  return true;
}

所以react的时间切片并不是基于requestIdleCallback实现的,而是通过限定每一帧任务调度执行的时间实现的

接下来我们再来看看这个performUnitOfWork,比较重量级,它完成每一个节点的递阶段和归节点

performUnitOfWork

ini 复制代码
js
 代码解读
复制代码
// 这个函数会被while循环不断调用
function performUnitOfWork(unitOfWork: Fiber): void {
  // 当前工作的fiber,会随着beginWork和completeUnitOfWork的过程不断变化
  const current = unitOfWork.alternate;

  let next;
  // ============= 省略===================
  // 开始协调阶段的递阶段,如果有子fiber节点会被返回,下一次的while循环去处理它
  next = beginWork(current, unitOfWork, renderLanes);

  // ============= 省略===================
  // 如果fiber节点找到头了,往下没有fiber节点创建了,就开始归阶段
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
  // 否则就开始下一个
    workInProgress = next;
  }

  ReactCurrentOwner.current = null;
}

我们先来看看beginWork,路径为\react-main\packages\react-reconciler\src\ReactFiberBeginWork.new.js

beginWork

php 复制代码
js
 代码解读
复制代码
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  workInProgress.lanes = NoLanes;
  // =========================== 省略 ==================================
  switch (workInProgress.tag) {
    // =========================== 省略 ==================================
    case FunctionComponent: {
      // =========================== 省略 ==================================
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case ClassComponent: {
     // =========================== 省略 ==================================
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    // =========================== 省略 ==================================
  }
  // =========================== 省略 ==================================
}

beginWork会根据fiber节点的类型来调用相应的方法,hook组件会调用updateFunctionComponent,class组件会调用updateClassComponent

我们单看hooks组件是怎么更新的:

scss 复制代码
js
 代码解读
复制代码
function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps: any,
  renderLanes,
) {
  let nextChildren;
  // =========================== 省略 ==================================
  // 这里会再将对应的函数组件调用一遍,得到新的JSX
  nextChildren = renderWithHooks(
      current,
      workInProgress,
      Component,
      nextProps,
      context,
      renderLanes,
  );
  // =========================== 省略 ==================================
  // 用上方得到的新的JSX去开始协调创建fiber节点
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  return workInProgress.child;
}

小结

renderRootSyncrenderRootConcurrent的区别是否要进行时间切片,然后调用对应的workLoop方法来while循环调用performUnitOfWork,直到把所有的任务处理完

performUnitOfWork中会调用beginWork来进行创建fiber节点,completeWork来创建Dom,至于它们更具体的内容可以看看几位前辈的文章

等协调阶段结束后就是提交阶段了

CommitRoot:进入提交阶段

提交阶段的代码可就非常长了,因此省略了很多内容,写了一些注释

scss 复制代码
js
 代码解读
复制代码
function commitRootImpl(
  root: FiberRoot,
  recoverableErrors: null | Array<CapturedValue<mixed>>,
  transitions: Array<Transition> | null,
  renderPriorityLevel: EventPriority,
) {
  // 这里就是BeforeMutation、Mutation、Layout三个阶段的执行
  if (subtreeHasEffects || rootHasEffect) {
    const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
      root,
      finishedWork,
    );

    commitEffects(root, finishedWork, lanes);
    // 这里会同步处理掉layoutEffect的回调,通过调用commitHookEffectListMount
    commitLayoutEffects(finishedWork, root, lanes);

    // 提交了就告诉调度器该让给浏览器渲染了
    requestPaint();
  }

  return null;
}

总个结

整个过程为,dispatchSetState --> scheduleUpdateOnFiber --> ensureRootIsScheduled --> performSyncWorkOnRoot --> renderRootSync --> wookLoopSync --> performUnitOfWork --> beginWork --> completeWork,处理完协调阶段后,继续进行performSyncWorkOnRoot中的commitRoot阶段,提交阶段结束了也就渲染到页面上了

setState在react18以后,绝对全部都是异步任务 (详见scheduleUpdateOnFiber部分),视情况用微任务或者宏任务调度

以上如有纰漏,敬请指教

相关推荐
拾光拾趣录几秒前
for..in 和 Object.keys 的区别:从“遍历对象属性的坑”说起
前端·javascript
OpenTiny社区11 分钟前
把 SearchBox 塞进项目,搜索转化率怒涨 400%?
前端·vue.js·github
编程猪猪侠40 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞44 分钟前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路1 小时前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构