在大型的vue应用中,基本上都会使用vuex来作状态管理。我们就来一步步手写一个vuex,加深理解对vuex的理解,用起来也更加得心应手。
实现基础用法
先来看看vuex最基础的的使用。
- store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
age: 12
},
mutations: {
increment(state){
state.age ++
}
},
})
- index.js
import Vue from "vue"
import App from "./App.vue"
import store from "./store"
var vm = new Vue({
el: '#root',
store,
render: h => h(App)
})
- app.vue
<template>
<div id="app">
<div>{{$store.state.age}}</div>
</div>
<button @click="$store.commit('increment',2)">increment</button>
</template>
(1)在 store/index.js 中,引入vuex后,做了两件事:Vue.use(Vuex)和new Vuex.Store(options)
(2)在index.js中,导入new Vuex.Store(options)生成的实例,然后new Vue的时候作为选项参数传入。
(3)在app.vue中,也就是vm的子组件实例都挂载了一个$store属性,可以直接通过this.$store访问。
整体框架
按照以上功能,首先来写vuex的整体框架。
Vue.use会调用vuex.install方法;new Vuex.store表示Vuex.store是个类。
class Store{
...
}
const install = (_Vue) => {
...
}
export default {
install,
Store
}
整体框架搭好后,下面分别来完善install和Store的功能。
intall方法
当我们在new Vue(options)的选项中传入store实例后,它每一个子组件上面都有一个$store属性。挂载全局$store就是在install方法完成,借助Vue.mixin()实现。
Vue.mixin(mixin)。全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。
let Vue
const install = (_Vue) => {
Vue = _Vue //install方法调用时,会将Vue作为参数传入
Vue.mixin({
beforeCreate(){
//通过this.$options可以获取new Vue(options)传递的参数options
if(this.$options.store){//说明this是根实例
this.$store = this.$options.store
}else{
//子组件$store属性指向父组件的$store属性
//这样每个组件的$store,并且都指向根实例的$store
this.$store = this.$parent && this.$parent.$store
}
}
})
}
state
我们通过this.$store.state访问数据。说明Store类有一个state属性。
class Store{
constructor(options){
this.state = options.state
}
}
- app.vue
<template>
<div id="app">
<div>{{$store.state.age}}</div>
</div>
<button @click="$store.state.age++">increment</button>
</template>
这样写能拿到$store.state.age的值,但是点击button修改值不能响应式的反映到视图上。实现数据响应,vuex借助了vue的响应机制。
class Store{
constructor(options){
this.vm = new Vue({ //vue对data中的数据进行了依赖收集,因此data中的数据是响应式的
data() {
return {
state: options.state
}
}
})
}
get state(){ //es6写法,访问state属性会执行此方法
return this.vm.state
}
}
getters
getters类似于Vue中的computed,返回基于state的计算值。
class Store{
constructor(options){
...
let getters = options.getters
//这里对象主要用来存储,Object.create(null)比起{}少了原型对象,可以提升一点点性能
this.getters = Object.create(null)
//options传入的每一个getter是函数,而我们要访问的getter是一个值,类似computed
Object.keys(getters).forEach( (getterName) => {
Object.defineProperty(this.getters, getterName, {
get: () => {
return getters[getterName](this.state)
}
})
})
}
}
测试:
//store/index.js
export default new Vuex.Store({
...
getters: {
computedAge(state){
return state.age + 10
}
}
})
//app.vue
<template>
<div id="app">
<div>{{$store.state.age}}</div>
<div>{{$store.getters.computedAge}}</div>
<button @click="$store.state.age++">increment</button>
</div>
</template>
mutation和commit
我们使用 vuex 改变数据时,是触发 commit 方法,像这样:this.$store.commit('increment',2)。这里的increment对应的是mutations中的事件类型。
export default new Vuex.Store({
state: {
age: 12
},
mutations: {
increment(state,payload){
state.age += payload
}
}
})
这其实是一个事件的订阅和发布的过程,拿到options.mutations,进行事件订阅,执行commit进行事件发布。
class Store{
constructor(options){
...
let mutations = options.mutations
this.mutations = Object.create(null)
//订阅事件
Object.keys(mutations).forEach((type) => {
//为每一个事件(type)维护一个订阅函数数组,每次订阅事件就往该数组里面添加订阅函数
const handlers = this.mutations[type] || (this.mutations[type] = [])
handlers.push((payload) => {
mutations[type](this.state,payload)
})
})
}
commit = (type,payload) => {
//发布事件,执行订阅函数数组里面的每一个函数
this.mutations[type].forEach((fn) => {
fn(payload)
})
}
}
action和dispatch
vuex提倡 mutation是同步函数;要异步改变state就用action, 通过dispatch方法触发。(在非严格模式下,mutation是异步函数其实也不会报错)
//store/index.js
export default new Vuex.Store({
state: {
age: 12
},
actions: {
incrementAsync(store,payload){
setTimeout(() => {
store.commit('increment',payload)
},1000)
}
}
})
//app.vue
<template>
<div id="app">
<div>{{$store.state.age}}</div>
<button @click="$store.dispatch('incrementAsync',5)">incrementAsync</button>
</div>
</template>
这也是一个事件的订阅和发布的过程,拿到options.actions,进行事件订阅,执行dispatch进行事件发布。类比就是action--mutation,dispatch--commit。
class Store{
constructor(options){
...
let actions = options.actions
this.actions = Object.create(null)
//订阅事件
Object.keys(actions).forEach((type) => {
//为每一个事件(type)维护一个订阅函数数组,每次订阅事件就往该数组里面添加订阅函数
const handlers = this.actions[type] || (this.actions[type] = [])
handlers.push((payload) => {
actions[type](this,payload) //action订阅函数的第一个参数是store
})
})
}
dispatch = (type,payload) => {
//发布事件,执行订阅函数数组里面的每一个函数
this.actions[type].forEach((fn) => {
fn(payload)
})
}
}
加module功能
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
随着项目规模变大,多人协作,分模块会使数据管理更清晰。
//store/index.js
const ModuleA = {
// namespaced: true,
state: {
age: 11
},
mutations: {
increment(state){
console.log('module A')
state.age ++
}
},
getters: {
computedAgeAFromA(state){
return state.age + ' aFromA'
}
}
}
export default new Vuex.Store({
modules: {
a: ModuleA
},
state: {
age: 12
},
getters: {
computedAge(state){
return state.age + 10
},
computedAgeA(state){
return state.a.age + 10
}
},
mutations: {
increment(state,payload){
console.log('module root')
state.age += payload
}
}
})
//app.vue
<template>
<div id="app">
<div>{{$store.state.age}}</div>
<div>{{$store.getters.computedAge}}</div>
<div>{{$store.getters.computedAgeA}}</div>
<div>{{$store.getters.computedAgeAFromA}}</div>
<button @click="$store.commit('increment',2)">increment</button>
</div>
</template>
- (1)在上面例子中,A模块定义的数据age通过state.a.age访问,根模块的数据age通过state.age访问
- (2)定义在A模块和根模块中的getters都会直接挂载到store上
- (3)执行store.commit时,A模块和根模块的同名mutation都会执行。(store.dispatch也同理)
也就是说,除了state保持树状机构,getters、mutations、actions都进行了扁平化,直接挂载在store上。
实现上述module功能,主要分两步:1、根据选项,创建模块树。 2、遍历模块树,安装模块,实现各个模块state、getters、mutations、actions的挂载。
创建模块树
vuex定义树中的每个module节点如下:
class Module { //module节点
constructor(moduleOption){
this._rawModule = moduleOption //传入的module选项
this._children = Object.create(null) //子节点,这里用对象存储子节点,大概是要拿到模块名吧,如果是数组,数组的每一项需要增加一个字段来表示模块名。
this.state = moduleOption.state //state
}
get namespaced () {
return !!this._rawModule.namespaced //模块是否使用namespace
}
}
根据选项生成module节点后,就可以来创建module树了。
export default class ModuleCollection { //this.root 整个module树的根
constructor (rootModuleOption) {
this.register([], rootModuleOption)
}
register(path, moduleOption){
const module = new Module(moduleOption)
if(path.length === 0){
this.root = module
}else{
let parentModule = path.slice(0,-1).reduce((module,key) => {
return module._children[key]
},this.root)
parentModule._children[path.pop()] = module
}
if(moduleOption.modules){
Object.keys(moduleOption.modules).forEach( (key) => {
this.register(path.concat(key),moduleOption.modules[key])
})
}
}
}
register(path, moduleOption)中path主要用于定位当前module节点的父节点。path为空数组时,说明当前节点是根节点,否则,借助slice和reduce找到当前节点的父节点。
和下面这种树的注册方式相比,传入path让每个module节点具名,安装模块时会用到。后面的namespace也是基于path。
register(parentModule, moduleOption){
const module = new Module(moduleOption)
if(!parentModule){
this.root = module
}else{
parentModule._children.push(module)//此时module._children是数组
}
if(moduleOption.modules){
Object.keys(moduleOption.modules).forEach( (key) => {
this.register(module,moduleOption.modules[key])
})
}
}
安装模块
class Store {
constructor(options){
...
this.getters = Object.create(null)
this.mutations = Object.create(null)
this.actions = Object.create(null)
this._modules = new ModuleCollection(options) //拿到module树
installModule(this,this.state,[],this._modules.root) //安装模块
}
...
}
function installModule(store,state,path,module){
let rawModule = module._rawModule
//state是保持树结构,挂载方法类似创建module树
/****
store.state = {
age: 1,
a: {
age: 2,
c: {
age: 4
}
},
b: {
age: 3
}
}
***/
if(path.length > 0){
let parentState = path.slice(0,-1).reduce((state,key) => {
return state[key]
},state)
Vue.set(parentState,path.pop(),rawModule.state)
}
//挂载getters
if(rawModule.getters){
Object.keys(rawModule.getters).forEach((getterName) =>{
Object.defineProperty(store.getters, getterName, {
get: () => {
return rawModule.getters[getterName](rawModule.state)
}
})
})
}
//挂载mutations
if(rawModule.mutations){
Object.keys(rawModule.mutations).forEach((type) => {
const handlers = store.mutations[type] || (store.mutations[type] = [])
handlers.push((payload) => {
rawModule.mutations[type](rawModule.state,payload)
})
})
}
//挂载actions
if(rawModule.actions){
Object.keys(rawModule.actions).forEach((type) => {
const handlers = store.actions[type] || (store.actions[type] = [])
handlers.push((payload) => {
rawModule.actions[type](store,payload)
})
})
}
// 递归
if(module._children){
Object.keys(module._children).forEach((key) => {
installModule(store,state,path.concat(key),module._children[key])
})
}
}
加namespace
没有namespace的话,当执行store.commit时,A模块和根模块的同名mutation都会执行。要避免重复执行,得在模块里写mutation之前要去其他模块查看一下mutation名称是否已经存在。使用namespace可以避免这个问题。需要在module里配置namespaced: true
const ModuleA = {
namespaced: true,
state: {},
mutations: {}
}
namespace是每个模块的唯一标志。vuex以模块节点的访问路径作为模块的namespace。
function getNamespace(root,path){
let module = root
return path.reduce((namespace, key) => {
module = module._children[key]
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
//path=[] --> ''
//path=['a'] --> 'a/'
//path=['a','c'] --> 'a/c/'
得到namespace后,在安装getters、mutations、actions时就把namespace加上,就ok了。
function installModule(store,state,path,module){
...
let namespace = getNamespace(store._modules.root,path)
if(rawModule.getters){
Object.keys(rawModule.getters).forEach((getterName) =>{
let namespacedGetter = namespace + getterName //<----- 这里
Object.defineProperty(store.getters, namespacedGetter, {
get: () => {
return rawModule.getters[getterName](rawModule.state)
}
})
})
}
if(rawModule.mutations){
Object.keys(rawModule.mutations).forEach((type) => {
let namespacedType = namespace + type //<----- 这里
const handlers = store.mutations[namespacedType] || (store.mutations[namespacedType] = [])
handlers.push((payload) => {
rawModule.mutations[type](rawModule.state,payload)
})
})
}
if(rawModule.actions){
Object.keys(rawModule.actions).forEach((type) => {
let namespacedType = namespace + type //<----- 这里
const handlers = store.actions[namespacedType] || (store.actions[namespacedType] = [])
handlers.push((payload) => {
rawModule.actions[type](store,payload)
})
})
}
...
}
源码
- vuex/index.js
import ModuleCollection from "./module-collection.js"
let Vue
function getNamespace(root,path){
let module = root
return path.reduce((namespace, key) => {
module = module._children[key]
return namespace + (module.namespaced ? key + '/' : '')
}, '')
}
function installModule(store,state,path,module){
let rawModule = module._rawModule
let namespace = getNamespace(store._modules.root,path)
if(path.length > 0){
let parentState = path.slice(0,-1).reduce((state,key) => {
return state[key]
},state)
Vue.set(parentState,path.pop(),rawModule.state)
}
if(rawModule.getters){
Object.keys(rawModule.getters).forEach((getterName) =>{
let namespacedGetter = namespace + getterName
Object.defineProperty(store.getters, namespacedGetter, {
get: () => {
return rawModule.getters[getterName](rawModule.state)
}
})
})
}
if(rawModule.mutations){
Object.keys(rawModule.mutations).forEach((type) => {
let namespacedType = namespace + type
const handlers = store.mutations[namespacedType] || (store.mutations[namespacedType] = [])
handlers.push((payload) => {
rawModule.mutations[type](rawModule.state,payload)
})
})
}
if(rawModule.actions){
Object.keys(rawModule.actions).forEach((type) => {
let namespacedType = namespace + type
const handlers = store.actions[namespacedType] || (store.actions[namespacedType] = [])
handlers.push((payload) => {
rawModule.actions[type](store,payload)
})
})
}
if(module._children){
Object.keys(module._children).forEach((key) => {
installModule(store,state,path.concat(key),module._children[key])
})
}
}
class Store {
constructor(options){
this.vm = new Vue({
data() {
return {
state: options.state
}
}
})
this.getters = Object.create(null)
this.mutations = Object.create(null)
this.actions = Object.create(null)
this._modules = new ModuleCollection(options)
installModule(this,this.state,[],this._modules.root)
console.log(this)
}
get state(){
return this.vm.state
}
commit = (type,payload) => {
this.mutations[type].forEach((fn) => {
fn(payload)
})
}
dispatch = (type,payload) => {
this.actions[type].forEach((fn) => {
fn(payload)
})
}
}
const install = (_Vue) => {
Vue = _Vue
Vue.mixin({
beforeCreate(){
if(this.$options.store){
this.$store = this.$options.store
}else{
this.$store = this.$parent && this.$parent.$store
}
}
})
}
export default {
install,
Store
}
- vuex/module-collection.js
class Module { //module节点
constructor(moduleOption){
this._rawModule = moduleOption
this._children = Object.create(null)
this.state = moduleOption.state
}
get namespaced () {
return !!this._rawModule.namespaced
}
}
export default class ModuleCollection { //this.root 整个module树的根
constructor (rootModuleOption) {
this.register([], rootModuleOption)
}
register(path, moduleOption){
const module = new Module(moduleOption)
if(path.length === 0){
this.root = module
}else{
let parentModule = path.slice(0,-1).reduce((module,key) => {
return module._children[key]
},this.root)
parentModule._children[path.pop()] = module
}
if(moduleOption.modules){
Object.keys(moduleOption.modules).forEach( (key) => {
this.register(path.concat(key),moduleOption.modules[key])
})
}
}
}
vue源码系列文章: