文章目录
Vuex?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
这个状态自管理应用包含以下几个部分:
- state,驱动应用的数据源;
- view,以声明方式将 state 映射到视图;
- actions,响应在 view 上的用户输入导致的状态变化。
当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
- 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态。
对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
Store
在index.ts中,定义并导出Vuex Store
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
const store = new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: {},
});
export default store;
Modules
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块。
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
todos模块
为todos定义一个module,放在modules文件夹下:
const state = {...};
const getters = {...};
const actions = {...};
const mutations = {...};
export default {
state,
getters,
actions,
mutations,
};
然后在index.ts中导入,在modules字段中引入:
import todos from "./modules/todos";
const store = new Vuex.Store({
state: {},
mutations: {},
actions: {},
modules: {
todos,
},
});
命名空间
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true
的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。
store.commit('b/subModule/login');
store.dispatch('b/subModule/login');
store.getters['b/subModule/login'];
需要通过[/路径]
来访问对应的getter、action 及 mutation
在带命名空间的模块内访问全局内容(Global Assets)
如果你希望使用全局 state 和 getter,rootState
和 rootGetters
会作为第三和第四参数传入 getter,也会通过 context 对象的属性传入 action。
若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true }
作为第三参数传给 dispatch 或 commit 即可。
modules: {
foo: {
namespaced: true,
getters: {
// 在这个模块的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四个参数来调用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
someOtherGetter: state => { ... }
},
actions: {
// 在这个模块中, dispatch 和 commit 也被局部化了
// 他们可以接受 `root` 属性以访问根 dispatch 或 commit
someAction ({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'
dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
},
someOtherAction (ctx, payload) { ... }
}
}
}
State
Vuex 使用单一状态树:一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源”而存在。这也意味着,每个应用将仅仅包含一个 store
实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
定义todo应用的state:
const state = {
todos: [],
};
在 Vue 组件中获得 Vuex 状态
计算属性
// 创建一个 Counter 组件
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}
根组件注册store
Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex)):
import Vue from "vue";
import App from "./App.vue";
import store from "./store";
Vue.config.productionTip = false;
new Vue({
store,
render: (h) => h(App),
}).$mount("#app");
通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store 访问到:
methods: {
...mapActions(["addTodo"]),
onSubmit(e) {
e.preventDefault();
const todo = {
title: (this as any).title,
id: (this as any).$store.state.todos.todos[0].id,
};
(this as any).addTodo(todo);
},
},
mapState 辅助函数
computed: {
...mapState({
message: state => state.todos
})
},
Getters
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
Getter 接受 state 作为其第一个参数:
const getters = {
allTodos: (state: Record<string, unknown>) => state.todos,
};
访问Getter返回值
通过属性访问
Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:
store.getters.allTodos
我们可以很容易地在任何组件中使用它:
computed: {
getAllTodos () {
return this.$store.getters.allTodos;
}
}
通过方法访问
你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。
getters: {
getAllTodos: (state) => () => {
return state.todos;
}
}
store.getters.getAllTodos();
mapGetters 辅助函数
mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
import { mapGetters, mapActions } from "vuex";
@Component({
name: "Todos",
//...
computed: mapGetters(["allTodos"]),
})
在template中可以访问allTodos
变量:
<div
@dblclick="onDblClick(todo)"
v-for="todo in allTodos"
:key="todo.id"
class="todo"
v-bind:class="{ 'is-complete': todo.completed }"
>
{{ todo.title }}
<i @click="deleteTodo(todo.id)" class="fas fa-trash-alt"></i>
</div>
Mutations
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
const mutations = {
setTodos: (
state: Record<string, unknown>,
todos: Record<string, Array<any>>
) => (state.todos = todos),
newTodo: (state: Record<string, any>, todo: Record<string, unknown>) =>
state.todos.unshift(todo),
removeTodo: (state: Record<string, any>, id: number) =>
(state.todos = state.todos.filter(
(todo: Record<string, unknown>) => todo.id !== id
)),
updateTodo: (
state: Record<string, any>,
updTodo: Record<string, unknown>
) => {
const index = state.todos.findIndex(
(todo: Record<string, unknown>) => todo.id === updTodo.id
);
if (index !== -1) {
state.todos.splice(index, 1, updTodo);
}
},
};
setTodos
,newTodo
等均为事件类型(或事件名称),
我们不能直接调用一个 mutation handler(因为是回调函数)。这个选项更像是事件注册:“当触发一个类型为 setTodos
的 mutation 时,调用此函数。”要唤醒一个 mutation handler,需要以相应的 type
调用 store.commit
方法:
可以向 store.commit 传入额外的参数,即 mutation 的 载荷(payload):
store.commit('increment', {
amount: 10
})
Mutation 必须是同步函数
现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而, mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
Actions
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
Action 函数接受一个与 store 实例具有相同方法和属性的 context
对象,因此你可以调用 context.commit
提交一个 mutation,或者通过 context.state
和 context.getters
来获取 state 和 getters。
实践中,我们会经常用到 ES2015 的 参数解构 来简化代码(特别是我们需要调用 commit
很多次的时候)。
const actions = {
async fetchTodos({ commit }: { commit: any }): Promise<void> {
const response = await axios.get(
"https://jsonplaceholder.cypress.io/todos"
);
//通过commit方法更新视图,第一个参数是在mutation里定义的事件类型,第二个参数是payload
commit("setTodos", response.data);
},
}
分发 Action
Action 通过 store.dispatch 方法触发:
store.dispatch('increment')
在组件中分发 Action:mapActions
在组件中使用 this.$store.dispatch('xxx')
分发 action,或者使用 mapActions
辅助函数将组件的 methods 映射为 store.dispatch
调用(需要先在根节点注入 store)。
在这个app中,使用mapActions
引入actions并通过this
调用:
@Component({
name: "Todos",
methods: {
...mapActions(["fetchTodos", "deleteTodo", "updateTodo"]),
onDblClick(todo) {
const updTodo = {
id: todo.id,
title: todo.title,
completed: !todo.completed,
};
(this as any).updateTodo(updTodo);
},
},
computed: mapGetters(["allTodos"]),
created() {
(this as any).fetchTodos();
},
})
其他
Vue生命周期钩子
created:
在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el property 目前尚不可用。
在app中,我们在created阶段获取所有todos并渲染:
@Component({
name: "Todos",
//...
created() {
(this as any).fetchTodos();
},
})
双击事件
通过@dblclick="onDblClick(todo)"
注册双击事件:
<div
@dblclick="onDblClick(todo)"
v-for="todo in allTodos"
:key="todo.id"
class="todo"
v-bind:class="{ 'is-complete': todo.completed }"
>
{{ todo.title }}
<i @click="deleteTodo(todo.id)" class="fas fa-trash-alt"></i>
</div>