useState
源码:https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberHooks.js
useState
对应了两个函数,分别是组件刚刚挂载时的mountState
,和组件更新时的updateState
我们这里直接用
montState
来做例子 它被调用时会通过
mountStateImpl
这个方法创建一个hook
对象并添加到workInProgressHook
链表上,workInProgressHook
其实就是当前工作的fiber
节点的hooks
链表
然后让赋值我们初始化传进来的initialState
给hook.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
的队列对象queue
的pending
的值其实是一个循环链表,每次update
对象的推入都是接入到这个循环链表上,之所以设计成循环链表
- 其一是因为它的插入效率为O(1)
- 其次是从链表的任何一个节点都可以遍历整个链表,这样在处理掉所有更新的时候直接从
queue.pending
出发就可以遍历链表
但是在推入队列后,由于优先级不同,被处理的逻辑也不同 。 像低优先级(其实也就是无优先级)的update
在updateState
的while循环
中会被跳过,也就是不会被处理,所以多次的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
上所有的任务都完成了,这个过程中就调用flushPassiveEffects
将useEffect
的回调处理了
接下来我们进入renderRootSync
看看
renderRootSync
: 进入协调阶段
renderRootSync
和renderRootConcurrent
的主要区别就是分别循环调用了workLoopSync
和workLoopConcurrent
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;
}
小结
renderRootSync
和renderRootConcurrent
的区别是否要进行时间切片,然后调用对应的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
部分),视情况用微任务或者宏任务调度
以上如有纰漏,敬请指教