function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
总结
mount 阶段的 useState 大致起到一个初始化的作用,在结束该函数的调用后我们会获得该 useState 对应的 hooks 对象。该对象长这样
触发 useState 的 dispatch
此时将断点打到 useState 返回的 dispatch 上,触发点击事件。获得下面的调用栈
dispatchSetState 是个关键的函数,再次强调一下。
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
) {
/**
* 这里暂时理解为获取当前 update 的优先级即可
*/
const lane = requestUpdateLane(fiber);
/**
* 与 setState 的 update 链表类似
* 这里的 update 也是一个链表结构
*/
const update: Update<S, A> = {
lane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};
/**
* 判断是否是渲染中更新
*/
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
const alternate = fiber.alternate;
/**
* 判断 react 当前是否空闲
* 比方说在 第一次调用 dispatch 后,fiber.lanes 就不为 NoLanes
* 因此 第二次调用 dispatch 便不会进入底下对 update 实例内的 eagerState 求值
*/
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher;
const currentState: S = (queue.lastRenderedState: any);
/**
* 计算期望的 state 并暂存入 eagerState
* lastRenderedReducer 内逻辑如下
* 判断当前 action 是否是一个函数
* 毕竟也存在 setText((preState) => preState + 1)这样的调用方式存在
* 如果是函数传入 currentState 并调用 action 获得计算后的值
* 如果不是,比如setText(1)这样的调用方式,就直接返回 action
*/
const eagerState = lastRenderedReducer(currentState, action);
// Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.hasEagerState = true; // 根据注释 防止被 reducer 重复计算
update.eagerState = eagerState;
/**
* 判断两者的值是否相等 浅比较 ===
*/
if (is(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
// TODO: Do we still need to entangle transitions in this case?
/**
* 根据注释,如果值未改变,则不需要去调度 react 去重新渲染
* 底下这个函数只是去处理 update 链表的结构
*/
enqueueConcurrentHookUpdateAndEagerlyBailout(
fiber,
queue,
update,
lane,
);
return; // 因为不需要调度,因此函数在这里中断
}
}
}
/**
* 因为运行到这里的 update 是被认为必须要被重新渲染到页面上的
* enqueueConcurrentHookUpdate 与 enqueueConcurrentHookUpdateAndEagerlyBailout 不同
* 会去调用 markUpdateLaneFromFiberToRoot
* 该函数会将当前 update 对应的 lane 自发生更新的 fiber 节点开始一层一层向上递归(fiber.return)
* 标记到其父 fiber 的 childLanes 上(比方说fiber.return -> fiber.return.return),直到 root 为止
* 调用 markUpdateLaneFromFiberToRoot 的目的是为 scheduleUpdateOnFiber 的调度做准备的
*/
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
/**
* 进入调度流程(scheduleUpdateOnFiber 的分析已经更新在最下面了)
* 不过在 v18 中会被 queueMicrotask 注册为 microTask
* 在 v17 则是以对应的优先级注册进 scheduler
*/
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
/**
* 一个不稳定的函数,大致上是处理 lanes,有点难懂不解析了
*/
entangleTransitionUpdate(root, queue, lane);
}
}
markUpdateInDevTools(fiber, lane, action);
}
function updateReducer<S, I, A>(
reducer: (S, A) => S,
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
/**
* 这个函数在 use(Layout)Effect 内也出现过,大致作用是
* 同时移动 WIP hooks 和 current hooks 的指针向后移动一位
* 详细注释可看 [React Hooks: 附录](https://github.com/IWSR/react-code-debug/issues/4)
*/
const hook = updateWorkInProgressHook(); // WIP hooks对象
const queue = hook.queue; // update 链表
if (queue === null) {
throw new Error(
'Should have a queue. This is likely a bug in React. Please file an issue.',
);
}
// 在 useState 的场景中 lastRenderedReducer 为 basicStateReducer —— 在 mountState 中预设的 reducer
queue.lastRenderedReducer = reducer;
const current: Hook = (currentHook: any);
// The last rebase update that is NOT part of the base state.
let baseQueue = current.baseQueue;
// The last pending update that hasn't been processed yet.
// 获取 pending 状态的 update,还未被计算
const pendingQueue = queue.pending;
/**
* 整理 update queue,为后续的更新做准备
*/
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
// 将 pendingQueue 与 baseQueue 相连
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
/**
* 处理后的 baseQueue 包含了所有 update 实例
*/
if (baseQueue !== null) {
// We have a queue to process.
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
const updateLane = update.lane;
/**
* 筛选出被包含在 renderLanes 内的 update
* 这步骤和位操作有关
* updateLane 如果属于 renderLanes 内,则代表着当前 update 的优先级比较紧急
* 需要被处理,反之则可以跳过,
* 类似的逻辑也存在于 class 组件的 update 链表内(这里不详细展开)
* 不过这里是 ! 操作,所以都是反的
*/
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
/**
* 进入了这个逻辑的 update 便意味着不是很紧急,可以慢慢处理
* 但是这并不意味着不处理,在空闲时仍然会从被跳过的 update 开始重新再计算一次状态
* 因此需要缓存被跳过的 update 实例
*/
const clone: Update<S, A> = {
lane: updateLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
/**
* 将跳过的 update 添加到 newBaseQueue 中
* 等到下一次 render 再重新计算
*/
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
// 更新 WIP fiber 的 lanes,下一次 render 时重新执行跳过 update 的关键
// completeWork 时会将这些 lanes 收集到 root(合并到 root 发生在 commitRootImpl),然后再重新调度(commitRootImpl 内的 ensureRootIsScheduled)
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
updateLane,
);
// 标记跳过的 lanes 到 workInProgressRootSkippedLanes 上
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.
// 优先级足够的场景,便执行更新
if (newBaseQueueLast !== null) {
/**
* newBaseQueueLast 不为空,说明存在被跳过的 update
* update 的状态计算可能存在联系
* 因此一旦有update被跳过,就以它为起点,
* 将后边直到最后的 update 无论优先级如何都截取下来。
* (和 class 组件的处理逻辑差不多)
*/
const clone: Update<S, A> = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
hasEagerState: update.hasEagerState,
eagerState: update.eagerState,
next: (null: any),
};
newBaseQueueLast = newBaseQueueLast.next = clone;
}
// Process this update.
// 执行本次的 update,计算新的 state
if (update.hasEagerState) {
// 已经计算过的状态会被打上标记,避免重复计算
// If this update is a state update (not a reducer) and was processed eagerly,
// we can use the eagerly computed state
newState = ((update.eagerState: any): S);
} else {
// 根据 state 和 action 计算新的 state
const action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = (newBaseQueueFirst: any);
}
// Mark that the fiber performed work, but only if the new state is
// different from the current state.
// === 校验不通过,则在当前的 WIP fiber 上标记更新,在 completeWork 阶段中会被收集到 root 上
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 更新新的数据到 hook 内
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
// Interleaved updates are stored on a separate queue. We aren't going to
// process them during this render, but we do need to track which lanes
// are remaining.
/**
* 处理 interleaved update
* 但我决定跳过这一块的分析,因为我完全不知道什么样的更新才能被叫做 interleaved update
* 在 /react-reconciler/src/ReactFiberWorkLoop.old.js 内有着这一块的描述
* Received an update to a tree that's in the middle of rendering. Mark
that there was an interleaved update work on this root.
* 但是又和 enqueueConcurrentHookUpdate 与
* enqueueConcurrentHookUpdateAndEagerlyBailout 内对于 queue.interleaved 的设置完全搭不上边
* 如果有对这方面有了解的朋友也请麻烦指导一下
*/
const lastInterleaved = queue.interleaved;
if (lastInterleaved !== null) {
let interleaved = lastInterleaved;
do {
const interleavedLane = interleaved.lane;
currentlyRenderingFiber.lanes = mergeLanes(
currentlyRenderingFiber.lanes,
interleavedLane,
);
markSkippedUpdateLanes(interleavedLane);
interleaved = ((interleaved: any).next: Update<S, A>);
} while (interleaved !== lastInterleaved);
} else if (baseQueue === null) {
// `queue.lanes` is used for entangling transitions. We can set it back to
// zero once the queue is empty.
queue.lanes = NoLanes;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
React Hooks: useState 分析
说明
TLNR
mount 场景下的 useState
直接对 useState 打断点进行调试,我们会进入 mountState 内
就这样 mount 阶段的 useState 的分析完了,东西很少。
另外请注意一下 basicStateReducer 这一函数。
总结
mount 阶段的 useState 大致起到一个初始化的作用,在结束该函数的调用后我们会获得该 useState 对应的 hooks 对象。该对象长这样
触发 useState 的 dispatch
此时将断点打到 useState 返回的 dispatch 上,触发点击事件。获得下面的调用栈
dispatchSetState 是个关键的函数,再次强调一下。
总结一下 useState 的 dispatch
对于 useState 的 dispatch 来说,每调用一次 dispathc 都会生成一个新的 update 实例且该实例会被挂载到其 hooks 的链表,但是调用 dispatch 后生成的新值会与其原始值(currentState)经过一次浅比较来决定这一次的更新是否需要被调度更新。在 v18 的版本中,这一次的调度任务是存在于 microTask 中的,因此可认为 useState 的 dispatch 属于异步操作。下面简单写个例子验证下。
点击后得到的结果
其调用栈,不难看到 microtask。
update 场景下的 useState
接下来我们稍微在原来的调试代码上添加一个 dispatch,新的调试代码大概长这样。
对整个页面的表现做一个记录,大致长这样
顺带一提
第二次 dispatch 时因为会不满足 fiber.lanes === NoLanes 这个条件因此直接跳入 enqueueConcurrentHookUpdate (具体看一下 dispatchSetState 的代码)
而在 enqueueConcurrentHookUpdate 对 update 链表连接后我们会得到如下结构的环状链表
回到正题,在 useState 的 update 阶段中会调用 updateState 内的 updateReducer 这一函数(就是 uesReducer 对应的 mount 版本)
总结一下
useState 的 update 阶段会通过 dispatch 函数生成的 update 链表去更新对应的状态,然而并非所有的 update 都会被更新,只有其优先级(lane)属于当前 renderLanes 内才会被优先计算,而跳过的 update 会被标记并在下一次 render 时被更新。
简单提供个例子来证明
从运行结果来看,由于 timeout1 的 update 被 startTransition降级,因此 timeout2 的 update 被优先更新了。
批处理
批处理其实在 v17 时代便已经有了,不过 v18 把它的限制条件给去除了然后就成了自动批处理(恼)。具体可以看看这篇文章。
比方说我们这么一段代码在运行(当然这里的调试环境依然是 v18)
根据上文中对 dispatchSetState 的分析中其实有提到,只要是有必要更新到页面上的 update 就会调用 scheduleUpdateOnFiber 来触发渲染。那么例子中连续调用了两次 dispatch ,也就是说触发了两次 scheduleUpdateOnFiber,那么问题来了——页面渲染了几次呢?
就一次。
至于为什么需要去看 scheduleUpdateOnFiber 里面做了什么,scheduleUpdateOnFiber 涉及到了整个更新的入口,因此也是一个比较重要的函数。
该函数主要负责处理
scheduleUpdateOnFiber
这时我们又遇到了一个 react 内的重要函数 —— ensureRootIsScheduled,进入该函数就能看到任务调度在 react 内的核心逻辑了(还有一部分核心逻辑在 Scheduler 内,可以看我写的 React Scheduler: Scheduler 源码分析)
ensureRootIsScheduled
分析完 ensureRootIsScheduled 我们也对批处理是如何实现的有了大致的了解,它的实现其实就是对 react 内同优先级任务的复用,这样子就做到了多次触发 hooks 的 dispatch 但是只渲染了一次。
不过到这配合上 React Scheduler: Scheduler 源码分析 其实我们已经把 react 内三大模块(调度 → 协调 → 渲染),调度的部分全部介绍完了,接下来让我一个问题来把整篇文章串起来。
React从触发 useState 的 dispatch 到渲染到页面做了哪些事 —— 调度篇