Vuex 介绍
- Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
- 详情查看:Vuex 是什么? | Vuex (vuejs.org)
实现 Vuex 思路
-
首先 Vuex 是通过
Vue.use(vuex)
,说明vuex
是个插件,肯定是要install
函数 -
同时会传入Vue,因为vuex是离不开Vue的,或者说是为他量身打造的一个工具
-
其次每个组件都可以使用
$store
,说明是挂载在每个组件上的,那么我们可以使用混入(mixin),在beforeCreate
生命周期中混入是最好的 -
然后就是进行的一些列针对
Store
的操作,其中最核心的是module
,或者说vuex
最核心的部分就是模块化了 -
那么根据module我们会创建一个类
(ModuleCollection)
通过递归注册模块,形成树结构 -
最就是根据树结构我们会实现一个函数,通过递归来安装模块,其中会封装一写工具函数,或者封装类和类的公用方法
-
另外还会就是实现注册模块、vuex的插件持久化,以及严格模式下(strice:true)只能通过mutation来更改状态等功能和一些辅助函数
- 最后放一张最终完成后的目录结构图
mixin
- 这是第一步,给每个组件添加
$store
属性,供所有组件都能使用
function vuexInit() {
const options = this.$options;
// 判断根组件是否有store
if (options.store) {
// 根实例先添加
this.$store = this.$options.store
} else if (options.parent && options.parent.$store) {
// 子组件拿到父组件的store
this.$store = options.parent.$store;
}
}
const applyMixin = (Vue) => {
Vue.mixin({
beforeCreate: vuexInit
})
}
export default applyMixin
util
- 将工具函数抽取出来,使代码简洁明了点
// 这个函数功能会经常用到包括(模块的一些方法),所以在此封装成一个函数
export const forEachValue = (obj, fn) => {
Object.keys(obj).forEach(key => fn(obj[key], key))
}
// 根据路径获取最新的state
export function getState(store, path) {
return path.reduce((newState, current) => {
return newState[current]
}, store.state)
}
module
- 封装的模块类,使每个类都能用到一些方法
import { forEachValue } from "../utils"
export default class Module {
constructor(rootModule) {
this._rawModule = rootModule
this._children = {}
this.state = rootModule.state
}
get namespaced() { // 属性访问器
return this._rawModule.namespaced
}
getChild(key) {
return this._children[key]
}
addChild(key, module) {
this._children[key] = module
}
forEachMutation(fn) {
if (this._rawModule.mutations) {
forEachValue(this._rawModule.mutations, fn)
}
}
forEachAction(fn) {
if (this._rawModule.actions) {
forEachValue(this._rawModule.actions, fn)
}
}
forEachGetter(fn) {
if (this._rawModule.getters) {
forEachValue(this._rawModule.getters, fn)
}
}
forEachChild(fn) {
if (this._children) {
forEachValue(this._children, fn)
}
}
}
moduleCollection
- 递归注册模块,形参树结构
import { forEachValue } from "../utils";
import Module from "./module";
export default class ModuleCollection {
constructor(options) {
this.register([], options)
}
// 注册模块 递归注册 根模块
register(path, rootModule) {
// 创建Module类的实例,Module类中封装了许多方法
let newModule = new Module(rootModule)
// 把当前要注册的模块上做了一个映射
rootModule.rawModule = newModule
if (path.length === 0) {
// 刚开始肯定是没有path肯定是不存在的,所有添加一个root属性存入根模块
this.root = newModule
}
else {
// 例如:['a', 'b', 'c']这种路径形式,代表a模块中有b模块,b模块中有c模块
// 那么其作用就是c应该在b模块中,所有slice(0,-1)拿到除最后一个的数组即['a','b']
// 通过reduce取到b模块,再将c模块放入b模块中
let parent = path.slice(0, -1).reduce((module, key) => module.getChild(key)
, this.root)
parent.addChild([path[path.length - 1]], newModule)
}
// 如果有modules,说明有子模块进行递归注册
if (rootModule.modules) {
forEachValue(rootModule.modules, (module, moduleName) => {
this.register([...path, moduleName], module)
})
}
}
// 获取命名空间
getNamespace(path) {
let root = this.root
// 根据path不断拿到对于模块查看是否有namespace属性
return path.reduce((namespace, key) => {
root = root.getChild(key) // 不断拿到子模块
// // 有的话拼接上作为地址
return namespace + (root.namespaced ? key + '/' : '')
}, '')
}
}
installModule
- 递归安装模块及Store传入的options
import Vue from 'vue'
import { getState } from '../utils'
export default function installModule(store, rootState, path, module) {
// 注册事件时 需要注册到对应的命名空间中 path就是所有的路径 根据path算出一个空间里
let namespace = store._modules.getNamespace(path)
if (path.length > 0) {
let parent = path.slice(0, -1).reduce((state, current) => {
return state[current]
}, rootState)
store._withCommitting(() => {
Vue.set(parent, path[path.length - 1], module.state)
})
}
module.forEachMutation((mutation, type) => {
store._mutations[namespace + type] = (store._mutations[type] || [])
store._mutations[namespace + type].push((payload) => {
store._withCommitting(() => {
mutation.call(store, getState(store, path), payload)
})
store._subscribers.forEach(sub => sub({ mutation, type }, store.state))
})
})
module.forEachAction((action, type) => {
store._actions[namespace + type] = (store._actions[type] || [])
store._actions[namespace + type].push((payload) => {
action.call(store, store, payload)
})
})
module.forEachGetter((getter, key) => {
store._wrappedGetters[namespace + key] = function (params) {
return getter(getState(store, path))
}
})
module.forEachChild((child, type) => {
installModule(store, rootState, path.concat(type), child)
})
}
resetStoreVm
- 将状态都挂载到Vue实例中,数据响应式
import { forEachValue } from "../utils"
import Vue from "vue"
export default function resetStoreVm(store, state) {
const wrappedGetters = store._wrappedGetters
let oldVm = store._vm
let computed = {}
store.getters = {}
// computed 缓存效果 当computed中值发送变化时再次订阅
forEachValue(wrappedGetters, (fn, key) => {
computed[key] = function () {
return fn() // 将getters对应key的函数调用
}
Object.defineProperty(store.getters, key, {
get: () => store._vm[key] // 读取时在vm上拿
})
})
// 创建Vue实例实现数据响应式
store._vm = new Vue({
data: {
$$state: state
},
computed
})
if (store.strict) {
console.log(store.strict);
// 状态一变化就会立即执行
store._vm.$watch(() => store._vm._data.$$state, () => {
console.assert(store._committing, '在mutation之外更改了状态');
}, { deep: true, sync: true }) // 深度 同步
}
if (oldVm) Vue.nextTick(() => oldVm.$destroy())
}
class store
- 这是针对Store的类
import applyMixin from './mixin'
import { forEachValue, getState } from './utils'
import ModuleCollection from './module/moduleCollection'
import installModule from './module/installModule'
import resetStoreVm from './module/resetStoreVm'
let Vue
class Store {
constructor(options = {}) {
this._modules = new ModuleCollection(options)
this._mutations = {} // 存放所有模块中的mutations
this._actions = {} // 存放所有模块中的actions
this._wrappedGetters = {} // 存放所有模块中的getters
this._subscribers = [] // 存放订阅的插件
this.strict = options.strict // 严格模式
this._committing = false // 同步的watcher
let state = this._modules.root.state // 根模块state
installModule(this, state, [], this._modules.root)
// 将状态放到vue的实例中
resetStoreVm(this, state)
options.plugins.forEach(plugin => plugin(this))
}
//
_withCommitting(fn) {
let committing = this._committing
this._committing = true // 在函数调用前标识_committing为true
fn()
this._committing = committing
}
// 用于持久化保存数据替换原数据的方法
replaceState(newState) {
this._vm._data.$$state = newState
}
// 订阅收集
subscribe(fn) {
this._subscribers.push(fn)
}
// commit
commit = (type, payload) => {
this._mutations[type].forEach(fn => fn(payload))
}
// dispatch
dispatch = (type, payload) => {
this._actions[type].forEach(fn => fn(payload))
}
// 类的属性访问器
get state() {
return this._vm._data.$$state
}
// 动态注册
registerModule(path, rawModule) {
if (!Array.isArray(path)) path = [path]
// 注册模块
this._modules.register(path, rawModule)
// 安装模块 并动态将状态新增上去
installModule(this, this.state, path, rawModule.rawModule)
// 重构数据响应式
resetStoreVm(this, this.state)
}
}
const install = (_Vue) => {
Vue = _Vue
applyMixin(Vue)
}
export default {
Store,
install
}
export {
Store,
install
}
实现持久化插件 & Store 数据
- 实现持久化插件,放入到
plugins
数组中 subscribe
只要状态变了就会执行
import Vue from 'vue'
import Vuex from '../vuex'
// import Vuex from 'vuex'
// import logger from 'vuex/dist/logger'
// 持久化插件
function persists(store) {
let local = localStorage.getItem('VUEX:STATE')
if (local) {
store.replaceState(JSON.parse(local))
}
store.subscribe((mutation, state) => {
// 只要频繁操作 就要考虑防抖和节流
localStorage.setItem('VUEX:STATE', JSON.stringify(state))
})
}
Vue.use(Vuex)
const store = new Vuex.Store({
strict: true, // 严格模式下,只能通过 mutation 来更改状态
plugins: [
persists,
// logger()
],
state: {
name: 'lain',
age: 16,
count: 0
},
getters: {
addCount({ count }) {
// console.log("".concat(state.name, "123").concat(state.age));
// return `${state.name}123${state.age}`
// return "".concat(state.name);
// return "".concat(state.name, 123).concat(state.age);
return count++
}
},
mutations: {
changeAge(state, payload) {
state.age += payload
},
redAge(state, payload) {
state.age -= payload
}
},
actions: {
redAge(ctx, payload) {
setTimeout(() => {
ctx.commit('redAge', payload)
}, 300)
}
},
})
export default store
实现辅助函数 (helpers)
- vuex提供了一些非常方便的辅助函数,比如mapState、mapGetter、mapMutation、mapAction
- 下面来实现一下这些辅助函数,只要明白一个原理其余都是类似的
export const mapState = arrList => {
const obj = {};
for (let i = 0; i < arrList.length; i++) {
obj[arrList[i]] = function () {
return this.$store.state[arrList[i]];
};
}
return obj;
};
export const mapGetters = arrList => {
const obj = {}
for (let i = 0; i < arrList.length; i++){
obj[arrList[i]] = function () {
return this.$store.getters[arrList[i]]
}
}
return obj
}
export const mapMutations = arrList => {
const obj = {}
for (let i = 0; i < arrList.length; i++){
obj[arrList[i]] = function (payload) {
return this.$store.commit(arrList[i], payload)
}
}
console.log(obj);
return obj
}
export const mapActions = arrList => {
const obj = {}
for (let i = 0; i < arrList.length; i++){
obj[arrList[i]] = function (payload) {
return this.$store.dispatch(arrList[i], payload)
}
}
return obj
}
index
- 最终统一导出
import { mapState, mapGetters, mapMutations, mapActions } from "./helpers";
import { Store, install } from "./store";
export default {
Store,
install,
mapState,
mapGetters,
mapMutations,
mapActions
}
export {
Store,
install,
mapState,
mapGetters,
mapMutations,
mapActions
}
- 那么Vuex的实现也告辞段落,下面进行简单测试,其余的模块或者命名空间等可以自行测试
测试 App
<template>
<div id="app">
<h2>年龄: {{ $store.state.age }}</h2>
<h2>姓名:{{ $store.state.name }}</h2>
<h2>age: {{ age }}</h2>
<h2>addCount: {{ addCount }}</h2>
<button @click="changeAge(1)">changeAge</button>
<button @click="redAge(1)">redAge</button>
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from "./vuex";
export default {
name: "App",
computed: {
...mapState(["age"]),
...mapGetters(["addCount"]),
},
methods: {
...mapMutations(["changeAge"]),
...mapActions(["redAge"]),
},
};
</script>
<style></style>