react源码分析之一 调用setStatae之后发生了什么?

老规矩开头讲废话

其实掘金上已经有不少的react源码分析了,其中有着很优秀的一些前辈写的优秀文章,如jacky 写的深入react系列,juejin.cn/user/125749... ,和青藤前端团队写的juejin.cn/post/708774... 本人初步学习react源码就是从几位前辈的文章中学习的,不得说不学习到了很多东西,在此感谢。

但是,实际在看几位前辈的源码分析时,有些问题一直让我百思不得其解,直到我把react源码下载下来分析,我发现和几位前辈讲的东西还是略有出路,因此想写下这篇文章重新组织一下逻辑,就从我们最常用的方法setState开始分析,我们调用了它以后发生了什么?

阅读本文前建议先阅读几位前辈的文章,以便更好的理解

setState究竟是个啥

先看看useState是个啥

首先丢出有省略修改的源码\react-reconciler\src\ReactFiberHooks.new.js,网上很多文章说useStatesetState是复用了useReducerupdateReducer,但是根据最新版本,这两个已经分别是两个函数了:

js 复制代码
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 创建hook对象并连接到workInProgressHook链表上
  const hook = mountWorkInProgressHook();
  // 此处省略。。。
  hook.memoizedState = hook.baseState = initialState; // 这个其实就是state
  // 创建一个队列对象
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  // 连接到hook链表上
  hook.queue = queue;
  
  // 这个就是setState
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

以上代码已经标上了注释,总结来讲,useState对应了两个函数,分别是组件刚刚挂载时的useState,和组件更新时的updateState,但主要逻辑不变,我们这里直接用montState来做例子

它被调用时会创建一个hook对象并添加到workInProgressHook链表上,workInProgressHook其实就是当前工作的fiber节点的hooks链表

然后就是我们用到的state,hook.memoizedState会被我们初始化传进来的initialState赋值

现在是setState,mountState会创建一个队列对象,并添加到hook链表中,最后将这个队列对象作为参数传给方法dispatchSetState,最后将hook.memoizedStatedipatch返回出去,也就是[state,setState]

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

dispatchSetState 是个啥

以下代码删了很多英文注释,自己添加了中文,感兴趣的可以看源码,\react-reconciler\src\ReactFiberHooks.new.js

js 复制代码
function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  // lane用于任务调度判断优先级,这里是获取当前fiber节点的优先级,主要就是判断你用了没用useTransition这个api,用了返回低优先级,否则就默认
  const lane = requestUpdateLane(fiber);

  // 创建一个update对象,记录本次更新的信息
  const update: Update<S, A> = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  // 是渲染阶段更新就暂时跳过加入队列
  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    // 上一次的fiber
    const alternate = fiber.alternate; 
    // 如果本次的fiber优先级最高,并且初次加载或者是上一次的fiber优先级也最高,走这里
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // 拿到渲染更新的函数或者值,也就是setState我们会两种使用
      // setState(0) 或 setState(() => 1+1),在这里一并逻辑处理了
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        // 如果有更新信息,走下面
        let prevDispatcher;
        try {
          // 当前的状态,也就是未setState前的状态
          const currentState: S = (queue.lastRenderedState: any);
          // 拿到当前setState
          const eagerState = lastRenderedReducer(currentState, action);
          update.hasEagerState = true;
          update.eagerState = eagerState;
          // 进行浅比较,如果排队到更新队列里去进行合并,跳过本次更新
          if (is(eagerState, currentState)) {
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return;
          }
        } catch (error) {
        }
      }
    }
    // 调整fiber的优先级
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);

    if (root !== null) {
      const eventTime = requestEventTime();
      // 这里是整个渲染更新的起点
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitionUpdate(root, queue, lane);
    }
  }
}

照例每行关键都有注释,简单来说就是调用setState后,会比较前后两次的state是否有变化,如果有变化就触发更新,否则跳过,然后调用scheduleUpdateOnFiber开始整个的更新逻辑

scheduleUpdateOnFiber 它是个啥

丢出源码:

js 复制代码
export function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  // 检查工作循环当前是否已暂停并等待数据加载完成
  if (
    workInProgressSuspendedReason === SuspendedOnData &&
    root === workInProgressRoot
  ) {
    // 传入的更新可能会解锁当前的渲染。中断当前的尝试,并从顶部重新开始。
    prepareFreshStack(root, NoLanes);
    markRootSuspended(root, workInProgressRootRenderLanes);
  }

  // 标记当前节点有个等待的更新
  markRootUpdated(root, lane, eventTime);

  if (
    // 走到这里是个错误  会报错 忽略这里
  ) {
  } else {
    // 是否使用了useTransition,是的话调整优先级和起始时间
    if (enableTransitionTracing) {
      const transition = ReactCurrentBatchConfig.transition;
      if (transition !== null && transition.name != null) {
        if (transition.startTime === -1) {
          transition.startTime = now();
        }

        addTransitionToLanesMap(root, transition, lane);
      }
    }

    if (root === workInProgressRoot) {
      // 走到这里说明在渲染的过程中又触发了一次更新,将这个节点标记为有多次更新
      if (
        deferRenderPhaseUpdateToNextBatch ||
        (executionContext & RenderContext) === NoContext
      ) {
        workInProgressRootInterleavedUpdatedLanes = mergeLanes(
          workInProgressRootInterleavedUpdatedLanes,
          lane,
        );
      }
      if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
        // 说明这个节点已经被延迟更新了,这次的更新也完成不了,将它标记为挂起状态
        markRootSuspended(root, workInProgressRootRenderLanes);
      }
    }
    // 这里是关键!!!!!!!
    // 调度节点上所有的任务并触发渲染
    ensureRootIsScheduled(root, eventTime);
    if (
      lane === SyncLane &&
      executionContext === NoContext &&
      (fiber.mode & ConcurrentMode) === NoMode &&
      // Treat `act` as if it's inside `batchedUpdates`, even in legacy mode.
      !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
    ) {
      // 调用所有的同步任务
      resetRenderTimer();
      flushSyncCallbacksOnlyInLegacyMode();
    }
  }
}

以上代码的关键是ensureRootIsScheduled,我们去看这个方法:

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到目前为止还是默认同步任务,同步任务调用performSyncWorkOnRoot来触发节点的更新,否则就是performConcurrentWorkOnRoot

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

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,这是协调阶段入口

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

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

接下来我们进入renderRootSync看看

renderRootSync : 进入协调阶段

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

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

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

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

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组件是怎么更新的:

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:进入提交阶段

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

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阶段,提交阶段结束了也就渲染到页面上了

以上如有纰漏,敬请指教!

相关推荐
m0_748240251 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar1 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人1 小时前
前端知识补充—CSS
前端·css
GISer_Jing2 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245522 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v2 小时前
webpack最基础的配置
前端·webpack·node.js
pubuzhixing2 小时前
开源白板新方案:Plait 同时支持 Angular 和 React 啦!
前端·开源·github
2401_857600952 小时前
SSM 与 Vue 共筑电脑测评系统:精准洞察电脑世界
前端·javascript·vue.js
2401_857600952 小时前
数字时代的医疗挂号变革:SSM+Vue 系统设计与实现之道
前端·javascript·vue.js
GDAL2 小时前
vue入门教程:组件透传 Attributes
前端·javascript·vue.js