Vuex场景下Array.push()报状态变异错误但concat()不报错原因
问题场景
我在实现新数组合并到已有数组的逻辑时遇到了Vuex状态修改报错,初始业务代码如下:
props: { ItemData: // 该属性由父组件传入,数据源来自Vuex store { type: Array, default: null } } setup(props) { const Item = ref(props.ItemData); onMounted( ()=> { Form.Title = Item[0].Title Form.Files = Item[0].Files }) }
上述代码执行后生成的响应式对象Form结构如下:
Form = reactive({ Title: null, Files: [ {name: "photo", extension: ".webp", size: '1024'}, {name: "doc", extension: ".pdf", size: '2048'} ] })
Form.Files的数据来源于父组件传入的prop,源头是Vuex store中存储的状态;如果表单加载时没有已有文件,Form.Files也可能为空数组[]。
用户上传新文件时,新文件对应的NewFiles数据结构如下:
NewFiles = [ [ File, {name: "newphoto", extension: ".jpg", size: "1024" } ], [ File, {name: "newdoc", extension: ".docx", size: "2048" } ] ]
其中File是<input multiple type="file" />控件选中的原生文件对象。
问题复现
我最初尝试通过如下代码将NewFiles合并到Form.Files中:
Form.Files.push(...NewFiles);
该写法在Vue调试工具中可观察到NewFiles确实被添加到Form.Files下,但控制台抛出错误[vuex] do not mutate vuex store state outside mutation handlers.,后续代码执行中断。
之后我改用concat()方法实现合并,代码如下:
const AllFiles = Form.Files.concat(NewFiles); Form.BroadcastFiles = AllFiles;
该写法执行时无任何报错,但存在两点疑问:
- 我并未直接操作Vuex状态,为何
push()会触发Vuex报错? - 若确实涉及Vuex状态操作,为何
concat()不会触发相同的报错?
原因解答
push()触发报错的核心原因是引用同源:你在onMounted中直接执行Form.Files = Item[0].Files,这行代码没有做任何拷贝,是直接把Vuex中存储的Files数组的内存引用赋值给了Form.Files。也就是说此时Form.Files和Vuex store里的对应数组指向内存里的同一个对象。而push是数组的变异方法,会直接修改调用它的原数组,你调用Form.Files.push()本质上就是直接修改了Vuex存储的原状态,自然会触发Vuex严格模式的拦截——Vuex严格模式下只要检测到store状态在mutation函数之外被修改,不管你是直接操作store实例,还是拿到状态引用间接修改,都会抛出这个错误。concat()不报错的核心原因是方法不修改原数组:concat属于数组的非变异方法,它不会改动调用它的原有数组,而是会把合并后的结果作为一个全新的数组返回。你执行Form.Files.concat(NewFiles)的过程中,指向Vuex状态的原数组从头到尾没有被改动,生成的AllFiles是完全独立于Vuex状态的新数组,你把这个新数组赋值给Form.BroadcastFiles的操作完全不涉及Vuex状态的修改,自然不会触发报错。
实操提示:你当前的赋值写法本身就存在隐患,直接把prop中的引用类型值赋值给本地变量,后续操作稍不注意就会误改数据源。如果需要在组件本地操作这个文件列表,赋值时先做一次浅拷贝切断引用关联即可:
Form.Files = [...Item[0].Files],如果后续需要修改数组内的对象属性,就做一次深拷贝。另外你写的const Item = ref(props.ItemData)不会同步props的后续更新,如果需要保持响应式关联,建议使用toRef或者computed来声明。
内容的提问来源于stack exchange,提问作者volume one

