5

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

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

请按照片段下方的步骤重现问题。

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

编辑 2 :在“更新 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>

怎么了

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

预计会发生什么

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

更新

正如React 文档所述,useEffect

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

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

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

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

请按照片段下方的步骤重现问题。

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>

怎么了

  1. ✅ 组件首先呈现在屏幕上,消息记录在控制台中。
  2. ✅ 点击“切换”。online设置为false。状态已经改变,所以组件被重新渲染。
  3. ⛔ 打开开发工具并在网络面板中切换到“离线”。onlinewas already 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>

怎么了

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

问题

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

4

2 回答 2

11

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

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

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

于 2020-03-29T19:25:12.557 回答
3

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

来自React 文档

摆脱状态更新

如果您将 State Hook 更新为与当前状态相同的值,React 将退出而不渲染子级或触发效果。(React 使用Object.is比较算法。)

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

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

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

这仅适用于功能组件。对于纯类组件,render如果状态未更改,则永远不会调用该方法。

我们无法避免重新运行。记住这个函数memo()也无济于事,因为它只检查 props的变化,而不是状态。所以我们只需要考虑这种情况。

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

于 2020-03-28T13:40:37.050 回答