这是项目之前遇到的一个bug,最终发现是由于 reset Vuex state
不正确,污染了 initState
导致的,隐藏得还挺深的,在这里记录一下。
(PS:想直接看代码实现的同学可以从第三节,正确地 reset module state
的姿势 开始看)
背景
项目是用 Vue + Nuxt
写的一个H5网页。
下图是分类页的界面,左边的导航给出的分类项可以叠加选择。
在选择了任意分类项后点击 重置
可以把所有选中的项或输入的值恢复到未设置状态。
其中 价格范围
是输入最小值和最大值。
一个bug
某天产品经理跟我反馈了一个bug……
简单来说,就是如果用户设置过价格范围,然后点了重置,下次再次设置价格然后点击重置的时候,会无法重置价格……
为了更好地说明问题,我写了个简单的demo页面,给大家演示一下。
错误的示范
下面我们来看看这个错误实现的代码是怎样的。
基于上面的界面,而且 vuex
也分了模块,所以这个分类页的 store
是这样的:
category.js
const initState = {
selectedIds: {
gender: null,
category: [],
discount: [],
priceRange: {
start: null,
end: null
},
source: [],
}
}
export const state = () => {
return Object.assign({}, initState)
}
export const mutations = {
SET_FILTER_IDS_STATE(state, data) {
Object.keys(data).forEach(key => {
console.log('key', key)
if (key === 'priceRange') {
state.selectedIds.priceRange.start = data[key].start
state.selectedIds.priceRange.end = data[key].end
} else {
state.selectedIds[key] = data[key]
}
})
},
RESET_ALL_FILTERS(state) {
Object.keys(initState).forEach(key => {
Object.assign(state[key], initState[key])
})
},
}
export const actions = {
async setFilters({ commit }, { selectedIds }) {
commit('SET_FILTER_IDS_STATE', selectedIds)
},
async resetAllFilters({ commit }) {
commit('RESET_ALL_FILTERS')
},
}
export const getters = {
selectedIds(state) {
return state.selectedIds
},
}
问题就出在上面的 RESET_ALL_FILTERS
方法。这是网上找到的比较多人建议的 reset state
的方法。
其实这种实现方式在大部分情况下还是work的,但是!!因为我们这个分类页的 state
是个层级比较深的对象,而里面 Object.assign(state[key], initState[key])
这一句,就是关键!
因为 Object.assign
方法,其实是浅拷贝,所以当重置 priceRange
的时候,由于 priceRange
是个对象,那生成的 target
【Object.assign(target, ...sources)
】其实只是把引用指向了 initState.priceRange
的引用,也就是说,经过第一次重置之后,initState
的 priceRange
和当前的 category state
的 priceRange
是指向了同一块内存的。
所以,当后面再次设置性别和价格然后点重置的时候,性别可以正常重置,但是价格已经无法重置了,因为 initState
已经被污染了!!
正确地 reset module state
的姿势
方法一
既然经过上面的解释,我们明白了是浅拷贝的锅,那很自然地就会想到用深拷贝的方式来解决这个问题。
下面直接上代码。
category.js
import cloneDeep from 'lodash.clonedeep'
export const state = () => {
return cloneDeep(initState)
}
export const mutations = {
RESET_ALL_FILTERS(state) {
Object.assign(state, cloneDeep(initState))
},
}
PS:这里就只放跟上文 错误示范 里对比有修改的部分啦
方法二
在整理这篇文章的时候我又google了一下 vuex reset store
,找到了个更优雅的实现方式。
如果我们把 initState
写成一个函数,比如 getDefaultState
,这个函数就只是返回 initState
的,然后每次重置的时候先调用这个 getDefaultState
再赋值,那就能保证 initState
一定是初始值啦,也就同样可以避免 initState
被污染的问题了。
还是上代码。
category.js
const getDefaultState = () => {
return {
selectedIds: {
gender: null,
category: [],
discount: [],
priceRange: {
start: null,
end: null
},
source: [],
}
}
}
export const state = getDefaultState
export const mutations = {
RESET_ALL_FILTERS(state) {
const initState = getDefaultState()
Object.keys(initState).forEach(key => {
state[key] = initState[key]
})
},
}
PS:这里只放跟上文 错误示范 里对比有修改的部分
总结
上面写了两种 reset state
的实现方式,我个人觉得第二种更优雅。
当然,其实还有一个问题,就是这个 category state
设计得过于复杂了,我们一般做项目的时候其实不建议嵌套太深,容易出问题。所以在一开始设计数据 model
的时候,还是要多加考虑呀。
参考
- Reset Vuex Module State Like a Pro
- Clear all stores or set them all to its initial state
附录
最后附上 demo
页面的代码,方便有需要的同学自取演示。
demo.vue
Vuex state
{{key}}:
{{selectedIds[key].start && selectedIds[key].end ? selectedIds[key].start + '-' + selectedIds[key].end : '未选择'}}
{{selectedIds[key] || "未选择"}}
{{selectedIds[key].join(',') || '未选择'}}