vuex:状态管理库
GitHub:GitHub - vuejs/vuex: 🗃️ Centralized State Management for Vue.js.
vuex 已更新到 v4.x 版本,v5 版本不会再出,官方推荐使用pinia
作为 vuex 的 v5 版本
非父子通信、状态管理:
1.State 通过:this.$store.state.共享状态属性,来访问共享状态属性
在计算属性中返回某个状态:
// 创建一个 Counter 组件
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count() {
return $store.state.count; // 返回状态,通过计算属性接收,然后在页面上使用
},
},
};
1.1 mapState
辅助函数:来访问共享状态 state 属性的第二种方法
一个组件需要获取多个状态,可以使用
mapState
辅助函数帮助我们生成计算属性,让你少按几次键:javascript// 在单独构建的版本中辅助函数为 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; }, }), };
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给
mapState
传一个字符串数组。javascriptcomputed: mapState([ // 映射 this.count 为 store.state.count 'count' ]) // 可以配合ES6展开运算符使用:可以对数组或对象进行展开,方便后面可以自定义属性合并 computed: { ...mapState(['count']), }
2.Getter 通过:getters: {} 定义 store 的计算方法:相当于 vue 的计算属性
const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
// 页面内可以通过this.$store.getters.doneTodos,访问
doneTodos (state, getters) {
console.log(getters.doneTodos.length) // 其他 getter
return state.todos.filter(todo => todo.done)
}
// 也可以通过让 getter 返回一个函数,来实现给 getter 传参
// 页面内可以通过this.$store.getters.getTodoById(2),访问
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
})
2.1 mapGetters
辅助函数:第二种调用 getters 属性的方法
mapGetters
辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:javascriptimport { mapGetters } from "vuex"; export default { // ... computed: { // 使用对象展开运算符将 getter 混入 computed 对象中 ...mapGetters([ "doneTodosCount", "anotherGetter", // ... ]), }, };
将一个 getter 属性另取一个名字,使用对象形式:
javascript...mapGetters({ // 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount` doneCount: 'doneTodosCount' })
3.Mutation 通过:mutations: {} 里面添加修改状态的函数方法
3.1 使用常量替代 Mutation 事件类型
使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码合作者对整个 app 包含的 mutation 一目了然:防止名字冲突、更方便修改
// mutation-types.js
export const SOME_MUTATION = "SOME_MUTATION";
// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'
const store = createStore({
state: { ... },
mutations: {
// 我们可以使用 ES2015(ES6) 风格的计算属性命名功能(对象的变量属性)
// 来使用一个常量作为函数名
[SOME_MUTATION] (state) {
// 修改 state
}
}
})
用不用常量取决于你——在需要多人协作的大型项目中,这会很有帮助。但如果你不喜欢,你完全可以不这样做。
4.Action 通过:action: {} 里面添加方法调用 mutations 里面的方法
const store = createStore({
// 定义共享状态
state: {
count: 0
},
// 修改状态
mutations: {
// 页面内通过:this.$store.commit('increment', data参数),普通风格
// 或this.$store.commit({ type: 'increment', amount: 10 }),对象风格触发
increment (state, data) {
state.count++
// state.count += data.amount // data参数的使用:两种风格都是这样使用
}
},
// 异步修改状态的方法(异步修改状态)
actions: {
// 页面内通过:this.$store.dispatch('increment', data参数),普通风格
// 或this.$store.dispatch({ type: 'increment', amount: 10 }),对象风格触发
increment (context) {
// 异步提交到mutations的方法
context.commit('increment')
}
// ** new Promise组合 Action
// 页面中可以:this.$store.dispatch('actionA').then(() => {...}),调用
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
}
)
}
}
})
3.1 mapMutations
辅助函数:在组件中提交 Mutation--第二种调用状态管理 Mutation 里的修改的方法
import { mapMutations } from "vuex";
export default {
// ...
methods: {
//
...mapMutations([
"increment", // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
// `mapMutations` 也支持载荷:
"incrementBy", // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
]),
// 调用另一个名字
...mapMutations({
add: "increment", // 将 `this.add()` 映射为 `this.$store.commit('increment')`
}),
},
};
4.1 mapActions
辅助函数:在组件中分发Action--第二种调用状态管理 action 里的异步修改的方法
import { mapActions } from "vuex";
export default {
// ...
methods: {
...mapActions([
// 将 `this.increment()`方法 映射为 `this.$store.dispatch('increment')`
"increment",
// `mapActions` 也支持载荷:
// 将 `this.incrementBy(amount)`方法 映射为 `this.$store.dispatch('incrementBy', amount)`
"incrementBy",
]),
// 调用另一个名字
...mapActions({
// 将 `this.add()`方法 映射为 `this.$store.dispatch('increment')`
add: "increment",
}),
},
};
4.2.通过:vue 的 Devtools 浏览器调试插件可以看到 mutations 的方法的调用情况和结果
5.通过:Module 来将 store 分割成模块,每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割
const moduleA = {
state: () => ({ ... }), // state的第二种写法:回调函数
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
// 命名空间的模块:当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名
namespaced: true,
// 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
state: () => ({ ... }),
getters: {
isAdmin () { ... } // 命名空间调用-> getters['moduleB/isAdmin']
// 在这个模块的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四个参数来调用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'moduleB/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
rootGetters['bar/someOtherGetter'] // 调用其他命名空间的-> 'bar/someOtherGetter'
},
},
mutations: {
login () { ... } // 命名空间调用-> commit('moduleB/login')
},
actions: {
login () { ... } // 命名空间调用-> dispatch('moduleB/login')
}
}
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
需要缓存的状态数据
1.为了不需要多次请求 tab 里的数据,减轻服务器压力。可以使用 vuex 的 actions 异步提交 mutations 把请求的数据缓存起来,在对应页面的时候,先判断是否有缓存数据,如果有,直接使用,没有再去请求
2.登录的信息,需要用到的登录用户的信息,后期要用这些参数来请求相关接口
Vuex 实现原理
Vuex 的原理关键:使用 Vue 实例管理状态
Vuex 装载分析
这个问题实际就是问到 Vuex 的 store 是如何装载到组件中的,首先利用了 Vue 的插件机制使用 Vue.use(Vuex)来去安装 Vuex 插件,那么此时会调用 vuex 的 install 方法,当调用 install 时此时会利用 mixin 机制在 beforeCreate 阶段去执行 vuexInit。核心源码如下:
Vue.mixin({ beforeCreate: vuexInit });
我们可以发现在 beforeCreate 阶段调用了 vuexInit 方法,我们分析一下 vuexInit 方法。
/**
* Vuex init hook, injected into each instances init hooks list.
*/
function vuexInit() {
const options = this.$options;
// store injection
if (options.store) {
this.$store =
typeof options.store === "function" ? options.store() : options.store;
} else if (options.parent && options.parent.$store) {
this.$store = options.parent.$store;
}
}
分析如上代码:
将初始化 Vue 根组件时传入的 store 设置到 this 对象的$store属性上,子组件从其父组件引用$store 属性,层层嵌套进行设置。
在任意组件中执行 this.$store 都能找到装载的那个 store 对象。
Vuex 中的 state 与 getter
vuex 的 Store 会划分出 state 和 getters 两个数据区。getter 是从 store 的 state 中派生出的状态。那么首先我们先看我们是如何访问 state 的?
get state () {
return this._vm._data.$$state
}
当我们使用 this.$store.state.xxx去获取xxx属性时,实际获取的是store挂载到_vm中store._vm.data.$$state 中的数据。
state 是如何挂载上去的?我们在 Store constructor 找到了核心函数 resetStoreVM,观察 resetStoreVM 的核心代码,其主要做的事情是初始化了一个 vue 实例_vm,由于 vue 的 data 是响应式的,所以,$$state 也是响应式的,那么当我们 在一个组件实例中 对 state.xxx 进行 更新时,基于 vue 的 data 的响应式机制,所有相关组件的 state.xxx 的值都会自动更新,视图自然也会自动更新,核心代码如下:
store._vm = new Vue({
data: {
$$state: state,
},
computed,
});
上面所介绍的是 state,那么接下来我们介绍一下 getter,其核心源码也是在
resetStoreVM 中,核心源码如下:
forEachValue(wrappedGetters, (fn, key) => {
// use computed to leverage its lazy-caching mechanism
// direct inline function use will lead to closure preserving oldVm.
// using partial to return function with only arguments preserved in closure environment.
computed[key] = partial(fn, store);
Object.defineProperty(store.getters, key, {
get: () => store._vm[key],
enumerable: true, // for local getters
});
});
until.js 的部分源码如下:
/**
* forEach for object
*/
export function forEachValue(obj, fn) {
Object.keys(obj).forEach((key) => fn(obj[key], key));
}
export function partial(fn, arg) {
return function () {
return fn(arg);
};
}
对 wrappedGetters 进行处理,让 getter 存储至 computed 对象上,对 getter 对象的属性进行数据劫持,当触发 get 时,返回store._vm[key]
,最后将 computed 挂载到 vue 实例上,当做计算属性。
store._vm = new Vue({
data: {
$$state: state,
},
computed,
});
Vuex 原理讲解
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vuex原理解析</title>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14"></script>
</head>
<body>
<div id="root1">{{data1}}</div>
<div id="root2">{{data2}}</div>
<div id="root3">
<button @click="change">change</button>
</div>
<script>
// 注册vue插件
function registerPlugin(Vue) {
const vuex = {};
vuex._vm = new Vue({
data: {
message: "hello vue.js",
},
});
// vuex赋值
vuex.state = vuex._vm;
vuex.mutations = {
setMessage(value) {
vuex.state.message = value;
},
};
// 初始化
function init() {
this.$store = vuex;
}
// 所有的vue实例beforeCreate时都调用init方法
Vue.mixin({
beforeCreate: init,
});
}
// 使用插件
Vue.use(registerPlugin);
// Vue实例化例子
new Vue({
el: "#root1",
computed: {
data1() {
return this.$store.state.message;
},
},
});
new Vue({
el: "#root2",
computed: {
data2() {
return this.$store.state.message;
},
},
});
new Vue({
el: "#root3",
methods: {
change() {
const newValue = this.$store.state.message + ".";
this.$store.mutations.setMessage(newValue);
},
},
});
</script>
</body>
</html>
总结
vuex 利用了 vue 的 mixin 机制,混合 beforeCreate 钩子 将 store 注入至 vue 组件实例上,并注册了 vuex store 的引用属性 $store。
vuex 的 state 是借助 vue 的响应式 data 实现的。getter 是借助 vue 的计算属性 computed 特性实现的。