Vuex 学习笔记

最后更新:
阅读次数:

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

核心概念

  • state: 存放各种状态

  • getter: 存放某些状态基于某个条件计算后的状态

  • mutation: 用于直接更改 store 中的状态(state)

  • action: 可以用来封装含有异步逻辑的可复用的代码

  • module: 模块化状态树,以免单一状态树太过臃肿

    • namespaced: true 启用模块的命名空间,使模块具有更高的封装度和复用性

项目目录结构

Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:

  • 应用层级的状态应该集中到单个 store 对象中
  • 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的
  • 异步逻辑都应该封装到 action 里面
|...
├── components
│ ├── App.vue
│ └── ...
├── store
│ ├── index.js # 我们组装模块并导出 store 的地方
│ ├── state.js # 根级别的 state
│ ├── getters.js # 根级别的 getters
│ ├── mutations.js # 根级别的 mutation
│ ├── mutation-types.js # 统一集中管理 mutation 类型,可选
│ ├── actions.js # 根级别的 action
│ └── modules
│ ├── cart.js # 购物车模块
│ └── products.js # 产品模块
└── router
└── index.js # 路由

基础语法

// state: 存储状态的对象
const state = {
count: 0
};

// getter: 可以看作是 store 的计算属性
const getters = {
isGreaterThanTen(state) {
return state.count > 10;
}
};

// mutation: 用于更改状态 state
const mutations = {
ADD_ONE(state) {
state.count++;
}
};

// action: 可以封装可复用的异步操作
const actions = {
delayAddOne(context) {
setTimeout(() => {
context.commit("ADD_ONE");
}, 1000);
}
};

const store = new Vuex.Store({
state,
getters,
mutations,
actions
});

export default store;
import store from "./vuex/store.js";

new Vue({
store,
computed: {
count() {
return this.$store.state.count;
},
isGreaterThanTen() {
return this.$store.getters.isGreaterThanTen;
}
},
methods: {
addOne() {
this.$store.commit("ADD_ONE");
},
delayAddOne() {
this.$store.dispatch("delayAddOne");
}
}
}).$mount("#app");

State

Vuex 使用了单一状态树,即用一个对象就包含了全部的应用层级状态。至此它便作为一个唯一数据源 (SSOT)而存在。这也意味着,每个应用(每个组件)将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。

  • 在组件中获取 Vuex 状态的最好方式是:使用计算属性
// 获取一个 Vuex 状态
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count() {
return this.$store.state.count;
}
}
};
  • 当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性

当映射的计算属性的名称与 state 的属性名称相同时,我们可以给 mapState 传一个字符串数组,以获取多个 Vuex 状态。

// 获取多个 Vuex 状态
// 在构建版本里可以使用: import { mapState } from 'vuex'
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
// 映射为 this.state1、this.state2
...Vuex.mapState(["state1", "state2"]),

// 映射为 this.stateThree
...Vuex.mapState({
stateThree: "state3"
}),

count() {
return `${this.$store.state.count} 次`;
}
}
};

Getter

  • getter:可以认为是 store 的计算属性

就像 Vue 的计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。

const state = {
todos: [
{ id: 1, text: "二号去听经", done: true },
{ id: 2, text: "三号去餐厅", done: false },
{ id: 3, text: "晚上住旅店", done: true },
{ id: 4, text: "然后看电影", done: false }
]
};

const getters = {
doneTodos: state => {
return state.todos.filter(todo => todo.done);
}
};

const store = new Vuex.Store({
state,
getters
});
  • 在组件中,我们可以像获取 state 那样来通过计算属性获取 getters。
// 获取一个 Vuex getters
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
doneTodos() {
return this.$store.getters.doneTodos;
}
}
};
  • 与获取多个 state 类似,使用 mapGetters 辅助函数可以将多个 getter 映射到本地组件的计算属性中
// 获取多个 Vuex 状态
// 在构建版本里可以使用: import { mapGetters } from 'vuex'
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
// 映射为 this.getter1、this.getter2
...Vuex.mapGetters(["getter1", "getter2"]),

// 映射为 this.getterThree
...Vuex.mapGetters({
getterThree: "getter3"
}),

count() {
return `${this.$store.state.count} 次`;
}
}
};

Mutation

更改 Vuex 的 store 中的状态(state)的唯一方法是显式地提交 mutation。

Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个对应的类型 (type) 和 一个回调函数 (handler)。并且这个函数的第一个参数是 state。

const state = {
count: 0
};

const mutations = {
// 一个类型为 ADD_ONE 的 mutation
ADD_ONE(state) {
state.count++;
}
};

const store = new Vuex.Store({
state,
mutations
});
// 显式地提交 commit
const Counter = {
template: `<div>{{ count }} <button @click="addOne">加一</button></div>`,
methods: {
addOne() {
this.$store.commit("ADD_ONE");
}
}
};
  • 我们可以向 this.$store.commit 传入额外的参数,即 mutation 的 载荷(payload)
const mutations = {
ADD_ONE(state) {
state.count++;
},
ADD_N(state, n) {
state.count += n;
}
};

// 组件中
this.$store.commit("ADD_N", 18);
  • mutation 类型书写规范

在大型项目中,会有很多 mutation,这里建议使用 ES6 的风格来定义 mutation 类型

// 统一对 mutation 类型进行管理,然后暴露出去接口
// mutation-types.js
export const ADD_ONE = "ADD_ONE";
export const ADD_N = "ADD_N";
// store.js
import Vuex from "vuex";
import { ADD_ONE } from "./mutation-types";

const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
[ADD_ONE](state) {
state.count++;
}
}
});
  • mutation 必须是同步函数:即函数内部不能有异步操作的代码

原因没搞懂,待补充~

  • 使用 mapMutations 辅助函数可以将组件中的 methods 映射为 this.$store.commit 调用。
// 在构建版本里可以使用: import { mapMutations } from 'vuex'
const Counter = {
template: `<div id="container">
<div>{{ count }}</div>
<div><button @click="ADD_ONE">+1</button></div>
<div><button @click="addNum(10)">+10</button></div>
</div>`,
methods: {
// 映射为 this.ADD_ONE()
...Vuex.mapMutations(["ADD_ONE"]),

// 映射为 this.addNum(n)
...Vuex.mapMutations({
addNum: "ADD_N"
})
}
};

Action

  • action 类似于 mutation,不同在于:
    • action 提交的是 mutation,而不是直接变更状态
    • action 可以包含任意异步操作,而 mutation 只能包含同步操作

action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。

const state = {
count: 0
};

const mutations = {
ADD_ONE(state) {
state.count++;
}
};

const actions = {
// 传入 context 对象
delay1sAddOne(context) {
setTimeout(() => {
context.commit("ADD_ONE");
}, 1000);
},

// 使用 ES6 的参数解构,简化代码
delay2sAddOne({ commit }) {
setTimeout(() => {
context.commit("ADD_ONE");
}, 2000);
}
};

const store = new Vuex.Store({
state,
mutations,
actions
});
// 在组件中触发 action
const Counter = {
template: `<div>{{ count }} <button @click="doAction">延迟1秒后加一</button></div>`,
methods: {
doAction() {
this.$store.dispatch("delay1sAddOne");
}
}
};
  • 在分发 action 的时候也可以给它传入参数(载荷)
const actions = {
delayAddOne(context, time) {
setTimeout(() => {
context.commit("ADD_ONE");
}, time);
}
};
this.$store.dispatch("delay1sAddOne", 3000);
  • 使用 mapActions 辅助函数可以将组件中的 methods 映射为 this.$store.dispatch 调用。
// 在构建版本里可以使用: import { mapActions } from 'vuex'
const Counter = {
template: `<div id="container">
<div>{{ count }}</div>
<div><button @click="delay1sAddOne">延迟一秒后加一</button></div>
<div><button @click="addOneInTwoSeconds">延迟两秒后加一</button></div>
</div>`,
methods: {
// 映射为 this.delay1sAddOne()
...Vuex.mapActions(["delay1sAddOne"]),

// 映射为 this.addOneInTwoSeconds()
...Vuex.mapMutations({
addOneInTwoSeconds: "delay2sAddOne"
})
}
};
  • 由于 action 中的操作可以是异步的,并且 action 函数传入的 context 对象类似 store 实例,所以我们可以组合使用 action
const actions = {
actionA(context) {
return new Promise((resolve, reject) => {
setTimeout(() => {
context.commit("ADD_ONE");
resolve();
}, 1000);
});
},
// action 中调用 action
actionB(context) {
context.dispatch("actionA").then(() => {
console.log("In actionB, actionA resolved");
});
}
};

Module

如果在大型应用中使用 单一状态树,那么 vuex 的 state 对象将变的无比巨大和臃肿,非常不好维护。

为了解决这个问题,vuex 允许我们将 store 分割成多个模块(module)。每个模块可以拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割。

// 子模块 userPage
const userPage = {
state: {
status: "online"
},
mutations: {
SIGN_OUT(state) {
// 这里的 `state` 对象是模块的局部状态
state.status = "offline";
}
}
};

// 子模块 articlePage
const articlePage = {
state: {
isCodeHighlight: true
},
mutations: {
TOGGLE_CODE_HIGHLIGHT(state) {
state.isCodeHighlight = !state.isCodeHighlight;
}
}
};
// 定义 vuex 根模块状态
const state = {
isShowSidebar: false
};

const mutations = {
TOGGLE_SIDEBAR(state) {
state.isShowSidebar = !state.isShowSidebar;
}
};

const store = new Vuex.Store({
state,
mutations,

// vuex 根模块引入子模块
modules: {
userPage,
articlePage
}
});
<div id="app">
<div class="test">
<span>isShowSidebar:{{ isShowSidebar.toString() }}</span>
<button @click="toggleSidebar">toggleSidebar</button>
</div>
<div class="test">
<span>userStatus:{{ userStatus }}</span>
<button @click="signOut">signOut</button>
</div>
</div>
// 新建 vue 实例
new Vue({
store,
computed: {
isShowSidebar() {
// 获取 vuex 根模块中的状态
return this.$store.state.isShowSidebar;
},
userStatus() {
// 获取 vuex 子模块中的状态
return this.$store.state.userPage.status;
}
},
methods: {
toggleSidebar() {
// 提交 vuex 根模块的 mutation
this.$store.commit("TOGGLE_SIDEBAR");
},
signOut() {
// 提交 vuex 子模块的 mutation
this.$store.commit("SIGN_OUT");
}
}
}).$mount("#app");

子模块的命名空间

默认情况下,子模块内部的 action、mutation 和 getter 是注册在全局命名空间的,这样使得多个模块能够对同一 mutation 或 action 作出响应,容易混乱逻辑。

如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true 的方式使其成为命名空间模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

建议在使用模块时启用命名空间~

// 启用命名空间的子模块 userPage
const userPage = {
// 启用命名空间
namespaced: true,

state: {
// 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
status: "online"
},
getters: {
isOnline(state) {
return state.status === "online";
}
},
mutations: {
SIGN_OUT(state) {
state.status = "offline";
}
},
actions: {
FORCE_SIGN_OUT(context) {
setTimeout(() => {
context.state.status = "offline";
}, 10000);
}
},

// 嵌套模块
modules: {
// 嵌套的模块会继承父模块的命名空间
testModule1: {
state: {},
getters: {
test1() {
return "test111";
}
}
},
// 嵌套的模块也启用命名空间
testModule2: {
namespaced: true,
state: {},
getters: {
test2() {
return "test222";
}
}
}
}
};
new Vue({
store,
computed: {
// 获取子模块的 state
userStatus() {
return this.$store.state.userPage.status;
},
// 获取子模块的 getter
isOnline() {
return this.$store.getters["userPage/isOnline"];
},
// 获取子模块的 未设置命名空间的 嵌套模块的 getter
test1() {
return this.$store.getters["userPage/test1"];
},
// 获取子模块的 设置了命名空间的 嵌套模块的 getter
test2() {
return this.$store.getters["userPage/testModule2/test2"];
}
},
methods: {
// 提交 vuex 子模块的 mutation
signOut() {
this.$store.commit("userPage/SIGN_OUT");
},
// 分发 vuex 子模块的 action
fouceSignOut() {
this.$store.dispatch("userPage/FORCE_SIGN_OUT");
}
}
}).$mount("#app");

getter、mutation、action 的参数问题

  • 对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象 state

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

  • 对于模块内部的 getter,根节点状态会作为第三个参数暴露出来 (state, getters, rootState, rootGetters)

  • mutation 和 action 的第二个参数都是 payload

  • action 的第一个参数是 context

// context 包含以下属性
{
state, // 等同于 store.state, 若在模块中则为局部状态
rootState, // 等同于 store.state, 只存在于模块中
commit, // 等同于 store.commit
dispatch, // 等同于 store.dispatch
getters; // 等同于 store.getters
}

v-model 双向绑定 Vuex 状态

在组件中,可以使用 v-model 来双向绑定 data 中的属性的值。但是如果现在与 v-model 绑定的值是一个 vuex 的状态呢?

由于 vuex 的状态只能通过 mutation 来进行改变,所以我们可以使用属性的 gettersetter 来使得 v-model 与 vuex 的一个状态也可以进行双向绑定

const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
CHANGE_COUNT(state, value) {
state.count = value;
}
}
});

// 组件
const Counter = {
template: `<div>
<div>{{ count }}</div>
<div> <input v-model="count"/> </div>
</div>`,
computed: {
count: {
get() {
return this.$store.state.count;
},
set(value) {
this.$store.commit("CHANGE_COUNT", value);
}
}
}
};