我正在使用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”,状态没有变化,因此组件不会重新渲染。 - ✅ 点击“切换”。
yes
被设置为“no”。状态已更改,因此组件会重新渲染。 - ✅ 点击“设置为是”。
yes
被设置为“yes”。状态再次更改,因此组件会重新渲染。 - ⛔ 再次点击“设置为是”。状态未更改,但组件仍然重新渲染。
- ✅ 随后点击“设置为是”将不会导致重新渲染,符合预期。
预期发生什么
在第4步中,由于状态未更改,因此不应重新渲染组件。
更新
正如React文档所述,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步中,由于状态未更改,组件不应该被重新呈现。
更新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的一个bug?
setState
方法时避免不必要的重新渲染只是一种性能优化,而不是语义上的保证……我没有看过源代码,但我的第一个猜测是,如果从嵌套在useEffect
内的函数的调用堆栈中调用了setState
,它将被选择退出(但也可能是完全不同的原因)。 - Aprillion