useEffect Hook示例:是什么原因导致重新渲染?

时间:2019-06-14 13:51:16

标签: reactjs react-hooks

我正在尝试找出useEffect导致重新渲染的时间。以下示例的结果使我感到非常惊讶:

https://codesandbox.io/embed/romantic-sun-j5i4m

/path/to/executable This\ is\ A\ Test

此示例的结果如下:

enter image description here

我的困惑源于两件事:我不知道为什么:

  1. 该组件仅渲染3次(我​​猜想,每次调用setCount +一个初始渲染-4次,该组件都会重新渲染)
  2. 该计数器永远只有两个值0和3:我猜想,由于此article状态,每个渲染器都会看到自己的状态和属性,因此整个循环将以每个状态为常数(1, 2,3)->但是为什么国家永远不会2?

如果有人能澄清我会很高兴。谢谢!

5 个答案:

答案 0 :(得分:3)

我将尽力解释(或逐步进行)正在发生的事情。我还在第7点和第10点做两个假设。

  1. 应用程序组件安装。
  2. useEffect在安装后被调用。
  3. useEffect将“保存”初始状态,因此counter在内部被引用时将为0。
  4. 循环运行3次。每次迭代setCount被调用以更新计数,并且控制台日志记录根据“存储”版本为0的计数器。因此,数字0在控制台中记录了3次。因为状态已更改(0-> 1,1-> 2,2-> 3),所以React像一个标志或类似的东西告诉自己要记住要重新渲染。
  5. 在执行useEffect期间,React尚未重新渲染任何内容,而是等待useEffect完成后重新渲染。
  6. 完成useEffect后,React会记住counter的状态在其执行过程中已更改,因此它将重新呈现该应用程序。
  7. 该应用重新渲染,并且再次调用useCounter。请注意,这里没有参数传递给useCounter自定义钩子。 假设: 我自己也不知道这一点,但是我认为默认参数似乎是重新创建的,或者至少以某种方式使React认为它是新的。因此,由于arr被视为新的,因此useEffect钩子将再次运行。这是我可以再次解释useEffect的唯一原因。
  8. 在第二次运行useEffect期间,counter的值为3。控制台日志将按预期记录3的数字3。
  9. useEffect第二次运行后,React发现计数器在执行过程中发生了变化(3-> 1,1-> 2,2-> 3),因此应用将重新渲染,从而导致第三个“渲染”日志。
  10. 假设: 因为从应用程序的角度来看,useCounter挂钩的内部状态在此渲染与上一渲染之间未发生变化,因此不会执行代码在其内部,因此useEffect没有被第三次调用。因此,应用程序的第一个渲染将始终运行挂钩代码。应用程序第二次看到钩子的内部状态从0变为3,因此决定重新运行它,第三次应用程序看到钩子的内部状态为3仍然为3,它决定不重新运行它。这是我想出的让钩子不再运行的最好原因。您可以在挂钩本身中放入一个日志,以查看它实际上并没有第三次运行。

这就是我所看到的,我希望这可以使它更清晰一些。

答案 1 :(得分:2)

我在react文档中为第三个渲染找到了explanation。我认为这澄清了为什么在不应用效果的情况下做出第三次渲染的反应:

  

如果将状态挂钩更新为与当前状态相同的值,   React将纾困而不会渲染子代或发射效果。   (React使用Object.is比较算法。)

     

请注意,React可能仍需要再次渲染该特定组件   救助之前。不用担心,因为React不会   不必要地“深入”到树上。如果您做的很昂贵   渲染时进行计算,您可以使用useMemo优化它们。

似乎useState和useReducer共享了这种纾困逻辑。

答案 2 :(得分:1)

setState和类似的钩子不会立即重新渲染您的组件。他们可能会批量更新或将更新推迟到以后。因此,在最新的setCountcounter === 3之后,您只能获得一次重新提交。

您将使用counter === 0获得初始渲染,并使用counter === 3获得两个其他渲染器。我不确定为什么它不会进入无限循环。 arr = [1, 2, 3]应在每次调用时创建一个新数组并触发useEffect

  1. 初始渲染集counter0
  2. useEffect记录0三次,将counter设置为3并触发重新渲染
  3. 首先使用counter === 3重新提交
  4. useEffect记录3三次,将counter设置为3和???

反应应在此处停止或从第3步进入无限循环。

答案 3 :(得分:0)

有一个巧合可能会在原始问题上造成一些混乱。主要是因为有3个渲染,而useCounter的默认参数长度等于3。下面您可以看到,即使对于更大的数组,也只有3个渲染。

function useCounter(arr = [1, 2, 3, 4 , 5 , 6]) {
  const [counter, setCount] = React.useState(0);
  React.useEffect(() => {
    for (const i of arr) {
      setCount(i);
      console.log(counter);
    }
  }, [arr]);
}

function App() {
  useCounter();
  console.log("render");
  return <div className = "App" / > ;
}

ReactDOM.render( <App /> ,
  document.getElementById("root")
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"></div>

另一个问题可能是,每次调用setState除外,除了第一个,它具有相同的值(数组的最后一个值),实际上取消了渲染。但是,如果setState用不同的值调用,则所呈现的流程将创建一个无限循环:)

因为其他每一个render都会触发一个useEffect,它会触发一个setSate,这个触发会触发一个render,这个触发会触发一个useEffect,依此类推。

希望这会使某人更清楚。

答案 4 :(得分:0)

以上解决方案非常说明了代码中正在发生的事情。如果有人在自定义钩子中使用默认参数时正在寻找如何避免重新渲染的方法。这是一个可能的解决方案。

import React, { useEffect, useState } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

const defaultVal = [1, 2, 3];

function useCounter(arr = defaultVal) {
  const [counter, setCount] = useState(0);

  useEffect(() => {
    console.log(counter);
    setCount(arr);
  }, [counter, arr]);

  return counter;
}

function App() {
  const counter = useCounter();
  console.log("render");
  return (
    <div className="App">
      <div>{counter}</div>
    </div>
  );
}

const rootElement = document.getElementById("root");

ReactDOM.render(<App />, rootElement);

说明:由于没有为自定义挂钩提供任何值,因此它采用的是默认值,该值是常量defaultVal。这意味着arr引用始终是相同的。由于引用未更改,因此不会触发useEffect钩子