我正在使用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>
yes
的值已经为“是”,因此状态未更改,因此该组件不会重新呈现。yes
设置为“ no”。状态已更改,因此重新渲染了该组件。yes
设置为“是”。状态再次更改,因此该组件被重新呈现。在第4步中,由于状态未更改,因此不应重新渲染组件。
作为React docs状态,useEffect
是
适合许多常见的副作用,例如设置 订阅和事件处理程序...
一个这样的用例就是监听诸如online
和offline
之类的浏览器事件。
在此示例中,当组件首次呈现时,我们通过向其传递一个空数组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>
online
设置为false
。状态已更改,因此重新渲染了该组件。online
已经false
,因此状态未未更改,但是组件仍被重新渲染。在第3步中,由于状态不变,因此不应重新渲染组件。
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的错误?
答案 0 :(得分:5)
答案是React使用一组启发式方法来确定它是否可以避免再次调用呈现函数。这些启发式方法可能会在版本之间发生变化,并且不能保证在状态相同时总是能够纾困。 React提供的唯一保证是如果状态相同,它不会重新渲染子组件。
您的渲染函数应该是纯函数。因此,它们运行多少次无关紧要。如果您要在渲染函数中计算一些昂贵的东西,并且担心不必要地调用它,则可以将该计算结果包装在useMemo
中。
通常来说,在React中“计数渲染”是没有用的。当确切地调用React时,您的功能取决于React本身,确切的时间将在版本之间不断变化。这不是合同的一部分。
答案 1 :(得分:2)
这似乎是预期的行为。
来自React docs:
退出状态更新
如果将状态挂钩更新为相同 值作为当前状态,React将纾困而不渲染 儿童或射击效果。 (反应使用
Object.is
comparison algorithm。)请注意,React可能仍需要再次渲染该特定组件 救助之前。不用担心,因为React不会 不必要地“深入”到树上。如果您做的很昂贵 渲染时进行计算,您可以使用
useMemo
对其进行优化。
因此,React 会在第一个演示的第4步和第二个演示的第3步重新渲染组件。因此,它执行函数内部的所有代码,并为组件的每个子组件调用React.createElement()
。
但是它不会渲染组件的任何后代,也不会触发效果。
仅对功能组件有效。对于纯类组件,如果状态未更改,则永远不会调用render
方法。
我们无所不能避免重新运行。用memo()
记忆功能也无济于事,因为it only checks for props changes而不是状态。所以我们只需要考虑这种情况。
这没有回答问题,为什么和什么时候React运行该函数但是失败了,什么时候根本不运行该函数。如果您知道原因,请添加您的答案。