基本概念
vue进行开发过程中有没有遇到这样一种场景,就是有些时候一些数据是一种通用的共享数据(比如登录信息),那么这类数据在各个组件模块中可能都会用到,如果每个组件中都去后台重新获取那么势必会造成性能浪费,为了解决这一问题一个新的状态管理工具 - vuex
就应运而生
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
什么是"状态管理模式"?
这个状态自管理应用包含以下几个部分:
- 状态,驱动应用的数据源;
- 视图,以声明方式将状态映射到视图;
- 操作,响应在视图上的用户输入导致的状态变化。
安装方式
CDN引用
javascript
https://unpkg.com/vuex@4
Unpkg.com 提供了基于 npm 的 CDN 链接。以上的链接会一直指向npm 上发布的最新版本。
您也可以通过 https://unpkg.com/vuex@4.0.0/dist/vuex.global.js 这样的方式指定特定的版本。
javascript
Npm
npm install vuex@next --save
Yarn
yarn add vuex@next --save
核心概念
State
- 单一状态树
Vuex 使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个"唯一数据源 (SSOT)"而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。 - 在 Vue 组件中获得 Vuex 状态
由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态:
javascript
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}}
每当store.state.count变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM
Vuex 通过 Vue 的插件系统将 store 实例从根组件中"注入"到所有的子组件里。且子组件能通过this.$store访问到。让我们更新下Counter的实现:
javascript
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}}
- mapState 辅助函数
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。
为了解决这个问题,我们可以使用 mapState辅助函数帮助我们生成计算属性,让你少按几次键
javascript
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
}
})}
- 对象展开运算符
mapState函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给computed属性。但是自从有了对象展开运算符【...】,我们可以极大地简化写法:
javascript
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})}
对象展开运算符
var obj = {a:1,b:2}
var obj1 = {c:3,d:4}
var obj2 ={...obj,...obj1}
var obj3 = {...obj,a:8,w:66}
var obj4 ={...obj,a:8,w:66,...obj1}
console.log(obj); //{a: 1, b: 2}
console.log(obj1); //{c: 3, d: 4}
console.log(obj2); //{a: 1, b: 2,c: 3, d: 4}
console.log(obj3); //{a: 8, b: 2, w: 66}
console.log(obj4); //{a: 8, b: 2, w: 66, c: 3, d: 4}
Getter
我们需要从 store 中的 state 中派生出一些状态,如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它------无论哪种方式都不是很理想。
Vuex 允许我们在 store 中定义"getter"(可以认为是 store 的计算属性)。
从 Vue 3.0 开始,getter 的结果不再像计算属性一样会被缓存起来
Getter 接受 state 作为其第一个参数:
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)
}
}})
- 通过属性访问
Getter 会暴露为 store.getters 对象,你可以以属性的形式访问这些值:
javascript
store.getters.doneTodos
Getter 也可以接受其他 getter 作为第二个参数
javascript
getters: {
// ...
doneTodosCount (state, getters) {
return getters.doneTodos.length
}}
- 通过方法访问
你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。
javascript
getters: {
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}}
store.getters.getTodoById(2)
getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
- mapGetters 辅助函数
mapGetters辅助函数仅仅是将store中的getter映射到局部计算属性
javascript
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}}
Mutation
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
javascript
const store = createStore({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}})
- 提交载荷(Payload)
你可以向 store.commit 传入额外的参数,即 mutation 的载荷(payload):
javascript
mutations: {
increment (state, n) {
state.count += n
}}
store.commit('increment', 10)
- 对象风格的提交方式
提交 mutation 的另一种方式是直接使用包含 type 属性的对象:
javascript
store.commit({
type: 'increment',
amount: 10})
当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此处理函数保持不变:
javascript
mutations: {
increment (state, payload) {
state.count += payload.amount
}}
- Mutation 必须是同步函数
一条重要的原则就是要记住 mutation 必须是同步函数
Action
Action 类似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接变更状态。
- Action 可以包含任意异步操作。
- 分发 Action
Action 通过 store.dispatch 方法触发:
javascript
store.dispatch('increment')
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}}
- 在组件中分发 Action
你在组件中使用this.$store.dispatch('xxx')分发 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')`
})
}}
Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿
为了解决以上问题,Vuex允许我们将store分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块
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 的状态
进阶
项目结构
Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:
- 应用层级的状态应该集中到单个 store 对象中。
- 提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
- 异步逻辑都应该封装到 action 里面。
只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。
对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:
热重载
使用 webpack 的 Hot Module Replacement API,Vuex 支持在开发过程中热重载 mutation、module、action 和 getter。你也可以在 Browserify 中使用 browserify-hmr 插件。
对于 mutation 和模块,你需要使用 store.hotUpdate() 方法:
javascript
// store.js
import { createStore } from 'vuex'
import mutations from './mutations'
import moduleA from './modules/a'
const state = { ... }
const store = createStore({
state,
mutations,
modules: {
a: moduleA
}})
if (module.hot) {
// 使 action 和 mutation 成为可热重载模块
module.hot.accept(['./mutations', './modules/a'], () => {
// 获取更新后的模块
// 因为 babel 6 的模块编译格式问题,这里需要加上 `.default`
const newMutations = require('./mutations').default
const newModuleA = require('./modules/a').default
// 加载新模块
store.hotUpdate({
mutations: newMutations,
modules: {
a: newModuleA
}
})
})}
动态模块热重载
如果你仅使用模块,你可以使用 require.context 来动态地加载或热重载所有的模块。
javascript
// store.js
import { createStore } from 'vuex'
// 加载所有模块。function loadModules() {
const context = require.context("./modules", false, /([a-z_]+)\.js$/i)
const modules = context
.keys()
.map((key) => ({ key, name: key.match(/([a-z_]+)\.js$/i)[1] }))
.reduce(
(modules, { key, name }) => ({
...modules,
[name]: context(key).default
}),
{}
)
return { context, modules }}
const { context, modules } = loadModules()
const store = new createStore({
modules})
if (module.hot) {
// 在任何模块发生改变时进行热重载。
module.hot.accept(context.id, () => {
const { modules } = loadModules()
store.hotUpdate({
modules
})
})}
组合式api
可以通过调用 useStore 函数,来在 setup 钩子函数中访问 store。这与在组件中使用选项式 API 访问 this.$store 是等效的。
javascript
import { useStore } from 'vuex'
export default {
setup () {
const store = useStore()
}}
访问 State 和 Getter
为了访问 state 和 getter,需要创建 computed 引用以保留响应性,这与在选项式 API 中创建计算属性等效。
javascript
import { computed } from 'vue'import { useStore } from 'vuex'
export default {
setup () {
const store = useStore()
return {
// 在 computed 函数中访问 state
count: computed(() => store.state.count),
// 在 computed 函数中访问 getter
double: computed(() => store.getters.double)
}
}}
访问 Mutation 和 Action
要使用 mutation 和 action 时,只需要在 setup 钩子函数中调用 commit 和 dispatch 函数。
javascript
import { useStore } from 'vuex'
export default {
setup () {
const store = useStore()
return {
// 使用 mutation
increment: () => store.commit('increment'),
// 使用 action
asyncIncrement: () => store.dispatch('asyncIncrement')
}
}}