Vuex的用法
安装Vuex
先在Vue 中安装Vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
配置Vuex
安装好Vuex 后需要实例化一个Store 对象,用于管理我们的数据。Store 的配置项包括:
state管理的数据信息mutations存放用于操作数据的方法actions也是用于存放操作数据的方法,区别在于:actions不直接操作state里面的数据,而是通过commit方法来调用mutations里面的方法进行操作数据,可以理解为封装了一层mutations方法调用。actions可以异步操作,并且可以多次commit调用,以修改不同的state,相当于一个mutations组
getters类似于Vue中的计算属性,会将该值缓存起来,只有其依赖的数据改变后,才会重新去计算该值modules将数据进行模块分层,每个module都拥有自身的state、mutations、actions、getters、modulues
new Vuex.Store({
state: { // state -> 等同于data
name: 'zxl',
age: 10
},
// commit调用
mutations: { // 等同于methods
changeAge(state, payload) {
console.log('state is:', state)
state.age += payload
}
},
// dispatch调用action
actions: { // 异步操作,可以多次commit mutation
changeAge(store, payload) {
setTimeout(() => {
store.commit('changeAge', payload)
}, 1000)
}
},
getters: { // 等同于计算属性
myAge(state) {
// 多次调用getter,只要不更新值都不会再次调用
console.log('ok')
return state.age + 10
}
},
modules: { // 模块分割
// 子模块的名字不能和父模块中的state属性重名,不然会覆盖父模块中的state属性
// 分层
a: {
/**
* // 命名空间,解决模块间同名getters冲突的问题,默认会将模块里面个getters合并到根上面去
* namespaced为false的话,getters会被覆盖挂载到根的getters上, mutations回push到_mutations数组里面
*
*/
namespaced: true,
state: {
name: 'dajuan',
age: 23
},
getters: {
/**
* 注意!!! 所有modules里面的getters会被合并挂载到Store对象的getters属性上
* 所以同名会被覆盖
* 可以添加一个namespaced属性来定义同名的getters
* @param state
* @returns {number}
*/
myAge(state) {
// 多次调用getter,只要不更新值都不会再次调用
console.log('ok')
return state.age + 101
}
},
mutations: {
changeName(state, payload) {
state.name = payload
},
changeAge(state, payload) {
state.age += payload
}
}
},
b: {
state: {
name: 'dd',
age: 13
},
mutations: {
changeName(state, payload) {
state.name = payload
},
changeAge(state, payload) {
state.age += payload
}
},
modules: {
c: {
// 这里的b没有namespaced,要调用mutations的话,依然直接使用commit('c/changeAge')就行了
namespaced: true,
state: {
age: 1001
},
mutations: {
changeAge(state, payload) {
state.age += payload
}
}
}
}
}
}
})
使用Vuex
虽然vuex中的数据可以直接通过$store.state访问到并且修改,但是不推荐这样去修改,这样直接修改的话会让数据流向变得很混乱。
<template>
<div id="app">
<h1>哈哈</h1>
<h1>{{ this.$store.state.name }}</h1>
<div>
<h4>Age is:</h4>
<span>{{ this.$store.state.age }}</span>
<span>{{ this.$store.getters.myAge }}</span>
<span>{{ this.$store.getters.myAge }}</span>
</div>
<button @click="() => {this.$store.state.age++}">更改年龄</button>
<button @click="() => {this.$store.commit('changeAge', 8)}">更改年龄2</button>
<button @click="() => {this.$store.dispatch('changeAge', 8)}">异步更新年龄</button>
<hr />
<p>a的信息:</p>
{{ this.$store.state.a.name }} ----- {{ this.$store.state.a.age }}
a的getters年龄:{{ this.$store.getters['a/myAge'] }}
<button @click="() => {this.$store.commit('a/changeAge', 8)}">更改a的年龄2</button>
<p>b的信息:</p>
{{ this.$store.state.b.name }} ----- {{ this.$store.state.b.age }}
<p>c的信息:</p>
{{ this.$store.state.b.c.age }}
<button @click="() => {this.$store.commit('c/changeAge', 8)}">更改c的年龄2</button>
<hr>
<h3>D模块:</h3>
开始D模块不存在,后续手动创建,动态注册模块
<p> name is: {{ $store.state.d && $store.state.d.name }}</p>
<p> gender is: {{ $store.state.d && $store.state.d.gender }}</p>
<p> getters.nameGender is: {{ $store.getters.nameGender }} </p>
<button @click="registerModule">动态创建模块</button>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
import store from './store'
export default {
name: 'App',
components: {
HelloWorld
},
methods: {
registerModule() {
store.registerModule('d', {
state: {
name: 'this is d module',
gender: 'man'
},
getters: {
nameGender(state) {
return state.name + '------' +state.gender
}
}
})
}
}
}
</script>
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
手撸Vuex原理
在使用Vuex的过程中,我们需要先使用Vue.use(Vuex)说明需要导出一个install的方法让Vue进行安装Vuex,同时我们也会new一个Store的实例对象用来存储我们的数据,并将其挂载到Vue的根app上,所以Vuex包需要导出一个install方法用于安装Vuex和一个Store类,用于实例化一个实例来存储数据。
在使用Vuex的时候,我们第一步是使用use方法来安装Vuex,然后实例化一个Strore对象,之后将这个Store实例挂载到Vue的根组件上面。但是在我们的使用过程中,不管是在根组件还是在子组件中我们都是通过this.$store来访问store的,这就说明我们实例化出来的Store实例是挂载到了每个组件上面,所以这一点就是在使用Vue.use(Vuex)时主要进行的事情:
安装阶段
importinstallfrom"./install";
importStorefrom"./store";
export default{
install,
Store
}
index.js就是导出一个install方法和一个Store类。如上文所说,install()方法是Vue在调用Vue.use()时会调用的,而Store就是Vuex的核心,用于管理我们的数据状态。
export let Vue;
export default function install(_Vue) {
Vue = _Vue;
Vue.mixin({
beforeCreate() {
/**
* 每个子组件都会调用这个混入的beforeCreate钩子函数,所以在实例化子组件的时候,
* this就是指向的当前这个子组件
*/
let options = this.$options;
if (options.store) {
this.$store = options.store;
} else {
if (this.$parent && this.$parent.$store) {
this.$store = this.$parent.$store;
}
}
},
});
}
install.js导出一个Vuex全局使用的Vue,这个Vue是在Vue.use()调用install方法时传入的Vue类。然后默认导出一个install方法,在这个install方法中使用mixin方法混入一个beforeCreate的钩子方法,在每个Vue组件被实例化的时候都会触发我们定义的这个钩子函数。在钩子函数里面通过$options字段可以获取到我们实例Vue组件时传入的参数,如果存在store字段的话,就将store字段挂载到实例的$store上;如果不存在store字段的话,就去找父组件,父组件存在并且父组件存在$store属性的话,就将父组件的$store属性赋值给子组件的$store字段上。由此操作就可以让所有的组件都同一个Store实例。
核心Store类
Store的实现主要实现步骤如下:
- 注册
Module;注册Module就是将传入的options进行格式化,有点像转成AST,这样子方便Vuex对配置项进行处理。 - 安装
Module;安装Module就是处理Module中的配置属性:getters、state、mutations、actions、module、namespaced、strict。 - 创建
Vue实例;将state挂载到Vue实例的$$data属性上,将getters里面的属性挂载到computed属性上。 - 处理
plugin插件;如果用户在配置项中传入了plugin参数,则调用plugin方法。
Vuex中可以通过modules字段来按照模块的方式对数据进行分层来管理,所以可以把一个Store实例理解为一个大的模块,然后每个modules下面的又是一个子模块。
注册模块
通过实例化一个ModuleCollection来实例化一个实例:
class Store {
constructor(options) {
// 注册模块
this._modules = new ModuleCollection(options)
}
...
}
ModuleCollection类接受一个参数options参数,即是用户new Store时传入的配置,也可以理解为一个Module的配置。ModuleCollection类在构造函数类会创建一个this.root的属性用来表示是根Module,然后去调用自身的register方法,register(path, rowModule)方法接受两个参数,第一个参数是Module的路径,第二个参数是Module自身的配置(Module自身的配置也是一个Store实例的配置)。例如模块a下面的模块b下面的模块c,那么模块c的路径就是[a, b, c]。register方法中会根据传入的Module配置实例化一个Module类。Module类有三个属性:
_row:_row表示原始的Module的配置_children:_children表示的是当前模块下的子模块state: 表示当前模块的state数据
并且Module类也会提供以下几个方法:
getChild(childName): 获取子模块addChild(childName, child)): 新增子模块forEachGetter(cb):获取gettersforEachMutation(cb):获取mutationsforEachAction(cb):获取actionsforEachChild(cb): 获取children
以及一个namespaced的getter方法:
get namespaced() { // namespaced用于标识是否写了namespaced
return !!this._row.namespaced
}
在Module类的构造函数里将传入的Store模块的配置赋值给_row属性,并将配置的state赋值给Module的state属性。
在register方法中实例化了一个Module类后,将该实例挂载到Store子模块的配置属性上。
.判断一下path的长度,如果长度为0的话,说明当前注册的Store模块的是Store的根模块,将Module的是实例赋值给ModuleCollection的root字段。如果path的长度不为0,则说明不是根模块,而是子模块,则需要找到他的父模块,然后将当前模块的实例挂载到父模块实例的_children字段上(通过调用父模块的addChild(childName, child)方法):
// [a], [a,b], [a,b,c]
// 找父亲
// 拆分路径,从根路径开始查询子模块
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo.getChild(current)
}, this.root)
parent.addChild(path[path.length - 1], newModule)
在注册完当前的Store模块后,通过modules字段来判断是否存在子模块,如果存在子模块的话,再注册子模块:
if (rawModule.modules) { // 如果传入配置里面有modules
forEach(rawModule.modules, (module, key) => {
console.log(`模块${key} is:`, module)
this.register(path.concat(key), module)
})
}
同时ModuleCollection类还提供了一个getNamespace(path)方法,用来返回一个Store模块路径的字符串:
/**
* 获取namespac
* @param path 路径
*/
getNamespace(path) {
// 返回一个字符串 a/b/c或者空字符串''
let root = this.root
let pathStr = path.reduce((ns, key) => {
let module = root.getChild(key)
root = module
return module.namespaced ? ns + key + '/' : ns
}, '')
return pathStr
}
完整的module-collection.js代码如下:
/**
* this.root = {
* _raw: '用户定义的模块',
* state: '当前模块的状态',
* _children: [ // 子模块列表
* a: {
* _raw: '用户定义的模块',
* state: '当前模块的状态',
* _children: [ // 子模块列表
* }
* ]
* }
*/
import {forEach} from "../utils";
import Module from "./Module";
export class ModuleCollection {
constructor(options) {
// 数据格式化操作
this.root = null;
console.log('options is:', options);
this.register([], options);
}
register(path, rawModule) {
let newModule = new Module(rawModule)
rawModule.ModuleInstance = newModule
if (path.length === 0) {
this.root = newModule
} else {
// [a], [a,b], [a,b,c]
// 找父亲
// 拆分路径,从根路径开始查询子模块
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo.getChild(current)
}, this.root)
parent.addChild(path[path.length - 1], newModule)
}
// 注册完当前模块,注册子模块
if (rawModule.modules) { // 如果传入配置里面有modules
forEach(rawModule.modules, (module, key) => {
console.log(`模块${key} is:`, module)
this.register(path.concat(key), module)
})
}
}
/**
* 获取namespac
* @param path 路径
*/
getNamespace(path) {
// 返回一个字符串 a/b/c或者空字符串''
let root = this.root
let pathStr = path.reduce((ns, key) => {
let module = root.getChild(key)
root = module
return module.namespaced ? ns + key + '/' : ns
}, '')
return pathStr
}
}
安装Module阶段
安装Store的Module就是处理state、getters、mutations等参数配置,调用installModule(store, path, module, rootState)来安装Store的模块。参数的说明如下:
store:Store的实例对象path:Store的模块的路径数组module:Store的已经注册好了的模块rootState:Store根模块的state数据
安装Module主要进行以下几个操作:
- 处理state
- 处理getters
- 处理mutations
- 处理actions
- 处理子模块
获取安装模块的路径
let ns = store._modules.getNamespace(path)
通过调用挂在到Store 实例上的getNamespace() 方法,传入当前的Modal 路径数组,就可以得到模块路径的字符串。
处理state
先通过store._modules即是注册好的ModuleCollection实例的getNamespace方法拿到拼接好的路径字符串。判断传入的path长度是否为0,如果不为0的话,说明是子模块,将子模块的状态声明到对应的父模块上去
if (path.length > 0) { // 子模块 [a], [a, b], [a, b, c]
// 需要找到对应的父模块,将状态声明上去
//
let parent = path.slice(0, -1).reduce((memo, current) => {
return memo[current]
}, rootState)
// 新增属性不会触发getter和setter方法,不会变成响应式,不能导致视图更新
// parent[path[path.length - 1]] = module.state
store._withCommitting(() => { // 内部的修改,不应该报错
Vue.set(parent, path[path.length - 1], module.state)
})
}
截取path的路径可以找到当前安装模块的父路径,然后通过迭代遍历父路径找到当前安装模块的父模块,再调用Vue.set()方法将当前模块的state挂载到父模块的state路径下。例如c模块在a模块下,则挂在根Store的state结构如下:
{
state: {
...,
a: {
...,
c: {
...
}
}
}
}
处理getters
module.forEachGetter((fn, key) => {
store.wrapperGetters[ns + key] = () => {
// getters: { // 等同于计算属性
// myAge(state) {
// // 多次调用getter,只要不更新值都不会再次调用
// console.log('ok')
// return state.age + 10
// }
// }
return fn.call(store, getNewState(store, path))
}
})
Modal 类上添加一个方法forEachGetter() ,方法接受一个回调方法,回调方法接受一个方法,即是在初始化Vuex时传入的gettes配置里面的方法以及该方法的key 。然后挂在一个方法到Sotre实例的wrapperGetters上面,key为模块的路径字符串加上getters里面配置的key,方法调用getters配置的方法,并用call()来改变其this的指向,使他指向Store实例,并将当前模块的state作为参数传入。
处理Mutations
module.forEachMutation((fn, key) => {
store.mutations[ns + key] = store.mutations[ns + key] || []
store.mutations[ns + key].push((payload) => {
store._withCommitting(() => {
// 用_withCommitting方法包一层来标记是用mutation来修改的数据
fn.call(store, getNewState(store, path), payload)
})
store._subscribes.forEach(fn => fn({type: ns + key, payload}, store.state))
})
})
调用Modal类上面的forEachMutation()方法,从Modal的_raw属性(原始的配置属性)上获取到mutations配置,取到key和方法后,然后将方法挂载到store实例的mutations上,键为模块路径加上mutation方法的key。因为是Vuex内部改变了state,因此使用_withCommitting()方法来包裹一层(严格模式下,不是通过commit来改变的state,需要抛出异常);遍历_subscribes字段获取到配置的插件并执行。
处理Actions
module.forEachAction((fn, key) => {
store.actions[ns + key] = store.actions[ns + key] || []
store.actions[ns + key].push((payload) => {
return fn.call(store, store, payload)
})
})
同理调用Modal类上的forEachAction()方法获取到配置里面的actions,然后将方法和key挂载到Store类上的actions字段上。
安装子模块
module.forEachChild((child, key) => {
installModule(store, path.concat(key), child, rootState)
})
通过forEachChild()方法获取到当前模块的子模块,然后子模块调用installModule()方法来安装子模块。
创建响应式
Vuex的数据管理是通过实例化一个Vue对象来管理的,响应式也是依赖于Vue。Vuex的state属性相当于Vue的data属性,而Vuex的getters属性则对应Vue的computed属性。通过遍历store对象上wrapperGetters属性获取到所有模块的的getters(在之前处理getters的时候,已经将getters的键改成了模块路径加key的形式),并且定义store.getters的get方法,当从store.getters里面去取值的时候实际是去Vue实例上面去取值(Vue中会将computed里面的属性挂载到Vue身上)。将state和getters传入Vue并实例化然后挂载到store._vm上。由于可以设置一个strict,如果strict为true的话,则如果不是通过commit来修改state需要抛出异常,所以这里需要watch整个state,并且由于在Vue中watcher是异步的,但是在Vuex是同步的,状态一变化就能监控到,状态变化立即执行,所以需要添加sync属性(Vue官方文档上面没有写,但是Vuex源码中就是这样子调用的)。同时由于可以手动注册新的模块,注册新模块的时候需要重新实例化一个Vue实例,原来的Vue实例就需要给销毁掉。
/**
* 重置Vue实例
* @param store
* @param state
*/
function resetVm(store, state) {
const oldVm = store._vm
const computed = {}
store.getters = {}
forEach(store.wrapperGetters, (fn, key) => {
computed[key] = fn
// 从getters上面取值的时候,
Object.defineProperty(store.getters, key, {
get: () => {
return store._vm[key] // 具备了缓存的功能,因为实际上去获取Vue上面的Computed属性
}
})
})
store._vm = new Vue({
data: {
$$state: state
},
computed
})
if (store.strict) { // 如果是严格模式,需要监听数据是否是从mutations中去更改的
store._vm.$watch(() => store._vm._data.$$state, () => {
// watcher默认是异步的, 但是vuex这里需要是同步的,状态一变化就能监控到,状态变化立即执行
// 所以需要添加sync属性
console.assert(store._committing, '要用mutation去修改数据')
}, {deep: true, sync: true})
}
if (oldVm) { // 如果老的Vue实例存在,需要将其卸载
Vue.nextTick(() => {
oldVm.$destroy()
})
}
}
安装插件
if (options.plugins) { // 是否传入了插件属性
options.plugins.forEach((plugin) => {
return plugin(this)
})
}
就是遍历插件,然后调用插件方法。将当前的Store实例作为参数传给插件方法。
Store提供的方法
subscribe(fn)
subscribe(fn) {
this._subscribes.push(fn)
}
提供给插件使用,在插件中调用此方法,会往_subscribes里面推入一个方法,该方法会在执行mutations后执行。
_withCommitting(fn)
_withCommitting(fn)方法用来判断是否是通过commit来修改的state,因为Store类中默认的this._committing为false,而在resetVm()中侦听数据改变时,如果this._committing为false的话就会报错。
_withCommitting(fn) {
this._committing = true; // 先将_committing改为true
// mutation修改state是同步的,所以当mutation修改state后,会直接触发Vue的watcher,由于设置了watcher是同步更新的,所以在watcher里面获取
// 到的store._committing是true
fn();
this._committing = false;
}
replaceState(newState)
replaceState(newState)方法是用来替换最新的state
replaceState(newState) {
this._withCommitting(() => {
this._vm._data.$$state = newState; // 替换为最新的state
// 直接替换_vm._data.$$state的缺陷在于,new Vue是在ModuleCollection以及installModule之后的,就是在Vuex中格式化数据中的state以及模块安装时getter、mutation中的state不一致
})
}
dispatch(actionName, payload)
dispatch(actionName, payload)方法用于派发action。
dispatch = (actionName, payload) => {
const actions = this.actions[actionName];
if (actions) {
actions.forEach((fn) => {
fn(payload)
})
}
}
需要注意的是这里获取到的action是在调用installModule()方法安装actions时包装过的方法。
commit(mutationName, payload)
commit(mutationName, payload)方法用于提交修改。
commit = (mutationName, payload) => {
const mutations = this.mutations[mutationName];
if (mutations) {
mutations.forEach((fn) => {
fn(payload)
})
}
}
同样,这里获取到fn也是在installModule()方法中进行包装过的,在更新完数据后,会遍历_subscribes字段执行里面的方法。
registerModule(path, module)
registerModule(path, module)方法用于注册一个模块。
/**
* 注册模块
* @param {String | String[]} path
* @param {Store} module
*/
registerModule(path, module) { // path可以传入字符串,或者一个路径数组,但是最终都会转成一个路径数组
if (typeof path === 'string') {
path = [path]
}
this._modules.register(path, module) // 注册模块
installModule(this, path, module.ModuleInstance, this.state) // 安装模块
resetVm(this, this.state)
}
一样的套路,调用register()方法来将新的Module添加到AST树中,然后调用installModule()方法来安装该模块,最后因为安装新的模块,state、getters信息可能有所改动,所以再重置一下Vue实例。
辅助方法
mapStates(stateList)
function mapStates(stateList) {
const obj = {};
stateList.forEach((state) => {
obj[state] = function () {
return this.$store.state[state]
}
})
return obj;
}
mapGetters(getterList)
function mapGetters(getterList) {
const obj = {};
getterList.forEach((getter) => {
obj[getter] = function () {
return this.$store.getters[getter]
}
})
return obj;
}
mapMutations(mutationList)
function mapMutations(mutationList) {
const obj = {};
mutationList.forEach((mutation) => {
obj[mutation] = function (payload) {
return this.$store.commit(mutation, payload)
}
})
return obj;
}
mapActions(actionList)
function mapActions(actionList) {
const obj = {};
actionList.forEach((action) => {
obj[action] = function (payload) {
return this.$store.dispatch(action, payload)
}
})
return obj;
}
mapMutations和mapActions的使用
new Vue({
...mapMutations(['mutationsName']),
...mapActions(['actcionName'])
})
mapMutations和mapActions也还是相当于包装了一层方法,方法接受payload参数,然后将这个参数传入mutations或者actions中去。