如何在 EventEmitter 的 on('change') 函数中使用钩子设置状态?
How to setState with hooks in EventEmitter's on('change') function?
出于学习的目的,我试图通过从头开始实施来分解通量模式的基础知识。我知道我可以使用 Class 组件 解决下面的问题,但为什么它与 class 组件的 setState({ ... })
一起使用而不是钩子的setValues
函数?
如您所料,问题是 setValues
函数似乎没有触发新状态的重新渲染。我觉得这与闭包有关,但我不太了解它们,所以我将不胜感激任何指导和帮助。
商店
// store.js
import EventEmitter from "events";
class StoreClass extends EventEmitter {
constructor() {
super();
this.values = []
}
addNow() {
this.values.push(Date.now());
this.emit('change')
}
getAll() {
return this.values;
}
}
const store = new StoreClass();
export default store;
组件
// App.js
import store from './store';
function App() {
const [values, setValues] = useState([]);
const handleClick = () => {
store.addNow();
};
useEffect(() => {
const onChangeHandler = () => {
console.log("Change called");
setValues(() => store.getAll())
};
store.on('change', onChangeHandler);
return () => store.removeAllListeners('change')
}, [values]);
return (
<div>
<button onClick={handleClick}>Click to add</button>
<ul>
{values.map(val => <li key={val}>{val}</li>)}
</ul>
</div>
);
}
useEffct 监听值的变化,但是 setValues 只存在于 useEffect 中,我认为你根本不需要 useEffect
你可以试试这样:
// App.js
import store from './store';
function App() {
const [values, setValues] = useState([]);
const onChangeHandler = () => {
console.log("Change called");
const storeValues = store.getAll();
setValues(storeValues);
};
const handleClick = () => {
store.addNow();
onChangeHandler();
};
return (
<div>
<button onClick={handleClick}>Click to add</button>
<ul>
{values.map(val => <li key={val}>{val}</li>)}
</ul>
</div>
);
}
再修改几个小时并再次阅读 React 文档后,我终于可以回答我自己的问题了。简而言之,问题是 store 中的 state 没有被一成不变地更新。
Redux 是 Flux 架构的一个实现,在他们的文档中说的很清楚:
Redux expects that all state updates are done immutably
因此,上面代码中的罪魁祸首就在store的addNow
函数中。 Array.push()
方法通过附加一项来改变现有数组。这样做的不可变方法是使用 Array.concat()
方法或 ES6 扩展语法(解构)[...this.values]
,然后将新数组分配给 this.values
.
addNow() {
this.values.push(Date.now()); // Problem - Mutable Change
this.values = this.values.concat(Date.now()) // Option 1 - Immutable Change
this.values = [...this.values, Date.now()] // Option 2 - Immutable Change
this.emit('change')
}
要查看反应组件中每个更改的效果,请将 useEffect
挂钩中的 setValues
函数更改为以下内容:
setValues((prev) => {
const newValue = store.getAll()
console.log("Previous:", prev)
console.log("New value:", newValue)
return newValue
})
在控制台中可以清楚地看到,当商店中的状态发生可变变化时,在单击第一个按钮后,之前的状态和新状态将始终相同并引用相同的数组。但是,当存储中的状态发生不可变变化时,先前状态和新状态不会引用同一个数组。此外,在控制台中可以清楚地看到,之前的状态数组比新状态数组的值少了一个。
为什么在使用 class 组件时状态更改会触发重新渲染,而在使用挂钩时不会?
根据 class component's setState function 的反应文档。
setState() will always lead to a re-render unless shouldComponentUpdate() returns false.
这意味着无论 store 中的状态是可变的还是不可变的,当触发 change 事件并调用 setState
函数时,react 将重新渲染组件并显示“新”状态。然而,useState
钩子不是这种情况。
根据 useState hook 的反应文档:
If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
这意味着当通过改变原始对象对存储中的数组进行更改时,反应会在决定再次渲染之前检查新状态和之前的状态是否相同。由于之前和当前状态的引用是相同的,因此 React 什么都不做。要解决此问题,商店中的更改必须不可变地完成。
出于学习的目的,我试图通过从头开始实施来分解通量模式的基础知识。我知道我可以使用 Class 组件 解决下面的问题,但为什么它与 class 组件的 setState({ ... })
一起使用而不是钩子的setValues
函数?
如您所料,问题是 setValues
函数似乎没有触发新状态的重新渲染。我觉得这与闭包有关,但我不太了解它们,所以我将不胜感激任何指导和帮助。
商店
// store.js
import EventEmitter from "events";
class StoreClass extends EventEmitter {
constructor() {
super();
this.values = []
}
addNow() {
this.values.push(Date.now());
this.emit('change')
}
getAll() {
return this.values;
}
}
const store = new StoreClass();
export default store;
组件
// App.js
import store from './store';
function App() {
const [values, setValues] = useState([]);
const handleClick = () => {
store.addNow();
};
useEffect(() => {
const onChangeHandler = () => {
console.log("Change called");
setValues(() => store.getAll())
};
store.on('change', onChangeHandler);
return () => store.removeAllListeners('change')
}, [values]);
return (
<div>
<button onClick={handleClick}>Click to add</button>
<ul>
{values.map(val => <li key={val}>{val}</li>)}
</ul>
</div>
);
}
useEffct 监听值的变化,但是 setValues 只存在于 useEffect 中,我认为你根本不需要 useEffect
你可以试试这样:
// App.js
import store from './store';
function App() {
const [values, setValues] = useState([]);
const onChangeHandler = () => {
console.log("Change called");
const storeValues = store.getAll();
setValues(storeValues);
};
const handleClick = () => {
store.addNow();
onChangeHandler();
};
return (
<div>
<button onClick={handleClick}>Click to add</button>
<ul>
{values.map(val => <li key={val}>{val}</li>)}
</ul>
</div>
);
}
再修改几个小时并再次阅读 React 文档后,我终于可以回答我自己的问题了。简而言之,问题是 store 中的 state 没有被一成不变地更新。
Redux 是 Flux 架构的一个实现,在他们的文档中说的很清楚:
Redux expects that all state updates are done immutably
因此,上面代码中的罪魁祸首就在store的addNow
函数中。 Array.push()
方法通过附加一项来改变现有数组。这样做的不可变方法是使用 Array.concat()
方法或 ES6 扩展语法(解构)[...this.values]
,然后将新数组分配给 this.values
.
addNow() {
this.values.push(Date.now()); // Problem - Mutable Change
this.values = this.values.concat(Date.now()) // Option 1 - Immutable Change
this.values = [...this.values, Date.now()] // Option 2 - Immutable Change
this.emit('change')
}
要查看反应组件中每个更改的效果,请将 useEffect
挂钩中的 setValues
函数更改为以下内容:
setValues((prev) => {
const newValue = store.getAll()
console.log("Previous:", prev)
console.log("New value:", newValue)
return newValue
})
在控制台中可以清楚地看到,当商店中的状态发生可变变化时,在单击第一个按钮后,之前的状态和新状态将始终相同并引用相同的数组。但是,当存储中的状态发生不可变变化时,先前状态和新状态不会引用同一个数组。此外,在控制台中可以清楚地看到,之前的状态数组比新状态数组的值少了一个。
为什么在使用 class 组件时状态更改会触发重新渲染,而在使用挂钩时不会?
根据 class component's setState function 的反应文档。
setState() will always lead to a re-render unless shouldComponentUpdate() returns false.
这意味着无论 store 中的状态是可变的还是不可变的,当触发 change 事件并调用 setState
函数时,react 将重新渲染组件并显示“新”状态。然而,useState
钩子不是这种情况。
根据 useState hook 的反应文档:
If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)
这意味着当通过改变原始对象对存储中的数组进行更改时,反应会在决定再次渲染之前检查新状态和之前的状态是否相同。由于之前和当前状态的引用是相同的,因此 React 什么都不做。要解决此问题,商店中的更改必须不可变地完成。