React 根据您调用的位置使用不同的方法来安排更新setState
.
例如,您的setState
事件处理程序内部将使用
export function enqueueConcurrentHookUpdate<S, A>(
fiber: Fiber,
queue: HookQueue<S, A>,
update: HookUpdate<S, A>,
lane: Lane,
)
当你的setState
在组件顶层使用
function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>,
)
你打电话时setState
,React会内部调用一个函数,
function dispatchSetState<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
)
如果你检查这个函数的声明,你会发现一个顶级条件检查,
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {}
Your setState
事件处理程序内部将使用else
阻止同时setState
在渲染阶段(顶级函数体)将使用if
block.
额外的
那么 React 如何决定是否处于渲染阶段呢?如果你检查代码isRenderPhaseUpdate
你可以看到,
function isRenderPhaseUpdate(fiber: Fiber) {
const alternate = fiber.alternate;
return (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
);
}
现在你可能听说过虚拟 DOM,实际上它是一个链表。链表的每个对象称为纤程节点。这些 Fiber 节点只不过是普通的 javascript 对象。每个光纤节点都有一个名为的字段,alternate
.
可以有 2 个独立的光纤树(虚拟 dom)。其中一棵 Fiber 树被称为当前树,它是提交给 DOM 的一棵树。另一种纤维树称为工作树。这是当状态更新发生但尚未提交到 DOM 时 React 新构建的一个。
因此,对于给定的组件,最多可以有两个 Fiber 节点(一个来自当前树,另一个来自 work-in-tree )。这两个光纤节点使用alternate
field.
currentlyRenderingFiber
是一个全局变量,它通过 React 跟踪当前渲染的 Fiber 节点。
现在你应该能够理解上面的正文了isRenderPhaseUpdate
功能。
补充说明结束
案例 01 - 事件处理程序
当你触发setState
从事件处理程序反应将使用else
上述函数的块。
如果你检查身体else
块你会发现以下代码片段,
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) { // <-- checks if the previous value is equal to current one.
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return; // <-- return early without call `enqueueConcurrentHookUpdate`
}
// never reaches here if the `eagerState` & `currentState` are same
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
const eventTime = requestEventTime();
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
entangleTransitionUpdate(root, queue, lane);
}
正如你所看到的is(eagerState, currentState)
true (这对于你的情况来说是正确的,因为两者eagerState
& currentState
保持该值"Shiva"
) React 将提前退出函数而不调用enqueueConcurrentHookUpdate
。这就是为什么 React 不会再次重新渲染相同的值。
CASE 02 - 组件的顶层
你打电话时setState
从组件的顶层开始,它将在 React 遍历组件树并调用组件时运行(由于 React 执行组件主体)
请注意,React 是调用您的组件的人。你只是定义它们。 React 调用您的组件并在遍历时获取输出,即渲染和构建新的 Fiber 树(又名工作中树)
现在如果你检查身体
function enqueueRenderPhaseUpdate<S, A>(
queue: UpdateQueue<S, A>,
update: Update<S, A>,
) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
注意上面函数的代码注释。它创建一个循环链接列表,其中包含来自setState
在你的组件顶层主体中。
根据代码注释,循环链表中的这些隐藏更新将应用于您最新的 Fiber 节点中的钩子
现在注意函数中的这一行enqueueRenderPhaseUpdate
,
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
didScheduleRenderPhaseUpdateDuringThisPass
是一个全局变量。该变量用于 while 循环,
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
if (numberOfReRenders >= RE_RENDER_LIMIT) {
throw new Error(
'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);
}
numberOfReRenders += 1;
// some other code
children = Component(props, secondArg);
} while (didScheduleRenderPhaseUpdateDuringThisPass);
每次执行组件主体时(Component(props, secondArg);
在 while 循环中),你正在触发enqueueRenderPhaseUpdate
因为setState
在你的组件主体中设置didScheduleRenderPhaseUpdateDuringThisPass
to true
这会触发while
再次循环调用Component
again.
一旦循环执行了25次,React就会抛出错误。
您可以在以下位置找到这些功能:
-
\react\packages\react-reconciler\src\ReactFiberHooks.new.js
( 2538行 )
\react\packages\react-reconciler\src\ReactFiberHooks.old.js