我正在尝试找出useEffect导致重新渲染的时间。以下示例的结果使我感到非常惊讶:
https://codesandbox.io/embed/romantic-sun-j5i4m
/path/to/executable This\ is\ A\ Test
此示例的结果如下:
我的困惑源于两件事:我不知道为什么:
如果有人能澄清我会很高兴。谢谢!
答案 0 :(得分:3)
我将尽力解释(或逐步进行)正在发生的事情。我还在第7点和第10点做两个假设。
useEffect
在安装后被调用。useEffect
将“保存”初始状态,因此counter
在内部被引用时将为0。setCount
被调用以更新计数,并且控制台日志记录根据“存储”版本为0的计数器。因此,数字0在控制台中记录了3次。因为状态已更改(0-> 1,1-> 2,2-> 3),所以React像一个标志或类似的东西告诉自己要记住要重新渲染。useEffect
期间,React尚未重新渲染任何内容,而是等待useEffect
完成后重新渲染。useEffect
后,React会记住counter
的状态在其执行过程中已更改,因此它将重新呈现该应用程序。useCounter
。请注意,这里没有参数传递给useCounter
自定义钩子。
假设: 我自己也不知道这一点,但是我认为默认参数似乎是重新创建的,或者至少以某种方式使React认为它是新的。因此,由于arr
被视为新的,因此useEffect
钩子将再次运行。这是我可以再次解释useEffect
的唯一原因。 useEffect
期间,counter
的值为3。控制台日志将按预期记录3的数字3。useEffect
第二次运行后,React发现计数器在执行过程中发生了变化(3-> 1,1-> 2,2-> 3),因此应用将重新渲染,从而导致第三个“渲染”日志。useCounter
挂钩的内部状态在此渲染与上一渲染之间未发生变化,因此不会执行代码在其内部,因此useEffect
没有被第三次调用。因此,应用程序的第一个渲染将始终运行挂钩代码。应用程序第二次看到钩子的内部状态从0变为3,因此决定重新运行它,第三次应用程序看到钩子的内部状态为3仍然为3,它决定不重新运行它。这是我想出的让钩子不再运行的最好原因。您可以在挂钩本身中放入一个日志,以查看它实际上并没有第三次运行。 这就是我所看到的,我希望这可以使它更清晰一些。
答案 1 :(得分:2)
我在react文档中为第三个渲染找到了explanation。我认为这澄清了为什么在不应用效果的情况下做出第三次渲染的反应:
如果将状态挂钩更新为与当前状态相同的值, React将纾困而不会渲染子代或发射效果。 (React使用Object.is比较算法。)
请注意,React可能仍需要再次渲染该特定组件 救助之前。不用担心,因为React不会 不必要地“深入”到树上。如果您做的很昂贵 渲染时进行计算,您可以使用useMemo优化它们。
似乎useState和useReducer共享了这种纾困逻辑。
答案 2 :(得分:1)
setState和类似的钩子不会立即重新渲染您的组件。他们可能会批量更新或将更新推迟到以后。因此,在最新的setCount
和counter === 3
之后,您只能获得一次重新提交。
您将使用counter === 0
获得初始渲染,并使用counter === 3
获得两个其他渲染器。我不确定为什么它不会进入无限循环。 arr = [1, 2, 3]
应在每次调用时创建一个新数组并触发useEffect
:
counter
至0
useEffect
记录0
三次,将counter
设置为3
并触发重新渲染counter === 3
重新提交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钩子