如何正确地 reset Vuex module state

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

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

背景

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

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

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

1.jpg

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

2.jpg

一个bug

某天产品经理跟我反馈了一个bug……
简单来说,就是如果用户设置过价格范围,然后点了重置,下次再次设置价格然后点击重置的时候,会无法重置价格……

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

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 是个对象,那生成的 targetObject.assign(target, ...sources)】其实只是把引用指向了 initState.priceRange 的引用,也就是说,经过第一次重置之后,initStatepriceRange 和当前的 category statepriceRange 是指向了同一块内存的。
所以,当后面再次设置性别和价格然后点重置的时候,性别可以正常重置,但是价格已经无法重置了,因为 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

<template>
  <div class="page">
    <section>
      <form>
        <div class="input-group">
          <label class="input-label">性别:</label>
          <div class="radio-group">
            <input id="man" type="radio" value="man" name="gender" v-model="gender" />
            <label for="man">男士</label>
          </div>
          <div class="radio-group">
            <input id="woman" type="radio" value="woman" name="gender" v-model="gender" />
            <label for="woman">女士</label>
          </div>
        </div>

        <div class="input-group">
          <label class="input-label">价格范围:</label>
          ¥<input class="input" type="number" v-model="minPrice" />至
          ¥<input class="input" type="number" v-model="maxPrice" />
        </div>

        <div class="btn-group">
          <button class="btn btn-reset" @click.prevent="onReset">重置</button>
          <button class="btn btn-submit" @click.prevent="onSubmit">提交</button>
        </div>
      </form>
    </section>

    <hr />

    <section class="vuex-display">
      <h3 class="vuex-display-title">Vuex state</h3>
      <div class="state-item" v-for="key in Object.keys(selectedIds)" :key="key">
        <span class="state-key">{{key}}:</span>
        <p
          v-if="key === 'priceRange'"
        >{{selectedIds[key].start && selectedIds[key].end ? selectedIds[key].start + '-' + selectedIds[key].end : '未选择'}}</p>
        <p v-else-if="key === 'gender'">{{selectedIds[key] || "未选择"}}</p>
        <p v-else>{{selectedIds[key].join(',') || '未选择'}}</p>
      </div>
    </section>
  </div>
</template>

<script>
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();
    }
  }
};
</script>

<style lang="scss" scoped>
.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;
}
</style>
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容