React 组件有时会在状态不变的情况下渲染两次

Posted

技术标签:

【中文标题】React 组件有时会在状态不变的情况下渲染两次【英文标题】:React component sometimes renders twice with unchanged state 【发布时间】:2020-07-08 01:25:07 【问题描述】:

我正在使用 Redux 订阅商店并更新组件。

这是一个没有 Redux 的简化示例。它使用模拟商店进行订阅和分发。

请按照 sn-p 下面的步骤重现问题。

编辑:请跳至更新下的第二个演示 sn-p,以获得更简洁和更接近现实生活的场景。问题不是关于 Redux。这是关于 React 的 setState 函数标识在某些情况下导致重新渲染,即使状态没有改变。

Edit 2:在“Update 2”下添加了更简洁的演示。

const useState, useEffect = React;

let counter = 0;

const createStore = () => 
	const listeners = [];
	
	const subscribe = (fn) => 
		listeners.push(fn);
		return () => 
			listeners.splice(listeners.indexOf(fn), 1);
		;
	
	
	const dispatch = () => 
		listeners.forEach(fn => fn());
	;
	
	return dispatch, subscribe;
;

const store = createStore();

function Test() 
	const [yes, setYes] = useState('yes');
	
	useEffect(() => 
		return store.subscribe(() => 
			setYes('yes');
		);
	, []);
	
	console.log(`Rendered $++counter`);
	
	return (
		<div>
			<h1>yes</h1>
			<button onClick=() => 
				setYes(yes === 'yes' ? 'no' : 'yes');
			>Toggle</button>
			<button onClick=() => 
				store.dispatch();
			>Set to Yes</button>
		</div>
	);


ReactDOM.render(<Test />, document.getElementById('root'));
<div id="root"></div>
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>

发生了什么

    ✅ 单击“设置为是”。由于yes 的值已经是“yes”,所以状态没有改变,因此组件不会重新渲染。 ✅ 点击“切换”。 yes 设置为“否”。状态已经改变,所以组件被重新渲染。 ✅ 单击“设置为是”。 yes 设置为“是”。状态再次发生变化,因此重新渲染了组件。 ⛔ 再次单击“设置为是”。状态没有改变,但组件仍然重新渲染。 ✅ 后续点击“设置为是”不会按预期导致重新渲染。

预计会发生什么

在第 4 步,组件不应重新渲染,因为状态未更改。

更新

作为React docs 状态,useEffect

适用于许多常见的副作用,例如设置 订阅和事件处理程序...

这样的一个用例可能是监听浏览器事件,例如 onlineoffline

在这个例子中,我们在组件第一次渲染时调用useEffect中的函数一次,传递一个空数组[]。该函数为在线状态更改设置事件侦听器。

假设,在应用程序的界面中,我们还有一个按钮可以手动切换在线状态。

请按照 sn-p 下面的步骤重现问题。

const useState, useEffect = React;

let counter = 0;

function Test() 
	const [online, setOnline] = useState(true);
	
	useEffect(() => 
		const onOnline = () => 
			setOnline(true);
		;
		const onOffline = () => 
			setOnline(false);
		;
		window.addEventListener('online', onOnline);
		window.addEventListener('offline', onOffline);
		
		return () => 
			window.removeEventListener('online', onOnline);
			window.removeEventListener('offline', onOffline);
		
	, []);
	
	console.log(`Rendered $++counter`);
	
	return (
		<div>
			<h1>online ? 'Online' : 'Offline'</h1>
			<button onClick=() => 
				setOnline(!online);
			>Toggle</button>
		</div>
	);


ReactDOM.render(<Test />, document.getElementById('root'));
<div id="root"></div>
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>

发生了什么

    ✅首先在屏幕上渲染组件,并在控制台中记录消息。 ✅ 点击“切换”。 online 设置为 false。状态已经改变,所以组件被重新渲染。 ⛔ 打开开发工具并在网络面板中切换到“离线”。 online 已经是 false,因此状态没有改变,但组件仍然重新渲染。

预计会发生什么

在第 3 步,组件不应重新渲染,因为状态未更改。

更新 2

const useState, useEffect = React;

let counterRenderComplete = 0;
let counterRenderStart = 0;


function Test() 
  const [yes, setYes] = useState('yes');

  console.log(`Component function called $++counterRenderComplete`);
  
  useEffect(() => console.log(`Render completed $++counterRenderStart`));

  return (
    <div>
      <h1>yes ? 'yes' : 'no'</h1>
      <button onClick=() => 
        setYes(!yes);
      >Toggle</button>
      <button onClick=() => 
        setYes('yes');
      >Set to Yes</button>
    </div>
  );


ReactDOM.render(<Test />, document.getElementById('root'));

 
<div id="root"></div>
<script src="https://unpkg.com/react/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom/umd/react-dom.development.js"></script>

发生了什么

    ✅ 单击“设置为是”。由于yes 的值已经是true,因此状态不变,因此不会重新渲染组件。 ✅ 点击“切换”。 yes 设置为 false。状态已经改变,所以组件被重新渲染。 ✅ 单击“设置为是”。 yes 设置为 true。状态再次发生变化,因此重新渲染了组件。 ⛔ 再次单击“设置为是”。尽管组件通过调用函数开始渲染过程,但状态没有改变。尽管如此,React 会在进程中间的某个地方退出渲染,并且不会调用效果。 ✅ 后续点击“设置为是”不会按预期导致重新渲染(函数调用)。

问题

为什么组件仍然重新渲染?我做错了什么,还是这是一个 React 错误?

【问题讨论】:

我相信,在调用setState之后,用相同的值避免不必要的重新渲染只是性能优化,而不是语义保证……我没有看过源代码,但我的第一个如果从嵌套在 useEffect 内的函数调用堆栈调用 setState,则猜测将是一个选择退出(但可能是完全不同的原因) 【参考方案1】:

答案是 React 使用一组启发式方法来确定是否可以避免再次调用渲染函数。这些启发式方法可能会在版本之间发生变化,并且不能保证在状态相同时总是退出。 React 提供的唯一保证是如果状态相同,它不会重新渲染子组件

您的渲染函数应该是纯的。因此,它们运行多少次并不重要。如果你在你的渲染函数中计算一些昂贵的东西并且担心调用它而不是必要的,你可以将这个计算包装在useMemo中。

一般来说,在 React 中“计数渲染”是没有用的。 React 何时调用您的函数取决于 React 本身,并且确切的时间将在版本之间不断变化。这不是合同的一部分。

【讨论】:

谢谢。这几乎回答了我的问题。在这件事上,函数组件与类组件的行为不同,这有点奇怪。对于纯类组件,如果状态未更改,则永远不会调用 render 方法。使用 memo() 记忆一个函数组件只会检查 props 的变化,而不是状态。所以,据我了解,目前还没有办法让函数组件既纯 props 又纯 state。正如您所指出的,这不应该是一个问题,因为我们应该在函数调用期间避免副作用和繁重的计算。【参考方案2】:

这似乎是一种预期的行为。

来自React docs:

退出状态更新

如果您将 State Hook 更新为相同的 值作为当前状态,React 将退出而不渲染 儿童或射击效果。 (React 使用 Object.is comparison algorithm。)

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

所以,React 确实在第一个演示的第 4 步和第二个演示的第 3 步重新渲染组件。因此,它执行函数内的所有代码,并为组件的每个子组件调用React.createElement()

但是它不会渲染组件的任何后代,也不会触发效果。

这仅适用于函数组件。对于纯类组件,如果状态没有改变,render 方法永远不会被调用。

我们无法避免重新运行。用memo() 记住函数也无济于事,因为it only checks for props changes,而不是状态。所以我们只需要考虑这种情况。

这并不能回答 React 为何以及何时运行该函数但退出,以及何时它根本不运行该函数的问题。如果您知道原因,请添加您的答案。

【讨论】:

@YevgenGorbunkov ,这正是正在发生的事情。我在问题中添加了第三个示例来证明这一点。点击只是调用setState(true) 并且呈现消息do 显示为较早,即使状态已经为真。

以上是关于React 组件有时会在状态不变的情况下渲染两次的主要内容,如果未能解决你的问题,请参考以下文章

在 React 中没有本地状态的情况下使用新数据重新渲染组件

React性能优化之减少组件渲染次数

React setState 在调用时渲染组件两次

为啥渲染方法在类组件中运行两次而没有包含任何状态?

React hooks 功能组件防止在状态更新时重新渲染

React-Router路由跳转时render触发两次的情况。