反应钩子渲染额外的时间 [英] React hook rendering an extra time

查看:29
本文介绍了反应钩子渲染额外的时间的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆