官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。
笔者个人理解,一句即可以概括: Vuex 是用于管理 Vue 应用跨组件数据的工具。
跨组件数据指的是,在 A、B、C 组件都需要用到的数据,比如购物车的数量,在很多页面是需要用到的。
说起这个还真有很多同学对 Vuex 的存储概念比较模糊,那么下面我们来分析一下 Vuex 和 localStorage、sessionStorage 在存储上的区别。
Vuex 存储在浏览器内存,它采用的是集中式存储管理应用的所有组件的状态,在不刷新网页的情况下,状态会一直保持,一旦刷新网页,所有状态都将会重制。
sessionStorage 是一种会话型存储,用于保存同一窗口或标签页的数据,数据保存在浏览器本地,在关闭窗口或标签页之后将会删除这些数据,这就是会话型存储,就跟人于人说话一样,人走了对话就结束了。
localStorage 是一种持久性存储,与 sessionStorage 的功能近乎相似,但是在数据的存储时长上有所区别。它可以让数据一直存在于浏览器本地,除非你主动的 clear 数据,或者重装浏览器。
很多同学会认为既然 localStorage 存储时效这么强大,为什么不能用它去代替 Vuex 管理应用的数据呢?当然,在某些场景下,数据存在 localStorage 是比较合适的,像一些不需要变化的数据。但是在 Vue 单页应用开发中,两个组件 A 和 B 共用一份数据,B 若是能响应 A 对数据的改动,这种情况下 localStorage 和 sessionStorage 就显得比较乏力,毕竟 Vuex 有一整套高度兼容 Vue.js 开发模式的结构。
什么是单向数据流呢?它指的是通过一定的规则去改变数据,数据触发视图的更新,通过视图中的方法去触发数据的更新,形成一个闭环。如下图所示:
但事与愿违,复杂应用里会遇到多个组件共享同状态,不同视图的行为变更同一个状态等等问题,这时单向数据流就会被破坏。
Vuex 的出现就是为了解决这类复杂场景应用,那么我们在看看一张官方提供的流程图:
绿色虚线框内被 Vuex 加持,Vue 组件通过 Dispatch 关键字触发 Actions,再通过 Commit 调用 Mutation 里的方法修改 State 数据,Vue 组件若是有依赖 Store 里的数据,那么便会触发 Vue
组件的 Render 重绘,这就又形成了一个闭环。
顾名思义,所有状态都将被存放在 State 中,类似 Vue 组件中的 data 属性,只不过 State 是面向整个应用,而 data 针对的是单个组件。在 Vue 入口页构造 Vue 实例的时候引入 store 之后,可以在组件中通过
this.$store.state 拿到。
它类似于 Vue 组件中的 computed 计算属性,计算一些需要二次改造的数据。举个例子,我在 Vue 组件中通过 this.$store.state 拿到 State 中的某个数据,但是我需要对这个数据过滤,一个 filter
方法就能拿到过滤后的数据,但是我在很多地方都要使用这个 filter 过滤条件,那么我不断的去复制粘贴(CV 大法),或者将这个 filter 方法抽离到公用函数再引入组件,两种方法都很鸡肋。Getter 为我们解决了这个难题,你可以在
store 中定义 getter 属性,state 数据可作为参数被传入,代码大致如下:
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false },
],
},
getters: {
doneTodos: (state) => {
return state.todos.filter((todo) => todo.done);
},
},
});
在 Vue 组件中便可以通过如下方式进行访问:
this.$store.getters.doneTodos; // [{ id: 1, text: '...', done: true }]
Getter 也可以接收其他的 getters 作为第二个参数:
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length;
};
}
// 使用
store.getters.doneTodosCount; // -> 1
可以通过 mapGetter 将 store 中的 getter 属性映射到局部计算属性内:
import { mapGetters } from 'vuex';
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
]),
},
};
在 Vue 组件中便可以直接用 this.doneTodosCount 取到你想要的过滤后的数据。
我们修改 State 状态,需要触发一些方法,这些方法就放在 mutations 属性中,mutations 属性中的方法接受 2 个参数,第一个参数是
state,内部包含所有状态值。第二个参数为提交载荷(Playload),也就是在外部通过 store.commit 方法触发 mutations 时额外带入的值。代码如下所示:
const store = new Vuex.Store({
state: {
count: 1,
},
mutations: {
increment(state) {
// 变更状态
state.count++;
},
},
});
// 触发
store.commit('increment'); // 参数名称必须对应 mutations 的方法属性值。
//
const store = new Vuex.Store({
state: {
count: 1,
},
mutations: {
increment(state, n) {
state.count += n;
},
},
});
// 带载荷触发
store.commit('increment', 10);
其实 Action 很好理解,它与 Mutation 类似,只不过 Action 是提交 mutation 而不是直接改变状态,并且 Action 被赋予异步的能力,也就是能在里面请求异步数据之后再触发状态的更新。
简单代码演示:
const store = new Vuex.Store({
state: {
count: 0,
},
mutations: {
increment(state, data) {
state.count += data.length;
},
},
actions: {
async increment(ctx) {
const data = await getData();
ctx.commit('increment', data);
},
},
});
// 分发 Action
store.dispatch('increment');
mapActions 帮我们更好的在页面中分发 Actions(在此之前需要在入口页注入 store):
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')`
}),
},
};
它能帮助我们在页面中减少不少代码量。更复杂的操作可以查看 官方页面。
我们想要给状态分模块管理,而不是将所有的状态一股脑的都放在一个 state 中导致状态过于臃肿,Module 就能帮我们办到。具体怎么实现呢?代码如下:
// 模块 A 的状态及触发更新的方法
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
// 模块 B 的状态及触发更新的方法
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
// Vuex 为我们提供了 modules 方法,可以将 store 分割成模块,每个模块都有属于自己的 state、getter
// mutation、action
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
模块内部管理这自己的 state,比如 mutation 接收的第一个参数是该模块的局部状态对象。
const moduleA = {
state: { count: 0 },
mutations: {
increment(state) {
// 这里的 `state` 对象是模块的局部状态
state.count++;
},
},
};
下面我们通过一个小实例讲 Vuex 的使用方法运用到实践当中,以便我们更好的巩固知识点。我们还是通过 CDN 的形式引入 Vue.js 和 Vuex,代码如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
<script src="https://cdn.bootcss.com/vuex/3.1.3/vuex.min.js"></script>
<title>Vuex</title>
</head>
<body>
<div id="app">
<div>{{ $store.state.count }}</div>
<button @click="add">加1</button>
<button @click="dec">减1</button>
<div>
<script>
const store = new Vuex.Store({
state: {
count: 0
},
// 开启严格模式,开启严格模式后,必须通过 mutation 来修改状态
strict: true,
// 触发 state 中的 count 加减运算的方法
mutations: {
add(state) {
state.count += 1
},
dec(state) {
state.count -= 1
}
},
getters: {
// 过滤偶数的 getter
filterEven: state => {
return !(state.count % 2)
}
}
})
const app = new Vue({
el: '#app',
store, // 将 Vuex 生成的实例作为 Vue 生成实例的参数
data: {
message: 'Hello Vue!'
},
methods: {
add() {
// 触发加法
this.$store.commit('add')
},
dec() {
// 触发减法
this.$store.commit('dec')
}
}
}).$mount('#app');
</script>
</body>
</html>
需要注意的是,静态资源的引入,Vuex 要在 Vue 的后面,因为 Vuex 内部是依赖 Vue,否则浏览器会报错。通过 Vuex.Store 生成实例建议使用 strict: true 严格模式,减少多人员参与项目时代码的紊乱。
上述代码执行后效果如下:
真实环境中 可能有些数据是通过异步来回去的,所以在这为想通过请求的方式来改变 state 中的数据,代码如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
<script src="https://cdn.bootcss.com/vuex/3.1.3/vuex.min.js"></script>
<title>Vuex</title>
</head>
<body>
<div id="app">
<div>{{ $store.state.count }}</div>
<button @click="asyncAdd">异步加1</button>
<button @click="asyncDec">异步减1</button>
<div>
<script>
// 模拟请求数据,延迟 2 秒返回数据
function AsyncData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 2000)
})
}
const store = new Vuex.Store({
state: {
count: 0
},
// 开启严格模式,开启严格模式后,必须通过 mutation 来修改状态。
strict: true,
mutations: {
add(state, num) {
state.count += num
},
dec(state, num) {
state.count -= num
}
},
actions: {
async add(ctx) {
const num = await AsyncData()
console.log('执行action:add')
ctx.commit('add', num)
},
async dec(ctx) {
const num = await AsyncData()
console.log('执行action:dec')
ctx.commit('dec', num)
}
},
getters: {
filterOdd: state => {
return !(state.count % 2)
}
}
})
const app = new Vue({
el: '#app',
store,
data: {
message: 'Hello Vue!'
},
methods: {
asyncAdd() {
this.$store.dispatch('add')
},
asyncDec() {
this.$store.dispatch('dec')
}
}
}).$mount('#app');
</script>
</body>
</html>
AsyncData 函数模拟接口请求延迟 2 秒返回要加的数据,要注意的是笔者在 actions 方法内执行了异步操作使用的是 async、await,低版本的浏览器还不能很好的支持它们,谷歌、火狐、360 极速等浏览器基本上已经支持。触发 actions 中的方法,需要在组件中调用的是 dispatch() 方法,要注意和 mutations 区分开。我们来看看浏览器的表现情况:
注意 Vuex 中异步的请求操作都需要放在 actions 中。
Vuex 可以帮助我们管理好共享状态,但是也不是所有的应用都要用上 Vuex,需要对项目的短期和长期效益进行权衡,如果不是开发大型的单页应用,就不必使用 Vuex,因为这样会让项目变得繁琐冗余,简单的小应用可以通过结合 localStorage 封装一个简单的状态管理插件。