React钩子渲染额外的时间

我的代码导致了意外的重新渲染。

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的链接:

https://codesandbox.io/s/kx393n58r7

***请澄清一下,我知道解决方案是使用setTimer(timer => timer + 1),但是我想知道为什么上面的代码会导致第四个渲染。

回答:

该函数具有大量的功能,这些功能在您调用由返回的setter时发生的事情useStatedispatchAction在ReactFiberHooks.js中(当前从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$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.expirationTimeafter

的第一个日志setTimer具有非零值。在第二个setTimer调用的日志中,该日志fiber.expirationTime已移至alternate.expirationTime仍阻止状态比较,因此重新呈现将是无条件的。之后,fiberalternate到期时间均为0(NoWork),然后进行状态比较并避免重新渲染。

对React Fiber Architecture的描述是尝试了解的目的的一个很好的起点expirationTime

用于理解它的源代码中最相关的部分是:

  • ReactFiberExpirationTime.js
  • ReactFiberScheduler.js

我相信到期时间主要与并发模式有关,并发模式默认情况下尚未启用。到期时间表示React将尽早强制提交工作的时间点。在该时间点之前,React可以选择批量更新。某些更新(例如,来自用户交互的更新)的到期时间非常短(高优先级),而其他更新(例如,提取完成后的来自异步代码的更新)的到期时间则更长(低优先级)。由setTimer内触发的更新setInterval回调将属于低优先级类别,并且有可能被分批处理(如果启用了并发模式)。由于有可能该工作已被批处理或可能被丢弃,因此如果先前的更新具有,React将无条件地将重新渲染排队(即使自上次更新以来状态不变)expirationTime

您可以在这里看到我的答案,以了解更多有关通过React代码找到实现此dispatchAction功能的方法的知识。

对于其他想要自己挖掘的人,这是一个带有我的React修改版的CodeSandbox:

修改静态

react文件是这些文件的修改后的副本:

https://unpkg.com/react@16/umd/react.development.js

https://unpkg.com/react-dom@16/umd/react-dom.development.js

以上是 React钩子渲染额外的时间 的全部内容, 来源链接: utcz.com/qa/407766.html

回到顶部