我们可以利用 Vuex 实现状态共享,那么,它是怎么实现的呢?
创建项目
vue create vuex-demo
// Vue CLI v4.5.13
// ? Please pick a preset: Manually select features
// ? Check the features needed for your project: Choose Vue version, Babel, Vuex
// ? Choose a version of Vue.js that you want to start the project with 2.x
// ? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
// ? Save this as a preset for future projects? No
Vue 入口文件注入 Vuex 方式
main.js 根组件注入了 store
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
store, // 目的是所有组件都能共享到这个 store,所以从根组件注入
render: h => h(App)
}).$mount('#app')
为什么不直接挂载到 Vue.prototype 上呢?因为啊,我的 vue 可能有多个实例,如果都挂在原型,那不是乱套了嘛!
Vuex 中的模块
模块依赖图
store.js 改造
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex) // 说明 vuex 有 install 函数
export default new Vuex.Store({ // 说明 vuex 有一个 Store 类
strict: true, // 开启严格模式
state: { // 共享状态。类似组件的 data,只不过这个是全局的
age: 18
},
getters: { // 计算属性,类似组件的 computed,全局的
getAge(state) {
return state.age - 1;
}
},
mutations: { // 同步更新状态,mutations 是唯一更改状态的方法(严格模式下只能 mutations 更改)
changeAge(state, payload) {
state.age += payload;
}
},
actions: {
changeAgeAsync({ commit }, payload) {
return new Promise((resolve) => {
setTimeout(() => {
commit('changeAge', payload);
resolve('成功啦');
}, 1000);
});
}
},
modules: {
}
})
页面使用
App.vue
<template>
<div id="app">
我的年龄是:{{ $store.state.age }}
我弟弟的年龄是:{{ $store.getters.getAge }}
<button @click="$store.commit('changeAge', 10)">同步修改</button>
<button @click="$store.dispatch('changeAgeAsync', 10)">异步修改</button>
<button @click="$store.state.age++">恶意直接修改(严格模式下会报错)</button>
</div>
</template>
mutations 和 actions 的区别
比如有场景:
- A 页面有个获取列表的操作,啪啪啪一顿请求接口后,store.commit('changeAge', newVal) 触发了一次 mutations 更新
- B 页面也有一个获取列表的操作,又啪啪啪一顿请求接口后,store.commit('changeAge', newVal) 触发了一次 mutations 更新 在两个组件都进行了这顿啪啪啪的请求接口,,我希望提取出来,于是我使用 actions 封装了请求的方法,在 A,B 页面的更新方式变为
store.dispatch('changeAge', newVal);
可以看到,**mutations 主要用于同步更新状态,使用 commit 触发,而 actions 主要用于异步更新状态,最后还是使用 mutations 更新数据,使用 dispatch 触发,需要注意的是,mutations 中写异步代码也能更新数据,但是在 使用 devtools 调试的时候会有问题,页面数据更新了,调试时的数据还是原来的,为了解决这个问题,才有了 actions。**看以下场景,体会 actions 的优势~
module 和 namespaced
我们可以使用 module 来分割模块间的配置和变量,但是如果父子模块拥有同样的的 mutations,比如说 changeAge,那么当我 commit('changeAge') 时,会同时触发父子模块中所有 changeAge 操作(被收集为一个数组),解决方法是增加命名空间,它会给每个模块的 changeAge 前拼接模块路径,比如 /a/changeAge。
Vuex plugin 实现持久化存储
vuex 有个比较严重的问题,它不能进行持久化存储能力,刷新页面状态就会重置~ 我们可以利用 vuex 的插件能力,通过 store.subscribe 实现状态收集,通过 store.replaceState 实现状态的替换,比如下面做一个持久化存储的插件。
const plugin = () => {
return store => { // 每次刷新初始化 vuex 都会走这里
let preState = JSON.parse(localStorage.getItem('@@VUEX_STATE')); // 取缓存的值
if (preState) {
store.replaceState(preState); // 取到则替换默认值
}
store.subscribe((type, state) => { // 每次操作都会触发 subscribe 回调
// type: 触发的 mutations 事件名和传递的值的,每次修改都会触发
// state: 更新后的 state
console.log(type, state);
localStorage.setItem('@@VUEX_STATE', JSON.stringify(state));
});
}
}
export default new Vuex.Store({
plugins: [plugin()],
strict: true,
state: {
age: 18
},
getters: {
getAge(state) {
return state.age - 1;
}
},
mutations: {
changeAge(state, payload) {
state.age += payload;
}
},
actions: {
}
})
实现 Vuex
我们从上面的使用中可以看出来,实现 Vuex 需要以下几步:
- 需要先实现一个 Vuex 插件(可以 use),里面包含用户注入根组件使用的 Store 和 install 函数,将用户注入的 store 变为每个组件的 store 调用)
- 实现 state,getters,actions,mutations
实现代码
Vuex 入口文件 src/ys-vuex/index.js
- 提供 Store 类,保存着用户状态树,mutations 树和 actions 树,当然还有用户的 getter 方法~
- 提供装载子级树的方法 installModule,并使用命名空间+key作为 getters,mutations,actions 的方法名。
- 提供 setStoreVM 方法,该方法是 vuex 的核心,它声明了一个 vue 实例挂载到自己 store._vm 上,并且页面取 vuex 值会从被代理到 store._vm.xxx,以此触发依赖收集和视图更新,而 Vuex 的getter 也被挂载为 vue 的 computed 方法,以此利用 vue 的计算属性,实现取值缓存和动态更新,可以看到,vuex 是借助了 vue 的核心能力的。
- 提供 vuex 初始化方法 install,该方法会使用用户传递来的 Vue 类(不写死,避免版本冲突),然后通过 Vue.mixin 注入 beforeCreate 方法,将根组件注册在 this.store,以便组件模板内直接使用 $store 变量。
code
import { forEachValue } from "./utils";
import ModuleCollection from "./module/module-collection";
let Vue;
// root = {
// _raw:默认显示用户原始的内容
// _children:{
// a:{ _raw: a模块的原始内容, _children:{},state:aState},
// a:{ _raw: b模块的原始内容, _children:{},state:bState}
// },
// state:'根模块的状态'
// }
class Store {
constructor(options) {
const store = this;
// 1. 对用户的数据进行格式化操作
this._module = new ModuleCollection(options);
// 2. 收集所有模块的 action 和 mutation、getters
this._actions = {}; // 收集用户定义的所有 actions
this._mutations = {}; // 收集用户定义的所有 mutations
this._wrappedGetters = {}; // 收集用户定义的所有 getter
let state = options.state; // 根状态
// 从根递归装载每个模块内 store 的 _actions,_mutations,_wrappedGetters
installModule(store, [], store._module.root, state);
// 给 store 增加 _vm「_vm 是一个 vue 实例,我们使用了 vue 的数据劫持和计算属性」
setStoreVM(store, state);
console.warn('整体的 Store', this);
}
commit = (type, payload) => { // this永远指向store容器
console.log(this._mutations, type);
this._mutations[type].forEach(fn => fn(payload))
}
dispatch = (type, payload) => { // this永远指向store容器
this._actions[type].forEach(fn => fn(payload))
}
get state() {
return this._vm._data.$$state // 响应式数据
}
}
/**
*
* @param {*} store store属性
* @param {*} path 构造的递归栈结构
* @param {*} module 当前安装的模块
*/
const installModule = (store, path, module, rootState) => { // 注册所有的getter,mutation,actions
// 计算当前的命名空间,在订阅的时候,每个 key 前面都增加一个命名空间
// root -> 从根开始查找 path 路径 ['a', 'c'] 如果都有 namespaced,拼成 a/c
let namespaced = store._module.getNamespace(path); // 从模块开始进行 nameSpace 路径拼接
console.error(namespaced);
if (path.length > 0) { // 循环的是子模块 [a]
// 给rootState 添加模块的属性
let parent = path.slice(0, -1).reduce((memo, current) => memo[current], rootState)
// 后续数据可能是动态注册的模块,如果原来没有属性,新增了不会导致视图更新
// 这一步是把子模块的名字定义在父亲上,以便$store.state.a.b.c.xxx能取到root.a.b.c模块的值
Vue.set(parent, path[path.length - 1], module.state)
}
module.forEachMutation((mutation, key) => {
store._mutations[namespaced + key] = (store._mutations[namespaced + key] || [])
store._mutations[namespaced + key].push((payload) => {
mutation(module.state, payload); // mutation执行
})
});
module.forEachAction((action, key) => {
store._actions[namespaced + key] = (store._actions[namespaced + key] || [])
store._actions[namespaced + key].push((payload) => {
action(store, payload); // action执行
})
});
module.forEachGetter((getter, key) => {
store._wrappedGetters[namespaced + key] = function () {
return getter(module.state); // 计算属性执行
}
});
console.log(store._actions);
module.forEachChildren((child, key) => { // 递归
installModule(store, path.concat(key), child, rootState);
})
}
function setStoreVM(store, state) {
let computed = {};
store.getters = {};
forEachValue(store._wrappedGetters, (fn, key) => {
computed[key] = () => {
return fn(store.state);
}
Object.defineProperty(store.getters, key, {
get: () => store._vm[key]
})
})
store._vm = new Vue({
data: {
$$state: state
},
computed
})
}
export default {
Store,
install(_vue) {
Vue = _vue;
// Vue.prototype.$store 比较暴力 全部都添加了,我只希望覆盖到自己的子组件即可
Vue.mixin({
// 实现所有的组件能共享store属性
beforeCreate() {
const options = this.$options;
if (options.store) { // 根组件
this.$store = options.store; // 根组件上有一个$store属性
} else { // 要么是子组件要么是没有注册store选项的另一个根组件 (排除掉平级组件)
if (this.$parent && this.$parent.$store) {
this.$store = this.$parent.$store
}
}
}
})
}
}
遍历的辅助方法 src/ys-vuex/utils/index.js
code
export const forEachValue = (obj, callback) => {
Object.keys(obj).forEach(key => {
callback(obj[key], key);
})
}
模块树生成方法 src/ys-vuex/module/module-collection.js
code
export const forEachValue = (obj, callback) => {
Object.keys(obj).forEach(key => {
callback(obj[key], key);
})
}
模块初始化方法 src/ys-vuex/module/module.js
code
import { forEachValue } from "../utils";
export default class Module {
constructor(rootModule) {
this._raw = rootModule;
this._children = {}
this.state = rootModule.state
}
get namespaced() {
return !!this._raw.namespaced; // 用于标识这个模块是否有 namespaced 属性
}
getChild(key) {
return this._children[key]
}
addChild(key, module) {
this._children[key] = module;
}
forEachMutation(callback) {
if (this._raw.mutations) {
forEachValue(this._raw.mutations, callback)
}
}
forEachAction(callback) {
if (this._raw.actions) {
forEachValue(this._raw.actions, callback)
}
}
forEachGetter(callback) {
if (this._raw.getters) {
forEachValue(this._raw.getters, callback)
}
}
forEachChildren(callback) {
if (this._children) {
forEachValue(this._children, callback)
}
}
}
测试文件
src/main.js
code
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
store, // 目的是所有组件都能共享到这个 store,所以从根组件注入
render: h => h(App)
}).$mount('#app')
src/store/index.js
code
import Vue from 'vue'
// import Vuex from 'vuex'
import Vuex from '@/ys-vuex';
import moduleA from './modules/module-a';
import moduleB from './modules/module-b';
Vue.use(Vuex) // 说明 vuex 有 install 函数
export default new Vuex.Store({ // 说明 vuex 有一个 Store 类
strict: true, // 开启严格模式
state: { // 共享状态。类似组件的 data,只不过这个是全局的
age: 18
},
getters: { // 计算属性,类似组件的 computed,全局的
getAge(state) {
return state.age - 1;
}
},
mutations: { // 同步更新状态,mutations 是唯一更改状态的方法(严格模式下只能 mutations 更改)
changeAge(state, payload) {
state.age += payload;
}
},
actions: {
changeAgeAsync({ commit }, payload) {
return new Promise((resolve) => {
setTimeout(() => {
commit('changeAge', payload);
resolve('成功啦');
}, 1000);
});
}
},
modules: {
a: moduleA,
b: moduleB
}
})
src/store/modules/module-a.js
code
// 加了 namespaced 之后,想修改触发本模块下 changeAge,需要 commit('a/changeAge', newVal);
// 特殊情况,比如没有 a 命名空间,但是有 c 的命名空间,那么页面就直接 commit('c/changeAge', newVal) 即可
// 不过取值依旧要 $state.a.c.cAge
export default {
namespaced: true,
state: {
aAge: 200
},
mutations: {
changeAge(state, payload) {
state.aAge += payload;
}
},
modules: {
c: {
namespaced: true, // commit('a/c/changeAge', newVal);
state: {
cAge: 400
},
mutations: {
changeAge(state, payload) {
state.cAge += payload;
}
}
}
}
}
src/store/modules/module-b.js
code
// 加了 namespaced 之后,想修改触发本模块下 changeAge,需要 commit('b/changeAge', newVal);
export default {
namespaced: true,
state: {
bAge: 300
},
mutations: {
changeAge(state, payload) {
state.bAge += payload;
}
}
}
src/App.vue
code
<template>
<div id="app">
我的年龄是:{{ $store.state.age }}
我弟弟的年龄是:{{ $store.getters.getAge }}
<button @click="$store.commit('changeAge', 10)">根同步修改</button>
<button @click="$store.dispatch('changeAgeAsync', 10)">根异步修改</button>
<button @click="$store.commit('a/changeAge', 10)">a 模块状态同步修改</button>
<button @click="$store.commit('b/changeAge', 10)">b 模块状态同步修改</button>
<button @click="$store.commit('a/c/changeAge', 10)">a.c模块状态同步修改</button>
<button @click="$store.state.age++">恶意直接修改(严格模式下会报错)</button>
<div>A模块中的状态: {{ $store.state.a.aAge }}</div>
<div>B模块中的状态: {{ $store.state.b.bAge }}</div>
<div>A.C模块中的状态: {{ $store.state.a.c.cAge }}</div>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
},
mounted() {
console.log(this)
}
}
</script>