Функция с большей частью того, что происходит, когда вы вызываете установщик, возвращаемый useState
, имеет значение dispatchAction
в ReactFiberHooks.js (в настоящее время начинается со строки 1009).
Блок кода, который проверяет, изменилось ли состояние (и, возможно, пропускает повторную визуализацию, если она не изменилась), в настоящее время окружен следующим условием:
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
Мое предположение при просмотре этого состояло в том, чтоэто условие оценивается как ложное после второго setTimer
вызова.Чтобы убедиться в этом, я скопировал файлы разработки CDN React и добавил некоторые журналы консоли в функцию dispatchAction
:
function dispatchAction(fiber, queue, action) {
!(numberOfReRenders < RE_RENDER_LIMIT) ? invariant(false, 'Too many re-renders. React limits the number of renders to prevent an infinite loop.') : void 0;
{
!(arguments.length <= 3) ? warning$1(false, "State updates from the useState() and useReducer() Hooks don't support the " + 'second callback argument. To execute a side effect after ' + 'rendering, declare it in the component body with useEffect().') : void 0;
}
console.log("dispatchAction1");
var alternate = fiber.alternate;
if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
// 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.
didScheduleRenderPhaseUpdate = true;
var update = {
expirationTime: renderExpirationTime,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
if (renderPhaseUpdates === null) {
renderPhaseUpdates = new Map();
}
var firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
if (firstRenderPhaseUpdate === undefined) {
renderPhaseUpdates.set(queue, update);
} else {
// Append the update to the end of the list.
var lastRenderPhaseUpdate = firstRenderPhaseUpdate;
while (lastRenderPhaseUpdate.next !== null) {
lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
}
lastRenderPhaseUpdate.next = update;
}
} else {
flushPassiveEffects();
console.log("dispatchAction2");
var currentTime = requestCurrentTime();
var _expirationTime = computeExpirationForFiber(currentTime, fiber);
var _update2 = {
expirationTime: _expirationTime,
action: action,
eagerReducer: null,
eagerState: null,
next: null
};
// Append the update to the end of the list.
var _last = queue.last;
if (_last === null) {
// This is the first update. Create a circular list.
_update2.next = _update2;
} else {
var first = _last.next;
if (first !== null) {
// Still circular.
_update2.next = first;
}
_last.next = _update2;
}
queue.last = _update2;
console.log("expiration: " + fiber.expirationTime);
if (alternate) {
console.log("alternate expiration: " + alternate.expirationTime);
}
if (fiber.expirationTime === NoWork && (alternate === null || alternate.expirationTime === NoWork)) {
console.log("dispatchAction3");
// 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.
var _eagerReducer = queue.eagerReducer;
if (_eagerReducer !== null) {
var prevDispatcher = void 0;
{
prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
var currentState = queue.eagerState;
var _eagerState = _eagerReducer(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.
_update2.eagerReducer = _eagerReducer;
_update2.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.
return;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
{
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
}
}
{
if (shouldWarnForUnbatchedSetState === true) {
warnIfNotCurrentlyBatchingInDev(fiber);
}
}
scheduleWork(fiber, _expirationTime);
}
}
, а вот вывод консоли с некоторыми дополнительными комментариями для ясности:
re-rendered 0 // initial render
dispatchAction1 // setIsOn
dispatchAction2
expiration: 0
dispatchAction3
re-rendered 0
dispatchAction1 // first call to setTimer
dispatchAction2
expiration: 1073741823
alternate expiration: 0
re-rendered 1
dispatchAction1 // second call to setTimer
dispatchAction2
expiration: 0
alternate expiration: 1073741823
re-rendered 1
dispatchAction1 // third and subsequent calls to setTimer all look like this
dispatchAction2
expiration: 0
alternate expiration: 0
dispatchAction3
NoWork
имеет значение ноль.Вы можете видеть, что первый лог fiber.expirationTime
после setTimer
имеет ненулевое значение.В журналах второго вызова setTimer
этот fiber.expirationTime
был перемещен в alternate.expirationTime
, что все еще не позволяет сравнивать состояние, поэтому повторная визуализация будет безусловной.После этого время истечения срока действия fiber
и alternate
равно 0 (NoWork), а затем выполняется сравнение состояний и предотвращается повторный рендеринг.
Это описание архитектуры React Fibre является хорошей отправной точкой для попытки понять цель expirationTime
.
Наиболее важные части исходного кода для его понимания:
Я считаю, что время истечения в основном относится к параллельному режиму, который по умолчанию еще не включен.Время истечения указывает момент времени, после которого React будет принудительно совершать работу при первой же возможности.До этого момента React может выбрать пакетное обновление.У некоторых обновлений (например, из-за взаимодействия с пользователем) истекает очень короткий (высокий приоритет), а у других обновлений (например, из асинхронного кода после завершения выборки) истекает более длинный (низкий приоритет).Обновления, инициируемые setTimer
из-за обратного вызова setInterval
, попадают в категорию с низким приоритетом и могут быть пакетными (если включен режим одновременной работы).Поскольку существует вероятность того, что эта работа была пакетирована или потенциально отброшена, React ставит повторную визуализацию безоговорочно (даже если состояние не изменилось с момента предыдущего обновления), если предыдущее обновление имело expirationTime
.
Вы можете увидеть мой ответ здесь , чтобы узнать немного больше о том, как пройти через код React, чтобы добраться до этой функции dispatchAction
.
Для тех, кто хочет покопатьсяСобственно, вот CodeSandbox с моей модифицированной версией React:
![Edit static](https://codesandbox.io/static/img/play-codesandbox.svg)
Файлы реагирования - это модифицированные копии этих файлов:
https://unpkg.com/react@16/umd/react.development.js
https://unpkg.com/react-dom@16/umd/react-dom.development.js