state重置 vuex_如何正确地 reset Vuex module state

这是项目之前遇到的一个bug,最终发现是由于 reset Vuex state 不正确,污染了 initState 导致的,隐藏得还挺深的,在这里记录一下。

(PS:想直接看代码实现的同学可以从第三节,正确地 reset module state 的姿势 开始看)

背景

项目是用 Vue + Nuxt 写的一个H5网页。

下图是分类页的界面,左边的导航给出的分类项可以叠加选择。

在选择了任意分类项后点击 重置 可以把所有选中的项或输入的值恢复到未设置状态。

eca899d7ad9c

1.jpg

其中 价格范围 是输入最小值和最大值。

eca899d7ad9c

2.jpg

一个bug

某天产品经理跟我反馈了一个bug……

简单来说,就是如果用户设置过价格范围,然后点了重置,下次再次设置价格然后点击重置的时候,会无法重置价格……

为了更好地说明问题,我写了个简单的demo页面,给大家演示一下。

eca899d7ad9c

demo.gif

错误的示范

下面我们来看看这个错误实现的代码是怎样的。

基于上面的界面,而且 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 的时候,还是要多加考虑呀。

参考

附录

最后附上 demo 页面的代码,方便有需要的同学自取演示。

demo.vue

性别:

男士

女士

价格范围:

¥至

重置

提交


Vuex state

{{key}}:

v-if="key === 'priceRange'"

>{{selectedIds[key].start && selectedIds[key].end ? selectedIds[key].start + '-' + selectedIds[key].end : '未选择'}}

{{selectedIds[key] || "未选择"}}

{{selectedIds[key].join(',') || '未选择'}}

import { mapGetters, mapActions } from "vuex";

export default {

name: "test",

layout: "single-page",

data() {

return {

minPrice: NaN,

maxPrice: NaN,

gender: null,

category: [],

discount: [],

source: []

};

},

computed: {

...mapGetters({

selectedIds: "categoryFilter/selectedIds"

})

},

async mounted() {

this.initPriceRange();

},

methods: {

...mapActions({

setFilters: "categoryFilter/setFilters",

resetFilters: "categoryFilter/resetAllFilters"

}),

async initFilters() {

try {

const res = await this.$axios.$get(

"api" + this.$api.filter.categoryList

);

if (res.status === 0) {

const { data } = res;

this.$store.commit("category/FETCH_FILTERS", {

data

});

}

} catch (e) {

console.error(e);

}

},

initPriceRange() {

this.minPrice = this.selectedIds.priceRange.start || NaN;

this.maxPrice = this.selectedIds.priceRange.end || NaN;

},

onSubmit() {

let selectedIds = {

gender: this.gender,

category: this.category,

discount: this.discount,

source: this.source,

priceRange: {

start: Number(this.minPrice),

end: Number(this.maxPrice)

}

};

this.setFilters({

selectedIds

});

},

onReset() {

this.minPrice = NaN;

this.maxPrice = NaN;

this.gender = null;

this.category = [];

this.discount = [];

this.source = [];

this.resetFilters();

}

}

};

.page {

padding: 20px;

}

.input-group {

color: #333;

display: flex;

justify-content: flex-start;

padding: 10px 0;

align-items: center;

}

.input {

width: 80px;

border: solid 0.5px #aaa;

border-radius: 5px;

padding: 0 10px;

line-height: 30px;

margin-right: 10px;

}

.input-label {

margin-right: 5px;

font-weight: bold;

}

input[type="radio"] {

margin-right: 5px;

}

.radio-group {

display: flex;

align-items: center;

margin-right: 20px;

}

.btn-group {

margin-top: 20px;

text-align: right;

display: flex;

justify-content: space-between;

}

.btn {

width: 60px;

height: 35px;

border-radius: 5px;

width: 46%;

}

.btn-submit {

background-color: #333;

border: none;

color: #fff;

}

.btn-reset {

border: #333 solid 0.5px;

background: #fff;

color: #333;

}

hr {

border: solid 0.5px #aaa;

margin: 10px 0;

}

section {

padding-bottom: 20px;

}

.vuex-display-title {

font-size: 20px;

padding: 10px 0;

}

.vuex-display {

color: #333;

}

.state-item {

line-height: 1.5;

padding-bottom: 10px;

}

.state-key {

font-weight: bold;

}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值