【Vue.js】Vuex

什么是 Vuex?

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

Vuex 的工作流:

Vuex 的基本思想是借鉴了 FluxReduxThe Elm Architecture。 但是与其他的库不同,Vuex 是高度集成在 Vue 里面的,它的更新完全依赖于 Vue 的高效细粒度化机制!

什么情况下我们应该使用 Vuex ?

从项目规模的角度来看:

  • 如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。
  • 如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。

从数据来源的角度:

  • 通常组件的数据( props )传递都是单一的,过分依赖 Vuex 来维护和全局性地修改数据可能会导致数据变得难以预测和维护

从性能的角度来看:

  • Vue 的数据都是响应式数据,在通常情况下也不需要把数据提升到全局上面进行全局管理
  • 在应用中,有一些可能需要全局更新并且不会导致应用闪屏刷新的情况,我们可能会考虑像 Vuex 一样的数据管理仓库。

注意:

使用 Vuex 时需要注意自己网页上面的版本:Vuex4 + Vue3 / Vuex3 + Vue2 !!!

安装 Vuex

直接下载 / CDN 引用

直接引入:unpkg.com/vuex@4 Unpkg.com 提供了基于 npm 的 CDN 链接。您也可以通过 unpkg.com/vuex@4.0.0/... 这样的方式指定特定的版本。

html 复制代码
<script src="https://unpkg.com/vue@3"></script>
<script src="https://unpkg.com/vuex@4"></script>

终端安装

shell 复制代码
# npm 安装
npm install vuex@next --save

# pnpm 安装
pnpm install vuex@next --save

# yarn 安装
yarn add vuex@next

从 github 上面克隆代码自己构建

shell 复制代码
git clone https://github.com/vuejs/vuex.git node_modules/vuex
cd node_modules/vuex
yarn
yarn build

创建并使用 store

声明 store

安装 Vuex 之后,让我们来创建一个 store ------仅需要提供一个初始 state 对象和一些 mutation:

javascript 复制代码
// 使用 createStore 这个方法来创建 store
import { createStore } from 'vuex';

const store = createStore({
  /*
    这里的 state 使用函数是为了避免 state 接收相同的初始化对象
  */
  state() {
    return {
      count: 0
    };
  },
  /*
    这里的 mutations 是一个方法集合,这些方法在执行的时候都会把 state 依赖注入进去执行并通知 store 更新 state
  */
  mutations: {
    addCount(state) {
      state.count += 1;
    }
  },
});

export default store;

注册 store

main.js中使用 store

javascript 复制代码
import { createApp } from 'vue';

import App from './App.vue';

import store from './store';

const app = createApp(App);

app.use(store);

app.mount('#app');

使用 store

在任意一个组件 <Whatever>中使用:

  • 使用 Vuex.useStore() 方法获取到 store 的实例
  • 使用 Vue.watch实时获取 state 的值
  • 使用 Vue.onMounted设置一个定时器实时调用 store.commit()方法 修改 state
javascript 复制代码
import { computed, watch, onMounted } from 'vue';
import { useStore } from 'vuex';

const store = useStore();

// 访问 state
watch(() => store.state, (newState, prevState) => {
  console.log(newState, prevState);
});

// 修改 state
onMounted(() => {
  setInterval(() => {
    store.commit('addCount');
  }, 2000);
});

特别注意:

修改 store 的值只能通过方法调用的方式,而并非赋值表达式,具体原因如下:

  1. 赋值表达式破坏了代码的可读性
  2. Vuex 的 devtool 主要在 mutations 里面工作,使用赋值表达式没有办法有效且实时地跟踪状态的变化

核心概念:

概念 中文名 描述
view 视图 集中管理状态对应显示的视图
state 状态 集中管理的数据状态
getter 获取器 集中管理的数据状态的【计算属性】
action 操作 对于状态仓库的通知
mutation 改变 通过安全、可靠的方式让仓库更新数据

state

概述:

在 Vuex 中,state 是集中管理的数据状态。

注册:

javascript 复制代码
// 使用 createStore 这个方法来创建 store
import { createStore } from 'vuex';

const store = createStore({
  state() {
    return {
      count: 0
    };
  },
  // ...
});

export default store;

使用:

对于单一的数据状态,使用 Vue.computed即可获取到 Vuex 仓库中的数据的状态:

javascript 复制代码
import { computed } from 'vue';
import { useStore } from 'vuex';

const store = useStore();

const count = computed(() => store.state.count);

不仅如此,在视图模板中,也可以使用 $store的方式来进行获取:

html 复制代码
<div class="counter">
  <h1>{{ $store.state.count }}</h1>
</div>

mapState 辅助函数 (在 options API 上面使用)

在 options API 中:当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性:

  • 使用对象
javascript 复制代码
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex';

export default {
  // ...
  computed: mapState({
    // 箭头函数可使代码更简练
    count: state => state.count,

    // 传字符串参数 'count' 等同于 `state => state.count`
    countAlias: 'count',

    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
    countPlusLocalState (state) {
      return state.count + this.localCount;
    }
  })
};
  • 使用数组:
javascript 复制代码
import { mapState } from 'vuex';

export default {
  computed: mapState([
    'count'
  ])
};
  • 展开 mapState :
javascript 复制代码
computed: {
  localComputed () { /* ... */ },
  // 使用对象展开运算符将此对象混入到外部对象中
  ...mapState({
    // ...
  })
}

getter

概述:

如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它------无论哪种方式都不是很理想。 Vuex 允许我们在 store 中定义"getter"(可以认为是 store 的【计算属性】)。

注册:

javascript 复制代码
const store = createStore({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos (state) {
      return state.todos.filter(todo => todo.done);
    },
    // 使用 getters 作为第二个参数
    doneTodosCount (state, getters) {
      return getters.doneTodos.length;
    },
    // 通过方法来访问
    getTodoById: (state) => (id) => {
      return state.todos.find(todo => todo.id === id);
    }
  }
});

使用:

  • 基本使用:
javascript 复制代码
import { watch } from 'vue';
import { useStore } from 'vuex';

const store = useStore();

watch(() => store.getters.doneTodos, (newGetter, oldGetter) => {
  console.log({ newGetter, oldGetter });
});

mapGetters 辅助函数:

  • 使用字符串数组
javascript 复制代码
import { mapGetters } from 'vuex';

export default {
  // ...
  computed: {
  // 使用对象展开运算符将 getter 混入 computed 对象中
    ...mapGetters([
      'doneTodosCount',
      'anotherGetter',
      // ...
    ])
  }
};
  • 使用名称映射:
javascript 复制代码
...mapGetters({
  // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
  doneCount: 'doneTodosCount'
})

mutation

概述:

更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)

基本使用:

javascript 复制代码
// 使用 createStore 这个方法来创建 store
import { createStore } from 'vuex';

const store = createStore({
  /*
    这里的 state 使用函数是为了避免 state 接收相同的初始化对象
  */
  state() {
    return {
      count: 0
    };
  },
  /*
    这里的 mutations 是一个方法集合,这些方法在执行的时候都会把 state 依赖注入进去执行并通知 store 更新 state
  */
  mutations: {
    addCount(state) {
      state.count += 1;
    }
  },
});

export default store;

新增提交参数 payload

你可以向 store.commit 传入额外的参数,即 mutation 的载荷(payload):

javascript 复制代码
// ...
mutations: {
  addCount(state, n) {
    state.count += n;
  }
}
javascript 复制代码
store.commit('addCount', 10);

commit()中使用整个对象来进行提交:

提交 mutation 的另一种方式是直接使用包含 type 属性的对象:

javascript 复制代码
store.commit({
  type: 'addCount',
  amount: 10
});

整个对象都作为载荷传给 mutation 函数,因此处理函数保持不变:

javascript 复制代码
mutations: {
  addCount (state, payload) {
    state.count += payload.amount;
  }
}

在组件中提交 mutations (options API)

你可以在组件中使用 this.$store.commit('xxx') 提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)。

javascript 复制代码
import { mapMutations } from 'vuex';

export default {
  // ...
  methods: {
    ...mapMutations([
      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

      // `mapMutations` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
  }
};

注意:

  • mutations 必须要是一个同步函数 !因为如果是异步函数,VueDevtool 就没有办法实时按顺序捕获到上一条状态和后一条状态的操作记录快照
  • 如果对 Vuex store 的操作可能包含了异步行为,请使用 action !

action

概述:

action 类似于 mutation,不同在于:

  • action 提交的是 mutation,而不是直接变更状态。
  • action 可以包含任意异步操作。
  • action的第一个参数是 contextmutation的第一个参数是 state

语法:

action 中的函数接受一个与 store 实例具有相同方法和属性的上下文对象 context (不是 store 本身),因此你可以调用 context.commit 提交一个 mutation,或者通过 context.statecontext.getters 来获取 stategetters

typescript 复制代码
interface IContext {
  commit(type: string, payload: any): void;
  dispatch(type: string, payload: any): void;
}

type TAction = {
  [actionFunction: string]: <T> (ctx: IContext, payload: T) => void;
};

建议:

通常,我们可以直接解构 context中的方法进行使用 (e.g commitstatedispatch)

action 派发:

action 通过 store.dispatch 方法触发:

typescript 复制代码
store.dispatch('increment');

action 在此处发挥的作用不是很大,但当我们考虑如下场景, action 的使用就很有必要了:

  1. 异步的 action 计算操作
javascript 复制代码
actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}
  1. 一个 action 派发多个 mutation
javascript 复制代码
import { createStore } from 'vuex';
import * as shop from '@/services/shop';
import * as types from './actionTypes';

const store = createStore({
  state() {
    return {
      cart: {
        // ...
        added: []
      }
    };
  },
  mutations: {
    [types.CHECKOUT_SUCCESS](state, payload) {
      // ...
    },
    [types.CHECKOUT_FAILURE](state, payload) {
      // ...
    }
  },
  actions: {
    checkout({ commit, state }, products) {
      // 把当前购物车的物品备份起来
      const savedCartItems = [...state.cart.added];
      // 发出结账请求
      // 然后乐观地清空购物车
      commit(types.CHECKOUT_REQUEST);
      // 购物 API 接受一个成功回调和一个失败回调
      shop.buyProducts(
        products,
        // 成功操作
        () => commit(types.CHECKOUT_SUCCESS),
        // 失败操作
        () => commit(types.CHECKOUT_FAILURE, savedCartItems)
      );
    }
  }
});

在组件中提交多个 action (options API)

你在组件中使用 dispatch 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store):

javascript 复制代码
import { mapActions } from 'vuex'

export default {
  // ...
  methods: {
    ...mapActions([
      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

      // `mapActions` 也支持载荷:
      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
    ]),
    ...mapActions({
      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
    })
  }
}

action 中派发另外一个 action

一个 store.dispatch 在不同模块中可以触发多个 action 函数。在这种情况下,只有当所有触发函数完成后,返回的 Promise 才会执行。

javascript 复制代码
actions: {
  // ...
  actionA(context, payload) {
    // ...
  },
  actionB ({ dispatch, commit }) {
    return dispatch('actionA').then(() => {
      commit('someOtherMutation')
    })
  }
}

module

概述:

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得难以维护。 为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。 每个模块拥有自己的 statemutationactiongetter、甚至是嵌套子模块 (sub modules)------从上至下进行同样方式的分割:

javascript 复制代码
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 的状态

module 的成分:

对于模块内部的 mutationgetter,接收的第一个参数是模块的局部状态对象

javascript 复制代码
const moduleA = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 这里的 `state` 对象是模块的局部状态
      state.count++
    }
  },
  getters: {
    doubleCount (state) {
      return state.count * 2
    }
  }
}

module 内部的 state 使用最外层的 state

对于模块内部的 action,局部状态通过 context.state 暴露出来,根节点状态则为 context.rootState

javascript 复制代码
const moduleA = {
  // ...
  actions: {
    incrementIfOddOnRootSum ({ state, commit, rootState }) {
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  }
}

module 内部的 getter 使用最外层的 getter

对于模块内部的 getter,根节点状态会作为第三个参数暴露出来:

javascript 复制代码
const moduleA = {
  // ...
  getters: {
    sumWithRootCount (state, getters, rootState) {
      return state.count + rootState.count
    }
  }
}

module 设置隔离

默认情况下,模块内部的 actionmutation 仍然是注册在全局命名空间的------这样使得多个模块能够对同一个 actionmutation 作出响应,Getter 同样也默认注册在全局命名空间。 如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:

javascript 复制代码
const store = createStore({
  modules: {
    account: {
      namespaced: true,

      // 模块内容(module assets)
      state: () => ({ ... }), // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
      getters: {
        isAdmin () { ... } // -> getters['account/isAdmin']
      },
      actions: {
        login () { ... } // -> dispatch('account/login')
      },
      mutations: {
        login () { ... } // -> commit('account/login')
      },

      // 嵌套模块
      modules: {
        // 继承父模块的命名空间
        myPage: {
          state: () => ({ ... }),
          getters: {
            profile () { ... } // -> getters['account/profile']
          }
        },

        // 进一步嵌套命名空间
        posts: {
          namespaced: true,

          state: () => ({ ... }),
          getters: {
            popular () { ... } // -> getters['account/posts/popular']
          }
        }
      }
    }
  }
})

上述代码就启用了命名空间的 getteraction 会收到局部化的 getterdispatchcommit。 换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced 属性后不需要修改模块内的代码。

带有命名空间的 module 访问全局的内容:

  • 如果你希望使用全局 stategetterrootStaterootGetters 会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action:
javascript 复制代码
getters: {
  // 在这个模块的 getter 中,`getters` 被局部化了
  // 你可以使用 getter 的第四个参数来调用 `rootGetters`
  someGetter (state, getters, rootState, rootGetters) {
    getters.someOtherGetter // -> 'foo/someOtherGetter'
    rootGetters.someOtherGetter // -> 'someOtherGetter'
    rootGetters['bar/someOtherGetter'] // -> 'bar/someOtherGetter'
  },
  someOtherGetter: state => { ... }
},
  • 若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true } 作为第三参数传给 dispatch 或 commit 即可。
javascript 复制代码
actions: {
  // 在这个模块中, dispatch 和 commit 也被局部化了
  // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
  someAction ({ dispatch, commit, getters, rootGetters }) {
    getters.someGetter // -> 'foo/someGetter'
    rootGetters.someGetter // -> 'someGetter'
    rootGetters['bar/someGetter'] // -> 'bar/someGetter'

    dispatch('someOtherAction') // -> 'foo/someOtherAction'
    dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

    commit('someMutation') // -> 'foo/someMutation'
    commit('someMutation', null, { root: true }) // -> 'someMutation'
  },
  someOtherAction (ctx, payload) { ... }
}
  • 完整代码:
javascript 复制代码
modules: {
  foo: {
    namespaced: true,

    getters: {
      // 在这个模块的 getter 中,`getters` 被局部化了
      // 你可以使用 getter 的第四个参数来调用 `rootGetters`
      someGetter (state, getters, rootState, rootGetters) {
        getters.someOtherGetter // -> 'foo/someOtherGetter'
        rootGetters.someOtherGetter // -> 'someOtherGetter'
        rootGetters['bar/someOtherGetter'] // -> 'bar/someOtherGetter'
      },
      someOtherGetter: state => { ... }
    },

    actions: {
      // 在这个模块中, dispatch 和 commit 也被局部化了
      // 他们可以接受 `root` 属性以访问根 dispatch 或 commit
      someAction ({ dispatch, commit, getters, rootGetters }) {
        getters.someGetter // -> 'foo/someGetter'
        rootGetters.someGetter // -> 'someGetter'
        rootGetters['bar/someGetter'] // -> 'bar/someGetter'

        dispatch('someOtherAction') // -> 'foo/someOtherAction'
        dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

        commit('someMutation') // -> 'foo/someMutation'
        commit('someMutation', null, { root: true }) // -> 'someMutation'
      },
      someOtherAction (ctx, payload) { ... }
    }
  }
}

在带命名空间的模块注册全局 action:

若需要在带命名空间的模块注册全局 action,你可添加 root: true,并将这个 action 的定义放在函数 handler 中。例如:

javascript 复制代码
{
  actions: {
    someOtherAction ({dispatch}) {
      dispatch('someAction')
    }
  },
  modules: {
    foo: {
      namespaced: true,

      actions: {
        someAction: {
          root: true,
          handler (namespacedContext, payload) { ... } // -> 'someAction'
        }
      }
    }
  }
}

带有 namespaced: true的绑定函数 (options API)

当使用 mapStatemapGettersmapActionsmapMutations 这些函数来绑定带命名空间的模块时,写起来可能比较繁琐:

javascript 复制代码
computed: {
  ...mapState({
    a: state => state.some.nested.module.a,
    b: state => state.some.nested.module.b
  }),
  ...mapGetters([
    'some/nested/module/someGetter', // -> this['some/nested/module/someGetter']
    'some/nested/module/someOtherGetter', // -> this['some/nested/module/someOtherGetter']
  ])
},
methods: {
  ...mapActions([
    'some/nested/module/foo', // -> this['some/nested/module/foo']()
    'some/nested/module/bar' // -> this['some/nested/module/bar']()
  ])
}

对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为:

javascript 复制代码
computed: {
  ...mapState('some/nested/module', {
    a: state => state.a,
    b: state => state.b
  }),
  ...mapGetters('some/nested/module', [
    'someGetter', // -> this.someGetter
    'someOtherGetter', // -> this.someOtherGetter
  ])
},
methods: {
  ...mapActions('some/nested/module', [
    'foo', // -> this.foo()
    'bar' // -> this.bar()
  ])
}

而且,你可以通过使用 createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

javascript 复制代码
import { createNamespacedHelpers } from 'vuex'

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
  computed: {
    // 在 `some/nested/module` 中查找
    ...mapState({
      a: state => state.a,
      b: state => state.b
    })
  },
  methods: {
    // 在 `some/nested/module` 中查找
    ...mapActions([
      'foo',
      'bar'
    ])
  }
}

插件:

参考:vuex.vuejs.org/zh/guide/pl...

测试:

参考:vuex.vuejs.org/zh/guide/te...

项目结构示例:

bash 复制代码
├── index.html
├── main.js
├── api
│   └── ... # 抽取出API请求
├── components
│   ├── App.vue
│   └── ...
└── store
    ├── index.js          # 我们组装模块并导出 store 的地方
    ├── actions.js        # 根级别的 action
    ├── mutations.js      # 根级别的 mutation
    └── modules
        ├── cart.js       # 购物车模块
        └── products.js   # 产品模块
相关推荐
上趣工作室几秒前
vue2在el-dialog打开的时候使该el-dialog中的某个输入框获得焦点方法总结
前端·javascript·vue.js
家里有只小肥猫1 分钟前
el-tree 父节点隐藏
前端·javascript·vue.js
_xaboy2 小时前
开源项目低代码表单设计器FcDesigner扩展自定义的容器组件.例如col
vue.js·低代码·开源·动态表单·formcreate·低代码表单·可视化表单设计器
_xaboy2 小时前
开源项目低代码表单设计器FcDesigner扩展自定义组件
vue.js·低代码·开源·动态表单·formcreate·可视化表单设计器
mez_Blog2 小时前
Vue之插槽(slot)
前端·javascript·vue.js·前端框架·插槽
爱睡D小猪2 小时前
vue文本高亮处理
前端·javascript·vue.js
paopaokaka_luck2 小时前
基于Spring Boot+Vue的多媒体素材管理系统的设计与实现
java·数据库·vue.js·spring boot·后端·算法
开心工作室_kaic2 小时前
ssm102“魅力”繁峙宣传网站的设计与实现+vue(论文+源码)_kaic
前端·javascript·vue.js
放逐者-保持本心,方可放逐2 小时前
vue3 中那些常用 靠copy 的内置函数
前端·javascript·vue.js·前端框架
IT古董2 小时前
【前端】vue 如何完全销毁一个组件
前端·javascript·vue.js