NGRx技术问题:使用扩展运算符时默认值为何被覆盖?
解决NGRx选择器中默认ViewModel被扩展运算符覆盖的问题
这个问题其实是JavaScript引用类型特性导致的典型坑,我来帮你拆解原因和解决办法:
为什么默认值会被覆盖?
首先,你的defaultManagersVM是一个包含引用类型属性的对象:byId是对象,allIds是数组,这两者在JavaScript中都是引用类型,而非值类型。
当你在NGRx选择器中做以下操作时,就会意外修改到全局的defaultManagersVM:
- 直接返回默认值的引用:比如空状态时直接
return defaultManagersVM,后续如果组件或其他逻辑修改了这个返回的VM(比如vm.allIds.push('new-id')),就会直接修改原默认对象的属性,因为它们指向同一个内存地址。 - 浅拷贝未覆盖内部引用类型:即使你用了扩展运算符
...defaultManagersVM,这只是浅拷贝——外层对象是新的,但内部的byId和allIds仍然引用着defaultManagersVM里的原对象/数组。如果后续逻辑修改了这些内部属性,同样会污染全局默认值。
举个错误示例:
// 错误写法:直接返回默认值引用 + 浅拷贝内部属性 const selectManagersVM = createSelector( selectManagers, (managers) => { if (!managers.length) return defaultManagersVM; // 直接返回引用 return { ...defaultManagersVM, byId: { ...defaultManagersVM.byId, ...managers.reduce((acc, m) => ({...acc, [m.id]: m}), {}) }, allIds: [...defaultManagersVM.allIds, ...managers.map(m => m.id)] }; } );
如果此时有代码修改了选择器返回的allIds数组,defaultManagersVM.allIds也会跟着变。
解决办法
1. 始终返回新的ViewModel实例
不管是空状态还是有数据的状态,都不要复用defaultManagersVM的引用,而是每次创建新的对象:
const selectManagersVM = createSelector( selectManagers, (managers) => { if (managers.length === 0) { // 返回全新的空VM,而非引用默认值 return { byId: {}, allIds: [] }; } // 有数据时直接生成新的VM return { byId: managers.reduce((acc, manager) => ({ ...acc, [manager.id]: manager }), {}), allIds: managers.map(m => m.id) }; } );
如果想复用默认值的结构,可以封装成一个工厂函数,每次调用都返回新对象:
// 工厂函数:每次调用都生成新的默认VM const createDefaultManagersVM = (): ManagersVM => ({ byId: {}, allIds: [] }); // 在选择器中使用 const selectManagersVM = createSelector( selectManagers, (managers) => { if (!managers.length) return createDefaultManagersVM(); // ...其他逻辑 } );
2. 完整覆盖引用类型属性(如果必须基于默认值扩展)
如果你一定要基于defaultManagersVM来扩展ViewModel,要确保内部的引用类型属性也被重新创建,彻底切断和原默认值的引用:
const selectManagersVM = createSelector( selectManagers, (managers) => { if (!managers.length) { // 即使是空状态,也创建新的内部属性 return { ...defaultManagersVM, byId: {}, allIds: [] }; } return { ...defaultManagersVM, // 重新创建byId对象,而非复用默认值的引用 byId: managers.reduce((acc, manager) => ({ ...acc, [manager.id]: manager }), {}), // 重新创建allIds数组 allIds: managers.map(m => m.id) }; } );
3. 遵循NGRx不可变操作原则
记住,NGRx的状态和衍生的ViewModel都应该是不可变的。永远不要直接修改从选择器返回的VM对象/数组,而是通过触发Action更新状态,再由选择器生成新的VM。这样从根源上避免意外修改默认值的问题。
内容的提问来源于stack exchange,提问作者MarkD
相关产品推荐
相关产品推荐

