反应钩子渲染额外的时间 [英] React hook rendering an extra time
问题描述
我的代码导致了意外数量的重新渲染.
function App() {const [isOn, setIsOn] = useState(false)const [定时器,setTimer] = useState(0)console.log('重新渲染', 计时器)useEffect(() => {让间隔如果(isOn){间隔 = setInterval(() => setTimer(timer + 1), 1000)}返回 () =>清除间隔(间隔)}, [isOn])返回 (<div>{计时器}{!isOn &&(<button type="button" onClick={() =>setIsOn(true)}>开始按钮>)}{isOn &&(<button type="button" onClick={() =>setIsOn(false)}>停止按钮>)}
);}
注意第 4 行的 console.log.我期望注销以下内容:
重新渲染 0
重新渲染 0
重新渲染 1
第一个日志用于初始渲染.第二个日志用于在isOn"状态通过单击按钮更改时重新渲染.第三个日志是当 setInterval 调用 setTimer 时,它会再次重新渲染.这是我实际得到的:
重新渲染 0
重新渲染 0
重新渲染 1
重新渲染 1
我不明白为什么会有第四个日志.这是它的 REPL 链接:
反应文件是这些文件的修改副本:
https://unpkg.com/react@16/umd/react.development.jshttps://unpkg.com/react-dom@16/umd/react-dom.development.js
My code is causing an unexpected amount of re-renders.
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>
);
}
Note the console.log on line 4. What I expected is the following to be logged out:
re-rendered 0
re-rendered 0
re-rendered 1
The first log is for the initial render. The second log is for the re-render when the "isOn" state changes via the button click. The third log is when setInterval calls setTimer so it's re-rendered again. Here is what I actually get:
re-rendered 0
re-rendered 0
re-rendered 1
re-rendered 1
I can't figure out why there is a fourth log. Here's a link to a REPL of it:
https://codesandbox.io/s/kx393n58r7
***Just to clarify, I know the solution is to use setTimer(timer => timer + 1), but I would like to know why the code above causes a fourth render.
The function with the bulk of what happens when you call the setter returned by useState
is dispatchAction
within ReactFiberHooks.js (currently starting at line 1009).
The block of code that checks to see if the state has changed (and potentially skips the re-render if it has not changed) is currently surrounded by the following condition:
if (
fiber.expirationTime === NoWork &&
(alternate === null || alternate.expirationTime === NoWork)
) {
My assumption on seeing this was that this condition evaluated to false after the second setTimer
call. To verify this, I copied the development CDN React files and added some console logs to the dispatchAction
function:
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);
}
}
and here's the console output with some additional comments for clarity:
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
has a value of zero. You can see that the first log of fiber.expirationTime
after setTimer
has a non-zero value. In the logs from the second setTimer
call, that fiber.expirationTime
has been moved to alternate.expirationTime
still preventing the state comparison so re-render will be unconditional. After that, both the fiber
and alternate
expiration times are 0 (NoWork) and then it does the state comparison and avoids a re-render.
This description of the React Fiber Architecture is a good starting point for trying to understand the purpose of expirationTime
.
The most relevant portions of the source code for understanding it are:
I believe the expiration times are mainly relevant for concurrent mode which is not yet enabled by default. The expiration time indicates the point in time after which React will force a commit of the work at the earliest opportunity. Prior to that point in time, React may choose to batch updates. Some updates (such as from user interactions) have a very short (high priority) expiration, and other updates (such as from async code after a fetch completes) have a longer (low priority) expiration. The updates triggered by setTimer
from within the setInterval
callback would fall in the low priority category and could potentially be batched (if concurrent mode were enabled). Since there is the possibility of that work having been batched or potentially discarded, React queues a re-render unconditionally (even when the state is unchanged since the previous update) if the previous update had an expirationTime
.
You can see my answer here to learn a bit more about finding your way through the React code to get to this dispatchAction
function.
For others who want to do some digging of their own, here's a CodeSandbox with my modified version of React:
The react files are modified copies of these files:
https://unpkg.com/react@16/umd/react.development.js
https://unpkg.com/react-dom@16/umd/react-dom.development.js
这篇关于反应钩子渲染额外的时间的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!