useEffect Hook 示例:导致重新渲染的原因是啥?

Posted

技术标签:

【中文标题】useEffect Hook 示例:导致重新渲染的原因是啥?【英文标题】:useEffect Hook Example: What causes the re-render?useEffect Hook 示例:导致重新渲染的原因是什么? 【发布时间】:2019-10-29 04:11:21 【问题描述】:

我试图弄清楚 useEffect 何时会导致重新渲染。我对以下示例的结果感到非常惊讶:

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

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


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

这个例子的结果如下:

我不知道为什么:

    组件仅渲染 3 次(我猜该组件会在每次调用 setCount + 一次初始渲染时重新渲染 - 所以 4 次) 计数器只有两个值 0 和 3:我猜,正如 article 所指出的,每个渲染都会看到自己的状态和道具,因此整个循环将以每个状态作为常数运行(1、2、 3) --> 但是为什么状态永远不是 2?

【问题讨论】:

设置状态是异步的,也不会改变counter 变量的值。所以即使你调用setState(...),函数内部的counter值也会被保留。这就是为什么您在第一次运行中看到 3x0 零,然后在第二次运行中看到 3x3(最后一次调用是 setCount(3) 所以这就是 counter 将在下一次重新渲染时设置为)。 好的 - 我知道计数器应该是特定渲染中的常量。但是 setCount 应该异步调度。我会假设 setCount 会触发重新渲染,其中计数器是 1 然后 2 然后 3。所以,我希望输出像 3x0 然后 3x1, 3x2 ... 如果你这样做setCount(1); setCount(2); setCount(3),那么在下一次重新渲染期间counter 将是 3。React 不仅仅应用一个状态更新,而是所有这些更新——而且它发生了您多次覆盖相同的状态。 感谢您的评论。但究竟是什么导致了重新渲染?如果更改状态不会立即导致重新渲染?归根结底,react 不知道我的数组有 3 个元素,第三个元素之后需要重新渲染? 现在 React DevTools 中有一个实验性功能可以回答这个确切的问题 - Why did this render? - 这是你可以如何install it。 【参考方案1】:

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

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

这就是我看到的情况,我希望这能让它更清楚一点。

【讨论】:

谢谢,很好的解释!但是有一个问题:如果 react 如此聪明地检测到状态没有改变并且没有运行 useEffect 为什么它首先运行渲染呢?如果没有任何变化,render 将返回相同的输出,并且不会将任何更改提交给 dom :-) 老实说,我对此没有真正的解释。可以假设,如果在执行useEffect 之前和之后的状态完全相同,那么组件将不会有任何更改要渲染,因此应该没有合理的理由重新渲染。 React 可能会意外执行此操作或允许奇怪的强制更新,但如果程序员打算强制更新组件,我个人认为他们应该更改状态变量,而不是更改它然后将其重新更改为原始状态价值。 “如果 react 如此聪明地检测到状态没有改变并且没有运行 useEffect 为什么它首先运行渲染呢?” 据我所知,useEffect 至少在第一次渲染并解释你的问题【参考方案2】:

我在 react 文档中找到了第三个渲染的 explanation。我认为这说明了为什么 react 在没有应用效果的情况下进行第三次渲染:

如果您将状态挂钩更新为与当前状态相同的值, React 将在不渲染子元素或触发效果的情况下退出。 (React 使用 Object.is 比较算法。)

请注意,React 可能仍需要再次渲染该特定组件 在保释之前。这不应该是一个问题,因为 React 不会 不必要地“深入”到树中。如果你做的很贵 渲染时计算,您可以使用 useMemo 对其进行优化。

useState 和 useReducer 似乎共享这种救助逻辑。

【讨论】:

【参考方案3】:

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

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

    初始渲染设置counter0 useEffect 记录 0 三次,将 counter 设置为 3 并触发重新渲染 第一次用counter === 3重新渲染 useEffect 记录3 3 次,将counter 设置为3 和???

React 应该在此处停止,或者从第 3 步开始进入无限循环。

【讨论】:

React 进行第三次渲染但不运行 useEffect。为什么? 我看不出有任何理由阻止 useEffect 在第三次渲染上运行。但即使您删除第二个参数,它也不会运行。我认为 react 不会用这段代码第三次渲染。 是的。这是这个问题中最有趣的部分 :-) 我认为当没有给出第二个参数时,它会在每次渲染后运行,但显然不是真的。 我实际上是在尝试用这个沙箱创建一个无限循环,因为我的项目中有非常相似的代码会创建一个无限循环,我想弄清楚 useEffect 中的 [] 是否会导致重新渲染或者只是发出信号来响应在 .下一次重新渲染。 我在下面的答案中添加了第三次渲染的反应文档中的解释。【参考方案4】:

有一个巧合可能会在原始问题中造成一些混乱。主要是因为有 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 等等。

希望这能让某些人更清楚。

【讨论】:

是的,你的 ans 有帮助。但是您能否建议我如何停止在控制台中重新渲染“渲染”...是否可以通过任何类型的钩子来停止重新渲染组件?【参考方案5】:

上述解决方案很好地解释了代码中发生的事情。如果有人正在寻找如何在自定义挂钩中使用默认参数时避免重新渲染。这是一个可能的解决方案。

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 钩子

【讨论】:

【参考方案6】:

这个问题和我阅读的所有答案都非常有见地,可以更好地理解 useEffect 和 useState 钩子,因为它们迫使我深入了解这些钩子。

虽然@ApplePearPerson 的回答非常明确,但我确实相信存在一些不正确的方面,我会用几个例子指出它们:

    组件被渲染,因此控制台中的第一个“渲染”。

    UseEffect 总是至少运行一个,在第一次渲染之后,这基本上 解释第二次渲染,这是为什么首先打印的棘手部分 0 x (计数器的初始值)

    useState 钩子的第二个参数是一个异步函数,因此具有异步行为:它等待其他代码运行,因此它等待 for in 块运行。

    所以 for in 块运行等等:

    i 从 1 变为 3,完成值为 3

    此时setCount改变计数器从0 t0 3

    如果有数组作为第二个参数,Useffect 会在依赖项更改时运行,所以在这种情况下,即使它不包含在内,它也会在从 setCount 更改的计数器上运行,正如你甚至可以从 Eslint 警告中看到的那样(React Hook useEffect缺少依赖项:'counter')

    hook one a render 的 useState 更改状态原因(这就是为什么引入 useRef 来更改 dom 元素而不导致重新渲染),尽管类中的 setState 并非总是如此(但这是另一个主题)

    最后一次渲染是因为每次渲染都会重新创建 arr,因为 ApplePearPerson “注意到​​”但它是一个全新的数组作为组件 已重新渲染,但计数器为 3,与 我拥有的最后一个值,也正好是 3,所以 useEffect 不再运行。

    这个截图可以帮助可视化我的总结

例如,如果我们用 for in 更改 for of,这意味着我们获取数组的键(即字符串),我们会看到在这种情况下 counter 的最后一个值为 2

https://codesandbox.io/s/kind-surf-oq02y?file=/src/App.js

可以通过添加设置为前一个的第二个计数器来完成另一个测试。 在这种情况下,我们获得了第四个 Render,因为 count2 落后于 1 个 useffect,并且它从 0 变为 3 会触发最后一次渲染,但不会触发最后一次 useEffect 运行。

总结一下:

有3个渲染:

首先是由于组件首先挂载。

第二个是由于 useEffect 在第一次渲染之后运行。

三是由于从0到3的依赖变化

https://codesandbox.io/s/kind-surf-oq02y?file=/src/App.js:362-383

【讨论】:

以上是关于useEffect Hook 示例:导致重新渲染的原因是啥?的主要内容,如果未能解决你的问题,请参考以下文章

useEffect() 导致重新渲染和对 api 的多个请求

如何在 React Native 应用程序中使用 React hook useEffect 为每 5 秒渲染设置间隔?

React - 带有异步 setState 的 useEffect 导致额外的重新渲染

useSelector 和 useEffect 重新渲染优化

如何阻止多个重新渲染执行多个 api 调用 useEffect?

有条件地调用 React Hook “useEffect”。在每个组件渲染中,必须以完全相同的顺序调用 React Hooks