React hook 渲染额外的时间
React hook rendering an extra time
我的代码导致了意外数量的重新呈现。
function App() {
const [isOn, setIsOn] = useState(false)
const [timer, setTimer] = useState(0)
console.log('re-rendered', timer)
useEffect(() => {
let interval
if (isOn) {
interval = setInterval(() => setTimer(timer + 1), 1000)
}
return () => clearInterval(interval)
}, [isOn])
return (
<div>
{timer}
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</div>
);
}
注意第 4 行的 console.log。我希望注销以下内容:
重新渲染 0
重新渲染 0
重新渲染 1
第一个日志用于初始渲染。第二个日志用于在 "isOn" 状态通过单击按钮更改时重新呈现。第三个日志是在 setInterval 调用 setTimer 时再次重新呈现。这是我实际得到的:
重新渲染 0
重新渲染 0
重新渲染 1
重新渲染 1
我想不通为什么会有第四条日志。这是它的 REPL 的 link:
https://codesandbox.io/s/kx393n58r7
***澄清一下,我知道解决方案是使用 setTimer(timer => timer + 1),但我想知道为什么上面的代码会导致第四次渲染。
调用 useState
返回的 setter 时发生的大部分事情的函数是 ReactFiberHooks.js 中的 dispatchAction
(当前从第 1009 行开始)。
检查状态是否已更改(如果未更改则可能跳过重新渲染)的代码块当前被以下条件包围:
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
我看到这个假设是在第二次 setTimer
调用后这个条件被评估为 false。为了验证这一点,我复制了开发 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(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 || alternate !== null && alternate === currentlyRenderingFiber) {
// 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.current;
ReactCurrentDispatcher.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.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
的值为零。可以看到 setTimer
之后的第一个日志 fiber.expirationTime
有一个非零值。在第二次 setTimer
调用的日志中,fiber.expirationTime
已移至 alternate.expirationTime
仍然阻止状态比较,因此重新渲染将是无条件的。之后,fiber
和 alternate
到期时间均为 0(NoWork),然后进行状态比较并避免重新渲染。
This description of the React Fiber Architecture 是试图理解 expirationTime
.
目的的一个很好的起点
源代码中与理解它最相关的部分是:
我认为到期时间主要与默认情况下尚未启用的并发模式有关。到期时间表示 React 将尽早强制提交工作的时间点。在那个时间点之前,React 可能会选择批量更新。一些更新(例如来自用户交互)的到期时间非常短(高优先级),而其他更新(例如来自提取完成后的异步代码)的到期时间较长(低优先级)。由 setInterval
回调中的 setTimer
触发的更新将属于低优先级类别,并且可能会被批处理(如果启用了并发模式)。由于该工作有可能已被批处理或可能被丢弃,如果上一次更新有 expirationTime
.[=37=,React 将无条件地重新渲染队列(即使自上次更新以来状态未更改) ]
您可以查看我的回答 以了解更多有关如何通过 React 代码找到此 dispatchAction
函数的方法。
对于其他想要自己进行一些挖掘的人,这里有一个 CodeSandbox,其中包含我修改过的 React 版本:
反应文件是这些文件的修改副本:
https://unpkg.com/react@16/umd/react.development.js
https://unpkg.com/react-dom@16/umd/react-dom.development.js
我的代码导致了意外数量的重新呈现。
function App() {
const [isOn, setIsOn] = useState(false)
const [timer, setTimer] = useState(0)
console.log('re-rendered', timer)
useEffect(() => {
let interval
if (isOn) {
interval = setInterval(() => setTimer(timer + 1), 1000)
}
return () => clearInterval(interval)
}, [isOn])
return (
<div>
{timer}
{!isOn && (
<button type="button" onClick={() => setIsOn(true)}>
Start
</button>
)}
{isOn && (
<button type="button" onClick={() => setIsOn(false)}>
Stop
</button>
)}
</div>
);
}
注意第 4 行的 console.log。我希望注销以下内容:
重新渲染 0
重新渲染 0
重新渲染 1
第一个日志用于初始渲染。第二个日志用于在 "isOn" 状态通过单击按钮更改时重新呈现。第三个日志是在 setInterval 调用 setTimer 时再次重新呈现。这是我实际得到的:
重新渲染 0
重新渲染 0
重新渲染 1
重新渲染 1
我想不通为什么会有第四条日志。这是它的 REPL 的 link:
https://codesandbox.io/s/kx393n58r7
***澄清一下,我知道解决方案是使用 setTimer(timer => timer + 1),但我想知道为什么上面的代码会导致第四次渲染。
调用 useState
返回的 setter 时发生的大部分事情的函数是 ReactFiberHooks.js 中的 dispatchAction
(当前从第 1009 行开始)。
检查状态是否已更改(如果未更改则可能跳过重新渲染)的代码块当前被以下条件包围:
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
我看到这个假设是在第二次 setTimer
调用后这个条件被评估为 false。为了验证这一点,我复制了开发 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(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 || alternate !== null && alternate === currentlyRenderingFiber) {
// 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.current;
ReactCurrentDispatcher.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.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
的值为零。可以看到 setTimer
之后的第一个日志 fiber.expirationTime
有一个非零值。在第二次 setTimer
调用的日志中,fiber.expirationTime
已移至 alternate.expirationTime
仍然阻止状态比较,因此重新渲染将是无条件的。之后,fiber
和 alternate
到期时间均为 0(NoWork),然后进行状态比较并避免重新渲染。
This description of the React Fiber Architecture 是试图理解 expirationTime
.
源代码中与理解它最相关的部分是:
我认为到期时间主要与默认情况下尚未启用的并发模式有关。到期时间表示 React 将尽早强制提交工作的时间点。在那个时间点之前,React 可能会选择批量更新。一些更新(例如来自用户交互)的到期时间非常短(高优先级),而其他更新(例如来自提取完成后的异步代码)的到期时间较长(低优先级)。由 setInterval
回调中的 setTimer
触发的更新将属于低优先级类别,并且可能会被批处理(如果启用了并发模式)。由于该工作有可能已被批处理或可能被丢弃,如果上一次更新有 expirationTime
.[=37=,React 将无条件地重新渲染队列(即使自上次更新以来状态未更改) ]
您可以查看我的回答 dispatchAction
函数的方法。
对于其他想要自己进行一些挖掘的人,这里有一个 CodeSandbox,其中包含我修改过的 React 版本:
反应文件是这些文件的修改副本:
https://unpkg.com/react@16/umd/react.development.js
https://unpkg.com/react-dom@16/umd/react-dom.development.js