Reactjs 多做一次反应

Reactjs 多做一次反应,reactjs,react-hooks,Reactjs,React Hooks,我的代码导致了意外的重新渲染量 function App() { const [isOn, setIsOn] = useState(false) const [timer, setTimer] = useState(0) console.log('re-rendered', timer) useEffect(() => { let interval if (isOn) { interval

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

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>
    );
 }
函数App(){
const[isOn,setIsOn]=useState(false)
常量[timer,setTimer]=useState(0)
console.log('重新呈现',计时器)
useffect(()=>{
让间隔
国际单项体育联合会{
间隔=设置间隔(()=>设置定时器(定时器+1),1000)
}
return()=>clearInterval(间隔)
},[isOn])
返回(
{计时器}
{!伊森&&(
setIsOn(对)}>
开始
)}
{isOn&&(
setIsOn(假)}>
停止
)}
);
}
注意第4行的console.log。我所期望的是注销以下内容:

重新渲染0

重新渲染0

重新渲染1

第一个日志用于初始渲染。第二个日志用于通过单击按钮更改“isOn”状态时重新渲染。第三个日志是setInterval调用setTimer时的日志,以便再次呈现。以下是我实际得到的:

重新渲染0

重新渲染0

重新渲染1

重新渲染1

我不明白为什么会有第四根圆木。以下是它的一个REPL链接:


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

调用由
useState
返回的setter时发生的大部分事情的函数是
dispatchAction
in(当前从第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);
  }
}
NoWork
的值为零。您可以看到
setTimer
之后的
fiber.expirationTime
的第一个日志具有非零值。在第二次
setTimer
调用的日志中,
fiber.expirationTime
已移动到
alternate.expirationTime
仍然阻止状态比较,因此重新呈现将是无条件的。之后,
光纤
备用
过期时间均为0(NoWork),然后进行状态比较,避免重新渲染

这是理解
expirationTime
的目的的一个很好的起点

理解源代码最相关的部分是:

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

您可以查看我的答案,了解有关如何通过React代码找到此
dispatchAction
函数的更多信息

对于其他想自己挖掘的人,这里有一个代码沙盒,其中包含我修改后的React版本:

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

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

useReducer
@mohamedramramami一样,
useState
中的setter触发与
useReducer
中的调度函数相同的代码(我在回答中提到的
dispatchAction
函数)。因此,是的,关于我的答案的所有内容也适用于
useReducer
useState
setter基本上是一个带有一个小减速机的分派。您能否详细说明为什么
setTimer(timer=>timer+1)
是解决方案,或者为什么支持这种语法?我想我在文档中没有看到过这样的内容。文档如下:
https://unpkg.com/react@16/umd/react.development.js
https://unpkg.com/react-dom@16/umd/react-dom.development.js