当前位置: 首页 > 面试题库 >

React钩子渲染额外的时间

桑博远
2023-03-14
问题内容

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

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


 类似资料:
  • 我有一个问题,我正在尝试将值设置为一个状态(选定的Pathway),关于通过 redux 设置的另一个常量(国家标签)。 设置“selectedPatway”后,我想在<code>中显示结果 一切都很好,但是当我刷新页面时,我在浏览器中得到一个“渲染的钩子比之前渲染的钩子多”。这是代码: 我浏览了整个互联网,我认为我已经正确地应用了所有东西(显然不是因为它不工作…)。 不有条件地调用钩子 在顶层使

  • 我有一个带有对象数组的组件,其中我正在根据字符串进行过滤。问题是当我尝试将此过滤器的返回设置为本地状态时,它会抛出错误,我不太理解原因。 所以,因为我希望这个数组处于我的状态,所以我决定这样做: 插入这一行后发生的事情是这样的: 它开始多次渲染。我假设,每次状态改变时,它都会重新渲染组件(如果我错了,请纠正我)。不过,我不知道它为什么要多次这样做。 因此,我想过使用 useEffect 来实现此处

  • 今天遇到了钩子的问题。我知道有一个类似的帖子,我阅读了使用钩子的规则。现在,当我发布表单时,它给了我这个错误。我知道这是因为我的钩子在if语句里面。但是我怎样才能把它弄出来呢?我不知道如果它不在函数或语句中,那么如何使用这个钩子。任何建议将不胜感激。代码如下: 自定义挂钩:

  • 根据文件: 在更新发生后立即调用。初始渲染时不调用此方法。 我们可以使用新的钩子来模拟,但是每次渲染后,甚至第一次渲染后都会运行。如何使其不在初始渲染时运行? 正如您在下面的示例中所看到的,在初始渲染过程中打印了,但是在初始渲染过程中没有打印。

  • 我有以下组件: ) 本质上,我希望<code>subsectionRefs调用<code>setRankChangeFocus(value)时重新初始化自身。 但是,当我加载页面时,我得到了“渲染的钩子比之前渲染的钩子多”错误-为什么?我做错了什么?

  • 如何将 2 个 graphql 查询与 react-apollo-hooks 结合使用,其中第 2 个查询依赖于从第 1 个查询中检索到的参数? 我尝试使用如下所示的两个查询: 然而,当我运行我的项目时,react-hooks给我以下消息: " index.js:1437警告:React检测到Upgrade调用的挂钩顺序发生了变化。如果不修复,这将导致错误和错误。有关更多信息,请阅读“挂钩规则”