开篇 🎉🎉🎉
- 这篇文章写了我很久,
Vuex
的绝大部份基础应该都囊括了,如果有什么问题或者哪里有不清晰的地方,请您指正。 - 希望我的这篇文章对你有帮助哦~
什么是vuex?⚡
- Vuex 是专门为 Vue.js 设计的状态管理库。
- 它提供了一种集中式存储(centralized store) ,可以在整个应用中使用它来存储和维护全局状态。
- 它同时还使你能够对存入的数据进行校验,以保证当这个数据再次被取出时是可预见而且正确的。
- 官方是这样说的
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
为什么需要vuex🍶
- 接下来说的两点会有交叉,但换了切入点来思考这个问题。
1. 通俗来说:“能够满足复杂组件中多个组件进行状态共享的需求。”
- 举一个实际中应用的例子
- 需求:
- 我们现在需要做一个社交网络中的消息部分。
- 在应用的顶部导航栏上放一个图标,用来显示收到的消息数量。
- 在应用的页面底部,还想要一个消息弹窗,同样是告诉你收到的信息消息。
- 分析
- 方法一
- 因为图标和弹窗着两个组件彼此在页面上并无直接联系,所以用events和props来连接它们会很困难。因为他们并没有父子组件的这种关系,所以需要使用一个额外组件来实现兄弟间传值。
- 问题:可以解决,但是写法很不好,会使项目结构变得复杂,写成屎山。
- 方法二
- 不通过连接两个组件的方式来共享数据,而是每个组件各自发送API请求。
- 问题:不同组件在不同的时间节点更新,这意味着它们会渲染不一样的数据,并且页面所发送的API请求也会远远超过其实际所需。
- 方法一
- 解决方法:
- 把信息的数据源剥离出来用一个全局变量或者全局单例的模式进行管理,这样就可以在导航和弹窗都得到这个消息。「Vuex的思想」
2. Vue
中的单向数据流🌴
通过一个例子来理解状态管理模式
const Counter = {
// 状态
data () {
return {
count: 0
}
},
// 视图
template: `<div>{{ count }}</div>`,
// 操作
methods: {
increment () {
this.count++
}
}
}
createApp(Counter).mount('#app')
这个状态自管理应用包含以下几个部分:
- 状态,驱动应用的数据源;
- 视图,以声明方式将状态映射到视图;
- 操作,响应在视图上的用户输入导致的状态变化。
实现了一个简单的计数器,状态在
data
里面,视图在template
里面,操作在methods
里面
以下是一个表示“单向数据流”理念的简单示意图:
但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
- 多个视图依赖于同一状态。
- 来自不同视图的行为需要变更同一状态。
对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。
以上的这些模式非常脆弱,通常会导致无法维护的代码。
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?
在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
上例的 Vuex
demo
// store/index.js
import Vuex from 'vuex';
export default new Vuex.Store({
// 状态
state;{
count: 0,
},
mutations: {
addNumber(state) {
state.count += 1;
},
}
});
// 组件
const Counter = {
// 状态
data () {
},
// 视图
template: `<div>{{ count }}</div>`,
// 操作
mounted() {
this.$store.dispatch('addNumber')
}
}
createApp(Counter).mount('#app')
什么时候使用vuex?🍧
- 如果你的页面不是特别复杂的
SPA
「单页应用程序」,代码量比较小,那就没必要使用Vuex
,使用一个简单状态管理就OK
了但是当你的代码量很大,或者说有多个地方需要共享数据,那么就需要使用Vuex
。
vuex使用流程图
看不懂没关系,继续往下看看。
安装🗡
CDN
<script src="https://unpkg.com/vuex"></script>
npm
npm install --save vuex
webpack
- 如果使用webpack类似的打包工具,就要调用Vue.use()
实例
- 创建
store
文件夹,并且创建index.js
文件,导出所需内容。
// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(vuex);
export default new Vuex.Store({
state:{}
});
- 在Vue实例化时作为一个属性引入。
// main.js
import Vue from 'vue';
import store from './store';
new Vue({
el:'#app',
store,
component: {
App
}
});
State⭐
State是什么?
- State表示数据在vuex中的存储状态,它就像一个在应用的任何角落都能够访问的庞大对象---是的,它就是单一数据源(single source of truth)。
在Vue组件中获得Vuex状态
由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性中返回某个状态
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count() {
return store.state.count
}
}
}
- 但是这种
store.state.count
的写法会在每个需要使用state
的组件中频繁的导入store
,然后访问store
上的state
属性。 - Vuex 通过 Vue 的插件系统将 store 实例从根组件中“注入”到所有的子组件里。且子组件能通过
this.$store
访问到。
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count() {
return this.$store.state.count
}
}
}
State辅助函数
- 当引用
store
属性不多时,直接在计算属性中访问store
是没问题的,但是当你大量引用store
属性时,多次引用就会变得很重复。 - 鉴于此,
vuex
提供了一个辅助函数mapState
,它返回一个用作计算属性的函数对象。
// 在单独构建的版本中辅助函数为 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
}
})
}
mapState
函数以第一个对象作为参数,并将其中的各个键值分别映射到一个计算属性。如果键值给定的是函数,则该函数会以
state
作为其第一个参数被调用,从而使你能够从这个参数上获得state
的值。
- 当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给
mapState
传一个字符串数组。
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])
对象展开运算符
computed: {
doubleFoo() {
return this.foo * 2;
},
...mapState({
messageCount: (state) => state.message.length,
somethingElse: 'somethingElse'
})
}
展开之后得到
computed: {
doubleFoo() {
return this.foo * 2;
},
messageCount() {
return this.$store.state.message.length;
},
somethingElse() {
return this.$store.somethingElse;
}
}
Getter✈️
- 有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数:
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
如果我们在组件中进行操作,需要有多个通过
state
中某一个值派生出来的值,要么复制state
中的那个值,要么直接使用那个值,这就会带来代码的冗余和数据之间的相互影响。
总结:不够优雅!
幸运的是,
vuex
为我们提供了getters
属性,它使我们能够将通常被重复使用的代码移动到vuex store
的内部,从而避免产生冗余。
Vuex
允许我们在store
中定义“getters
”(可以认为是store
的计算属性)。
Getter 接受 state 作为其第一个参数:
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 也可以接受其他 getter 作为第二个参数:
getters: {
// ...
doneTodosCount (state, getters) {
return getters.doneTodos.length
}
}
通过属性访问
我们可以类比state
的访问方式。
- 第一种是导入
store
,然后访问store
上的getters
属性。
store.getters.doneTodos
- 第二种就是通过
this.$store
访问。
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
通过方法访问
- 返回一个函数,这里可以给
getter
传参,从而达到你想要的效果。
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
Getters辅助函数
Attention !
Getters不支持函数写法
- 数组写法
computed: mapGetters(['unread','unreadForm'])
- 等效于
computed: {
unread() {
return this.$stores.getters.unread;
},
unreadForm() {
return this.$stores.getters.unreadForm;
}
}
- 对象写法
computed: ({
unreadMessage: 'unread',
unreadMessageForm: 'unreadFrom'
})
- 对象展开写法
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
如果你想将一个 getter 属性另取一个名字,使用对象形式:
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
类比State
和Getters
其实State
和Getters
都是存储的数据,只是State
就像data
一样,而Getters
就像计算属性一样,在组件中获得这些值的时候,都是通过computed
获取的。
Mutation🚤
如何对数据进行修改~
提交Mutation
是Vuex
中唯一的修改状态的方法
Vuex
中的mutation非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个 回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
Show me the Code !
// store/index.js
import Vuex from 'vuex';
export default new Vuex.Store({
state: {
message: []
},
mutations: {
addMessage(state,newMessage){
state.message.push(newMessage);
}
}
})
// component
const SendMessage = {
template: '<form @submit="handleSubmit"></form>',
data: () => ({
formData: {...}
}),
methods: {
handleSubmit() {
this.$store.commit('sendMessage',this.formData);
}
/*
handleSubmit中的方法也可以写成=>另一种对象风格的提交方式
this.$store.commit({
type: 'addMessage',
newMessage: this.formData
})
*/
}
}
- 注意:
mutations
可以传第二个参数「如上述Code」,这个参数叫做提交载荷(payload)
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的
mutation
会更易读
Mutation必须是同步函数
- 什么是同步函数?
同步行为:对应内存中执行顺序的处理器指令。每条指令都会严格按照它们出现的顺序来执行。「from
红宝书」
- 当只有一个窗口的时候,排在后面的人必须等待前面的人完成购票,自己才能购票,这就是 同步。
- 为什么必须是同步函数?
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
我们注意上面这段代码,在mutation里面加入了异步处理的函数。
其实mutation是可以正常使用的,但是我们在日常的开发中debug的时候,我们需要查看devtool中的mutation日志。
理论上来说,是mutation走一步,devtool记录一步,但是在mutation中加入异步函数就会导致我们devtool的记录失败,因为devtool不知道你里面的异步函数什么时候调用,在哪里调用
在组件中提交Mutation
- 你可以在组件中使用
this.$store.commit('xxx')
提交 mutation。 - 使用
mapMutations
辅助函数将组件中的 methods 映射为store.commit
调用(需要在根节点注入store
)。
import { mapMutations } from 'vuex';
export default {
// ...
methods: {
...mapMutations([
'increment',
// 'this.increment' => 'this.$store.commit('increment')''
'incrementBy',
// 'this.increment(amount)' => 'this.$store.commit('increment',amount)'
]),
// 函数式
...mapState({
add: 'increment' // 将 this.add() => this.$store.commit('increment')
})
}
}
Action🎂🎂🎂
Action
应该会更多的被使用,我这里尽量写的详细一些,多举几个例子供大家理解。
Action
与Mutation
的区别
- 用
mutation
只能做到同步变更,而action
则用于实现异步变更 - Action 提交的是 mutation,而不是直接变更状态。
注册一个action
- 例子
这个
action
会使用fetch API
来向服务器发送请求,以检查是否有新的消息,然后再把新消息追加到message
数组的结尾。
// #demo
import Vuex from 'vuex';
export default new Vuex.Store({
state: {
messages: []
},
mutations: {
addMessage(state, newMessage){
state.messages.push(newMessage);
},
addMessages(state,newMessages){
state.messages.push(...newMessages);
}
},
actions: {
// 检查服务器上的新消息,并且如果存在,则调用addMessages并传入这些新消息。
getMessage(context) {
fetch('/api/new-messages')
.then((res) => res.json())
.then((data) => {
if (data.message.length) {
context.commit('addMessages',data.messages);
}
});
}
}
});
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用
context.commit
提交一个 mutation,或者通过context.state
和context.getters
来获取 state 和 getters。
- 参数解构
actions: {
addMessages({ commit }){
commit('addMessages',data.messages)
}
}
分发Action
这里与上面的demo对应
const Notification = {
template: `<p>Message: {{ message.length }}
<a @click.prevent = "handleUpdate">(Update)</a>
</p>`,
// 得到数据
computed: mapState(['message']),
// 分发 Action 方法
methods: {
handleUpdate() {
this.$store.dispatch('getMessages');
}
}
};
现在,单击链接将触发如下流程
- 链接被单击,触发
handleUpdate
方法。 - 相应的
getMessages
被分发(dispatch
)。 - 相关的请求被发送往
/api/new-messages
- 请求获得响应时,如果存在新消息,则调用addMessages并将这些新消息作为其payload参数。
addMessages
将这些新消息追加到state
的messages
属性里。- 由于
state
发生了变化,计算属性messages
就会更新,从而页面上的文本也会相应改变。
再来看一个购物车案例
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)
)
}
}
Promise 与 Action💰
由于
Promise
和Action
有一腿,我们在看完了前面的例子之后,这里需要单独聊一聊。
- Promise出现的原因?
action
是异步函数,我们不知道什么时候它执行完成了,可以观察计算属性的改变,但是这样不够理想。
解决方法:返回一个Promise
对象来代替上述做法。
- 调用
dispatch
也会返回一个promise
对象,运用它就可以在action
运行结束时去运行其他代码。
- 还是用前面
Action
中的#demo
案例
// #demo
import Vuex from 'vuex';
export default new Vuex.Store({
state: {
messages: []
},
mutations: {
addMessage(state, newMessage){
state.messages.push(newMessage);
},
addMessages(state,newMessages){
state.messages.push(...newMessages);
}
},
actions: {
// 检查服务器上的新消息,并且如果存在,则调用addMessages并传入这些新消息。
getMessage(context) {
/////////////////////// Attention !
// 在这里加了一个return
return fetch('/api/new-messages')
.then((res) => res.json())
.then((data) => {
if (data.message.length) {
context.commit('addMessages',data.messages);
}
});
}
}
});
// 分发的组件
const Notification = {
template: `<p>Message: {{ message.length }}
<span v-if="loading">(updating)</span>
<a v-else.prevent = "handleUpdate">(Update)</a>
</p>`,
// 得到数据
computed: mapState(['message']),
// 分发 Action 方法
data() {
return {
updating: false,
}
},
methods: {
handleUpdate() {
this.updating = true;
this.$store.dispatch('getMessages')
.then(() => {
this.updating = false;
});
}
}
};
Module👑
胜利就在前方,
Last Part!
module
=> 如何组织store
每个
module
都是一个对象,并且拥有其自身的state
,getter
,mutation
,以及action
,通过使用modules
属性即将他们添加到store
中。
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module) 。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
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来管理复杂应用中的状态,以及vuex中各种各样的概念:
vuex store
是一切事物——state
、getter
、mutation
和action
——被存储和访问的必由之所。state
是应用中所有数据存放的对象。getter
使你能够将通用的逻辑聚合起来,以获取store
中的数据。mutation
用于同步变更store
中的数据。action
用于异步变更store
中的数据。state
、getter
、mutation
和action
都有其各自的辅助函数,用于协助你将它们加入组件当中,它们分别是mapState
、mapGetters
、mapMutations
和mapActions
。- 最后,还看到了使用模块来切分
vuex store
,使其成为一个个包含各自逻辑的代码块。
END...
感谢你的阅读~
如果这篇文章对你有帮助,可以点个赞再走嘛✨✨✨