27 Vuex相关

vuex是什么?怎么使用?哪种功能场景使用它?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。vuex 就是一个仓库,仓库里放了很多对象。其中 state 就是数据源存放地,对应于一般 vue 对象里面的 data 里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新它通过 mapState 把全局的 stategetters 映射到当前组件的 computed 计算属性

  • vuex 一般用于中大型 web 单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用 vuex 的必要性不是很大,因为完全可以用组件 prop 属性或者事件来完成父子组件之间的通信,vuex 更多地用于解决跨组件通信以及作为数据中心集中式存储数据。
  • 使用Vuex解决非父子组件之间通信问题 vuex 是通过将 state 作为数据中心、各个组件共享 state 实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于 State 中能有效解决多层级组件嵌套的跨组件通信问题

vuexState 在单页应用的开发中本身具有一个“数据库”的作用,可以将组件中用到的数据存储在 State 中,并在 Action 中封装数据读写的逻辑。这时候存在一个问题,一般什么样的数据会放在 State 中呢? 目前主要有两种数据会使用 vuex 进行管理:

  • 组件之间全局共享的数据
  • 通过后端异步请求的数据

包括以下几个模块

  • stateVuex 使用单一状态树,即每个应用将仅仅包含一个store 实例。里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新。它通过 mapState 把全局的 stategetters 映射到当前组件的 computed 计算属性
  • mutations:更改Vuexstore中的状态的唯一方法是提交mutation
  • gettersgetter 可以对 state 进行计算操作,它就是 store 的计算属性虽然在组件内也可以做计算属性,但是 getters 可以在多给件之间复用如果一个状态只在一个组件内使用,是可以不用 getters
  • actionaction 类似于 muation, 不同在于:action 提交的是 mutation,而不是直接变更状态action 可以包含任意异步操作
  • modules:面对复杂的应用程序,当管理的状态比较多时;我们需要将vuexstore对象分割成模块(modules)

modules:项目特别复杂的时候,可以让每一个模块拥有自己的statemutationactiongetters,使得结构非常清晰,方便管理

回答范例

思路

  • 给定义
  • 必要性阐述
  • 何时使用
  • 拓展:一些个人思考、实践经验等

回答范例

  1. Vuex 是一个专为 Vue.js 应用开发的状态管理模式 + 库 。它采用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
  2. 我们期待以一种简单的“单向数据流”的方式管理应用,即状态 - > 视图 -> 操作单向循环的方式。但当我们的应用遇到多个组件共享状态时,比如:多个视图依赖于同一状态或者来自不同视图的行为需要变更同一状态。此时单向数据流的简洁性很容易被破坏。因此,我们有必要把组件的共享状态抽取出来,以一个全局单例模式管理。通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。这是vuex存在的必要性,它和react生态中的redux之类是一个概念
  3. Vuex 解决状态管理的同时引入了不少概念:例如statemutationaction等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用 Vuex 反而是繁琐冗余的,一个简单的 store 模式就足够了。但是,如果要构建一个中大型单页应用,Vuex 基本是标配。
  4. 我在使用vuex过程中感受到一些等

可能的追问

  1. vuex有什么缺点吗?你在开发过程中有遇到什么问题吗?
  • 刷新浏览器,vuex中的state会重新变为初始状态。解决方案-插件 vuex-persistedstate
  1. actionmutation的区别是什么?为什么要区分它们?
  • action中处理异步,mutation不可以
  • mutation做原子操作
  • action可以整合多个mutation的集合
  • mutation 是同步更新数据(内部会进行是否为异步方式更新数据的检测) $watch 严格模式下会报错
  • action 异步操作,可以获取数据后调佣 mutation 提交最终数据
  • 流程顺序:“相应视图—>修改State”拆分成两部分,视图触发ActionAction再触发Mutation`。
  • 基于流程顺序,二者扮演不同的角色:Mutation:专注于修改State,理论上是修改State的唯一途径。Action:业务代码、异步请求
  • 角色不同,二者有不同的限制:Mutation:必须同步执行。Action:可以异步,但不能直接操作State

Vuex中actions和mutations有什么区别

题目分析

  • mutationsactionsvuex带来的两个独特的概念。新手程序员容易混淆,所以面试官喜欢问。
  • 我们只需记住修改状态只能是mutationsactions只能通过提交mutation修改状态即可

回答范例

  1. 更改 Vuexstore 中的状态的唯一方法是提交 mutationmutation 非常类似于事件:每个 mutation 都有一个字符串的类型 (type)和一个 回调函数 (handler) 。Action 类似于 mutation,不同在于:Action可以包含任意异步操作,但它不能修改状态, 需要提交mutation才能变更状态
  2. 开发时,包含异步操作或者复杂业务组合时使用action;需要直接修改状态则提交mutation。但由于dispatchcommit是两个API,容易引起混淆,实践中也会采用统一使用dispatch action的方式。调用dispatchcommit两个API时几乎完全一样,但是定义两者时却不甚相同,mutation的回调函数接收参数是state对象。action则是与Store实例具有相同方法和属性的上下文context对象,因此一般会解构它为{commit, dispatch, state},从而方便编码。另外dispatch会返回Promise实例便于处理内部异步结果
  3. 实现上commit(type)方法相当于调用options.mutations[type](state)dispatch(type)方法相当于调用options.actions[type](store),这样就很容易理解两者使用上的不同了

实现

我们可以像下面这样简单实现commitdispatch,从而辨别两者不同

    class Store {
        constructor(options) {
            this.state = reactive(options.state)
            this.options = options
        }
        commit(type, payload) {
            // 传入上下文和参数1都是state对象
            this.options.mutations[type].call(this.state, this.state, payload)
        }
        dispatch(type, payload) {
            // 传入上下文和参数1都是store本身
            this.options.actions[type].call(this, this, payload)
        }
    }

怎么监听vuex数据的变化

分析

  • vuex数据状态是响应式的,所以状态变视图跟着变,但是有时还是需要知道数据状态变了从而做一些事情。
  • 既然状态都是响应式的,那自然可以watch,另外vuex也提供了订阅的API:store.subscribe()

回答范例

  1. 我知道几种方法:
  • 可以通过watch选项或者watch方法监听状态
  • 可以使用vuex提供的API:store.subscribe()
  1. watch选项方式,可以以字符串形式监听$store.state.xxsubscribe方式,可以调用store.subscribe(cb),回调函数接收mutation对象和state对象,这样可以进一步判断mutation.type是否是期待的那个,从而进一步做后续处理。
  2. watch方式简单好用,且能获取变化前后值,首选;subscribe方法会被所有commit行为触发,因此还需要判断mutation.type,用起来略繁琐,一般用于vuex插件中

实践

watch方式

    const app = createApp({
        watch: {
          '$store.state.counter'() {
            console.log('counter change!');
          }
        }
    })

subscribe方式:

     store.subscribe((mutation, state) => {
        if (mutation.type === 'add') {
          console.log('counter change in subscribe()!');
        }
    })

Vuex 页面刷新数据丢失怎么解决

体验

可以从localStorage中获取作为状态初始值:

    const store = createStore({
      state () {
        return {
          count: localStorage.getItem('count')
        }
      }
    })

业务代码中,提交修改状态同时保存最新值:虽说实现了,但是每次还要手动刷新localStorage不太优雅

    store.commit('increment')
    localStorage.setItem('count', store.state.count)

回答范例

  1. vuex只是在内存保存状态,刷新之后就会丢失,如果要持久化就要存起来
  2. localStorage就很合适,提交mutation的时候同时存入localStoragestore中把值取出作为state的初始值即可。
  3. 这里有两个问题,不是所有状态都需要持久化;如果需要保存的状态很多,编写的代码就不够优雅,每个提交的地方都要单独做保存处理。这里就可以利用vuex提供的subscribe方法做一个统一的处理。甚至可以封装一个vuex插件以便复用。
  4. 类似的插件有vuex-persistvuex-persistedstate,内部的实现就是通过订阅mutation变化做统一处理,通过插件的选项控制哪些需要持久化

原理

可以看一下[vuex-persist (opens new window)](https://github.com/championswimmer/vuex- persist/blob/master/src/index.ts#L277)内部确实是利用subscribe实现的

Vuex 为什么要分模块并且加命名空间

  • 模块 : 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块
  • 命名空间 :默认情况下,模块内部的 actionmutationgetter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutationaction 作出响应。如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getteractionmutation 都会自动根据模块注册的路径调整命名

你有使用过vuex的module吗?

    const moduleA = {
      state: () => ({ ... }),
      mutations: { ... },
      actions: { ... },
      getters: { ... }
    }
    const moduleB = {
      state: () => ({ ... }),
      mutations: { ... },
      actions: { ... }
    }
    const store = createStore({
      modules: {
        a: moduleA,
        b: moduleB
      }
    })
    store.state.a // -> moduleA 的状态
    store.state.b // -> moduleB 的状态
    store.getters.c // -> moduleA里的getters
    store.commit('d') // -> 能同时触发子模块中同名mutation
    store.dispatch('e') // -> 能同时触发子模块中同名action
  • 用过module,项目规模变大之后,单独一个store对象会过于庞大臃肿,通过模块方式可以拆分开来便于维护
  • 可以按之前规则单独编写子模块代码,然后在主文件中通过modules选项组织起来:reateStore({modules:{...}})
  • 不过使用时要注意访问子模块状态时需要加上注册时模块名:store.state.a.xxx,但同时gettersmutationsactions又在全局空间中,使用方式和之前一样。如果要做到完全拆分,需要在子块加上namespace选项,此时再访问它们就要加上命名空间前缀。
  • 很显然,模块的方式可以拆分代码,但是缺点也很明显,就是使用起来比较繁琐复杂,容易出错。而且类型系统支持很差,不能给我们带来帮助。pinia显然在这方面有了很大改进,是时候切换过去了

你觉得vuex有什么缺点

分析

相较于reduxvuex已经相当简便好用了。但模块的使用比较繁琐,对ts支持也不好。

体验

使用模块:用起来比较繁琐,使用模式也不统一,基本上得不到类型系统的任何支持

    const store = createStore({
      modules: {
        a: moduleA
      }
    })
    store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState使用
    store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的
    store.getters['a/c'] // -> 有namespaced时要加path,使用模式又和state不一样
    store.commit('d') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutation
    store.commit('a/d') // -> 有namespaced时要加path,配合mapMutations使用感觉也没简化

回答范例

  1. vuex利用响应式,使用起来已经相当方便快捷了。但是在使用过程中感觉模块化这一块做的过于复杂,用的时候容易出错,还要经常查看文档
  2. 比如:访问state时要带上模块key,内嵌模块的话会很长,不得不配合mapState使用,加不加namespaced区别也很大,gettersmutationsactions这些默认是全局,加上之后必须用字符串类型的path来匹配,使用模式不统一,容易出错;对ts的支持也不友好,在使用模块时没有代码提示。
  3. 之前Vue2项目中用过vuex-module-decorators的解决方案,虽然类型支持上有所改善,但又要学一套新东西,增加了学习成本。pinia出现之后使用体验好了很多,Vue3 + pinia会是更好的组合

原理

下面我们来看看vuexstore.state.x.y这种嵌套的路径是怎么搞出来的

首先是子模块安装过程:父模块状态parentState上面设置了子模块名称moduleName,值为当前模块state对象。放在上面的例子中相当于:store.state['x'] = moduleX.state。此过程是递归的,那么store.state.x.y安装时就是:store.state['x']['y'] = moduleY.state

    //源码位置 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115
    if (!isRoot && !hot) {
        // 获取父模块state
        const parentState = getNestedState(rootState, path.slice(0, -1))
        // 获取子模块名称
        const moduleName = path[path.length - 1]
        store._withCommit(() => {
            // 把子模块state设置到父模块上
            parentState[moduleName] = module.state
        })
    }

用过pinia吗?有什么优点?

1. pinia是什么?

Vue3中,可以使用传统的Vuex来实现状态管理,也可以使用最新的pinia来实现状态管理,我们来看看官网如何解释pinia的:PiniaVue 的存储库,它允许您跨组件/页面共享状态。 * 实际上,pinia就是Vuex的升级版,官网也说过,为了尊重原作者,所以取名pinia,而没有取名Vuex,所以大家可以直接将pinia比作为Vue3Vuex

2. 为什么要使用pinia?

  • Vue2Vue3都支持,这让我们同时使用Vue2Vue3的小伙伴都能很快上手。
  • pinia中只有stategetteraction,抛弃了Vuex中的MutationVuexmutation一直都不太受小伙伴们的待见,pinia直接抛弃它了,这无疑减少了我们工作量。
  • piniaaction支持同步和异步,Vuex不支持
  • 良好的Typescript支持,毕竟我们Vue3都推荐使用TS来编写,这个时候使用pinia就非常合适了
  • 无需再创建各个模块嵌套了,Vuex中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia中每个store都是独立的,互相不影响。
  • 体积非常小,只有1KB左右。
  • pinia支持插件来扩展自身功能。
  • 支持服务端渲染

3. pinna使用

pinna文档 (opens new window)

  1. 准备工作

我们这里搭建一个最新的Vue3 + TS + Vite项目

    npm create vite@latest my-vite-app --template vue-ts
  1. pinia基础使用
    yarn add pinia
    // main.ts
    import { createApp } from "vue";
    import App from "./App.vue";
    import { createPinia } from "pinia";
    const pinia = createPinia();
    
    const app = createApp(App);
    app.use(pinia);
    app.mount("#app");

2.1 创建store

    //sbinsrc/store/user.ts
    import { defineStore } from 'pinia'
    
    // 第一个参数是应用程序中 store 的唯一 id
    export const useUsersStore = defineStore('users', {
      // 其它配置项
    })

创建store很简单,调用pinia中的defineStore函数即可,该函数接收两个参数:

  • name:一个字符串,必传项,该store的唯一id
  • options:一个对象,store的配置项,比如配置store内的数据,修改数据的方法等等。

我们可以定义任意数量的store,因为我们其实一个store就是一个函数,这也是pinia的好处之一,让我们的代码扁平化了,这和Vue3的实现思想是一样的

2.2 使用store

    <!-- src/App.vue -->
    <script setup lang="ts">
    import { useUsersStore } from "../src/store/user";
    const store = useUsersStore();
    console.log(store);
    </script>

2.3 添加state

    export const useUsersStore = defineStore("users", {
      state: () => {
        return {
          name: "test",
          age: 20,
          sex: "男",
        };
      },
    });

2.4 读取state数据

    <template>
      <img alt="Vue logo" src="./assets/logo.png" />
      <p>姓名:{{ name }}</p>
      <p>年龄:{{ age }}</p>
      <p>性别:{{ sex }}</p>
    </template>
    <script setup lang="ts">
    import { ref } from "vue";
    import { useUsersStore } from "../src/store/user";
    const store = useUsersStore();
    const name = ref<string>(store.name);
    const age = ref<number>(store.age);
    const sex = ref<string>(store.sex);
    </script>

上段代码中我们直接通过store.age等方式获取到了store存储的值,但是大家有没有发现,这样比较繁琐,我们其实可以用解构的方式来获取值,使得代码更简洁一点

    import { useUsersStore, storeToRefs } from "../src/store/user";
    const store = useUsersStore();
    const { name, age, sex } = storeToRefs(store); // storeToRefs获取的值是响应式的

2.5 修改state数据

    <template>
      <img alt="Vue logo" src="./assets/logo.png" />
      <p>姓名:{{ name }}</p>
      <p>年龄:{{ age }}</p>
      <p>性别:{{ sex }}</p>
      <button @click="changeName">更改姓名</button>
    </template>
    <script setup lang="ts">
    import child from './child.vue';
    import { useUsersStore, storeToRefs } from "../src/store/user";
    const store = useUsersStore();
    const { name, age, sex } = storeToRefs(store);
    const changeName = () => {
      store.name = "张三";
      console.log(store);
    };
    </script>

2.6 重置state

  • 有时候我们修改了state数据,想要将它还原,这个时候该怎么做呢?就比如用户填写了一部分表单,突然想重置为最初始的状态。
  • 此时,我们直接调用store$reset()方法即可,继续使用我们的例子,添加一个重置按钮
    <button @click="reset">重置store</button>
    // 重置store
    const reset = () => {
      store.$reset();
    };

当我们点击重置按钮时,store中的数据会变为初始状态,页面也会更新

2.7 批量更改state数据

如果我们一次性需要修改很多条数据的话,有更加简便的方法,使用store$patch方法,修改app.vue代码,添加一个批量更改数据的方法

    <button @click="patchStore">批量修改数据</button>
    // 批量修改数据
    const patchStore = () => {
      store.$patch({
        name: "张三",
        age: 100,
        sex: "女",
      });
    };
  • 有经验的小伙伴可能发现了,我们采用这种批量更改的方式似乎代价有一点大,假如我们state中有些字段无需更改,但是按照上段代码的写法,我们必须要将state中的所有字段例举出了。
  • 为了解决该问题,pinia提供的$patch方法还可以接收一个回调函数,它的用法有点像我们的数组循环回调函数了。
    store.$patch((state) => {
      state.items.push({ name: 'shoes', quantity: 1 })
      state.hasChanged = true
    })

2.8 直接替换整个state

pinia提供了方法让我们直接替换整个state对象,使用store$state方法

    store.$state = { counter: 666, name: '张三' }

上段代码会将我们提前声明的state替换为新的对象,可能这种场景用得比较少

  1. getters属性
  • gettersdefineStore参数配置项里面的另一个属性
  • 可以把getter想象成Vue中的计算属性,它的作用就是返回一个新的结果,既然它和Vue中的计算属性类似,那么它肯定也是会被缓存的,就和computed一样

3.1 添加getter

    export const useUsersStore = defineStore("users", {
      state: () => {
        return {
          name: "test",
          age: 10,
          sex: "男",
        };
      },
      getters: {
        getAddAge: (state) => {
          return state.age + 100;
        },
      },
    })

上段代码中我们在配置项参数中添加了getter属性,该属性对象中定义了一个getAddAge方法,该方法会默认接收一个state参数,也就是state对象,然后该方法返回的是一个新的数据

3.2 使用getter

    <template>
      <p>新年龄:{{ store.getAddAge }}</p>
      <button @click="patchStore">批量修改数据</button>
    </template>
    <script setup lang="ts">
    import { useUsersStore } from "../src/store/user";
    const store = useUsersStore();
    // 批量修改数据
    const patchStore = () => {
      store.$patch({
        name: "张三",
        age: 100,
        sex: "女",
      });
    };
    </script>

上段代码中我们直接在标签上使用了store.gettAddAge方法,这样可以保证响应式,其实我们state中的name等属性也可以以此种方式直接在标签上使用,也可以保持响应式

3.3 getter中调用其它getter

    export const useUsersStore = defineStore("users", {
      state: () => {
        return {
          name: "test",
          age: 20,
          sex: "男",
        };
      },
      getters: {
        getAddAge: (state) => {
          return state.age + 100;
        },
        getNameAndAge(): string {
          return this.name + this.getAddAge; // 调用其它getter
        },
      },
    });

3.3 getter传参

    export const useUsersStore = defineStore("users", {
      state: () => {
        return {
          name: "test",
          age: 20,
          sex: "男",
        };
      },
      getters: {
        getAddAge: (state) => {
          return (num: number) => state.age + num;
        },
        getNameAndAge(): string {
          return this.name + this.getAddAge; // 调用其它getter
        },
      },
    });
    <p>新年龄:{{ store.getAddAge(1100) }}</p>
  1. actions属性
  • 前面我们提到的stategetters属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data数据和computed计算属性一样。
  • 那么,如果我们有业务代码的话,最好就是卸载actions属性里面,该属性就和我们组件代码中的methods相似,用来放置一些处理业务逻辑的方法。
  • actions属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法

4.1 添加actions

    export const useUsersStore = defineStore("users", {
      state: () => {
        return {
          name: "test",
          age: 20,
          sex: "男",
        };
      },
      getters: {
        getAddAge: (state) => {
          return (num: number) => state.age + num;
        },
        getNameAndAge(): string {
          return this.name + this.getAddAge; // 调用其它getter
        },
      },
      actions: {
        // 在实际场景中,该方法可以是任何逻辑,比如发送请求、存储token等等。大家把actions方法当作一个普通的方法即可,特殊之处在于该方法内部的this指向的是当前store
        saveName(name: string) {
          this.name = name;
        },
      },
    });

4.2 使用actions

使用actions中的方法也非常简单,比如我们在App.vue中想要调用该方法

    const saveName = () => {
      store.saveName("poetries");
    };

总结

pinia的知识点很少,如果你有Vuex基础,那么学起来更是易如反掌

pinia无非就是以下3个大点:

  • state
  • getters
  • actions
Last Updated:
Contributors: leeguooooo