手写 Vuex4 源码(下)
目录
注册模块
注册模块上的 getters,mutations,actions 到 store 上
命名空间
严格模式
插件模式
总结
注册模块
平常使用中定义 modules 如下
// store/index.js
import { createStore } from 'vuex'export default createStore({// strict: true,state: {count: 1},// ...modules: {aCount: {state: {count: 1},modules: {cCount: {state: {count: 1},},}},bCount: {state: {count: 1},}}
})
组件中使用
// App.vue
<template>数量(根模块):{{$store.state.count}} <button @click="$store.state.count++">增加</button><br>数量(aCount模块):{{$store.state.aCount.count}} <button @click="$store.state.aCount.count++">增加</button><br>数量(cCount模块):{{$store.state.aCount.cCount.count}} <button @click="$store.state.aCount.cCount.count++">增加</button><br>
</template>

先实现将用户定义的多个 modules 进行格式化,创造父子关系
用户传进来的数据

格式化后的数据

上面的每个 modules 下的数据格式如下
this._raw = modules // 保存不做处理的源数据
this.state = modules.state // 保存状态
this.children = {} // 创建子对象
- _raw 保存不做处理的源数据
- state 保存状态
- children 保存子模
// 格式化前
bCount:{state: {count: 1},
}// 格式化后
bCount: {_raw: bModule, // 这里放的是格式化前的数据state: bModule.state,children: {}
},
所以这里定义一个类 moduleCollection,专门用来收集模块,将用户写的嵌套 modules 格式化,创造父子关系
class Store {constructor(options) {const store = this// 收集模块,将用户写的嵌套modules格式化,创造父子关系store._modules = new moduleCollection(options)console.log(store._modules)}
}
在 moduleCollection类中定义 register 方法处理数据,定义 this.root 保存处理过后的数据
class moduleCollection{constructor(rootModule){// root 存储格式化的数据,方便后续安装到 store.state 上this.root = nullthis.register(rootModule,[])}register(rootModule,path){}
}
register 方法接受两个参数
一个表示当前处理的模块数据 rootModule ,一个表示当前处理的是谁的模块数据 path
path之所以用数组表示,是因为后面建造父子关系时,使用path可以进行关联
比如 path 是空数组,则表示处理的是根模块的数据,是 [a] 则表示处理的是 a 模块,是[a,c] 则表示处理的是 c 模块的数据,并且,c模块的数据要加到 a 模块的 children 中。
对于 register 方法:
首先将用户定义的 store 格式化赋值给 this.root,这里可以抽象出一个类,因为每个模块的格式都是 _raw,state,children
class Module{constructor(modules){this._raw = modulesthis.state = modules.statethis.children = {}}getChild(key){return this.children[key]}addChild(key,module){this.children[key] = module}
}class moduleCollection{register(rootModule,path){const newModule = new Module(rootModule)if(path.length===0){this.root = newModule}}
}
然后判断 最外层的 store 中也就是根模块还有没有子模块,如果有,继续递归格式化子模块数据
// 如果根模块下还有子模块,则继续递归注册
if(rootModule.modules){Object.keys(rootModule.modules).forEach((key) =>{this.register(rootModule.modules[key],path.concat(key))})
}
用户定义的 store 中,根模块下定义了子模块,子模块里面分别是 aCount 和 bCount,所以执行到上面代码时, key 就是 aCount,bCount
rootModule.modules[key] 是他们对应的模块数据
当执行到 aCount 模块时,此时的 path 是[a],代表处理 aCount 的数据,这时我们要在根模块上添加 aCount,如果 path 是 [aCount,cCount],则需要在 aCount 模块上添加 cCount 模块,所以这里需要定义一个寻找父模块的方法。
使用 path.slice(0,-1) 得到父模块的 key,默认是根模块
const parent = path.slice(0,-1).reduce((modules,current) =>{return modules.getChild(current)
},this.root)
参数 modules 代表上一次执行结果,current代表当前元素,初始传入根模块
这里如果 path 是 [a], 传入给 reduce 时是 [] ,那么返回的就是 this.root
path 是[a,c],传入给reduce 时是 [a], 当执行module.getChild(current) 实际上就是 this.root.getChild(a)
找到父模块后,给父模块的 children 添加 modules
parent.addChild(path[path.length-1],newModule)
moduleCollection 类完整代码:
class moduleCollection{constructor(rootModule){// root 存储格式化的数据,方便后续安装到 store.state 上this.root = nullthis.register(rootModule,[])}register(rootModule,path){// 注册模块,每个模块的格式都是// _raw: rootModule,// state: rootModule.state,// children: {}// 所以给传进来的模块都格式化一下const newModule = new Module(rootModule)// 注册根模块if(path.length===0){this.root = newModule}else{// 注册子模块,将子模块添加到对应的父模块,通过 path路径可以知道对应的父模块const parent = path.slice(0,-1).reduce((modules,current) =>{return modules.getChild(current)},this.root)parent.addChild(path[path.length-1],newModule)}// 如果根模块下还有子模块,则继续递归注册if(rootModule.modules){Object.keys(rootModule.modules).forEach((key) =>{this.register(rootModule.modules[key],path.concat(key))})}}
}
得到格式化数据后,需要将各个模块的 state 安装在 store.state 上,以便之后调用:$store.state.aCount.cCount.count,$store.state安装后的样子应该是:
state:{count:1,aCount:{count:1,cCount:{count:1}},bCount:{count:1}
}
创建一个 installModules 函数
function installModules(store,modules,path){}class Store {constructor(options) {const store = thisstore._modules = new moduleCollection(options)console.log(store._modules)installModules(store,store._modules.root,[])console.log(store.state)}
}
store 是当前 Store 类的实例对象,模块安装的地方
modules 是要安装的模块
path 对应父子关系
installModules 方法和 register 方法类似
function installModules(store,modules,path){if(path.length===0){store.state = modules.state}else{const parent = path.slice(0,-1).reduce((result,current) =>{return result[current]},store.state)parent[path[path.length-1]] = modules.state}if(modules.children){Object.keys(modules.children).forEach((key) =>{installModules(store,modules.children[key],path.concat(key))})}
}
这样也就得到了一个完整的 state

在组件中引用也能正确显示了


注册模块上的 getters,mutations,actions 到 store 上
class Store {constructor(options) {const store = this// 收集模块,将用户写的嵌套modules格式化,创造父子关系store._modules = new moduleCollection(options)// 定义私有变量存放对应的 getters,actions,mutationsstore._getters = Object.create(null)store._mutations = Object.create(null)store._actions = Object.create(null)installModules(store,store._modules.root,[])}
}
同样也是在 installModules 方法里面进行存放操作
在格式化模块时,已经将每个模块定义成这样的数据格式:
this._raw = modules
this.state = modules.state
this.children = {}
_raw 存放的就是源数据,没有被格式化的数据。
所以,取得模块上的 getters 就是 modules._raw.getters;
取得模块上的 mutations 就是 modules._raw.mutations;
取得模块上的 actions 就是 modules._raw.actions;
遍历 modules._raw.getters ,安装到 store._getters 上。这里需要注意的是
getters的参数是 state,这个 state 本来是 modules._raw.state,但 _raw.state没有响应式,而后面store.state 是响应式的,需要根据 path 取得store.state里面对应的 state
function getCurrentState(state,path){return path.reduce((result,current) =>{return result[current]},state)
}function installModules(store,modules,path){······if(modules._raw.getters){forEachValue(modules._raw.getters,(getters,key) =>{store._getters[key] = () =>{// 这里的参数不能是 modules._raw.state,没有响应式// 而后面 store.state 会是响应式的,需要根据 path 取得store.state里面对应的 statereturn getters(getCurrentState(store.state,path))}})}······
}
注册 mutations
if(modules._raw.mutations){forEachValue(modules._raw.mutations,(mutations,key) =>{if(!store._mutations[key]){store._mutations[key] = []}store._mutations[key].push((preload) =>{ // store.commit(key,preload)mutations.call(store,getCurrentState(store.state,path),preload)})})
}
在模块里面,可能有多个同名的 mutations,所以这里可能有多个同名 key,需要用数组包装起来
注册 actions
if(modules._raw.actions){forEachValue(modules._raw.actions,(actions,key) =>{if(!store._actions[key]){store._actions[key] = []}store._actions[key].push((preload) =>{// store.dispatch({commit},preload)// actions执行后返回的是promiselet res = actions.call(store,store,preload)if(!isPromise(res)){return Promise.resolve(res)}return res})})
}
和 mutations 一样,也可能会有多个重名的 actions。区别是 actions 执行完后返回的是一个 promise
命名空间
命名空间的用法,添加 namespaced:true
// store.js
aCount: {namespaced:true,state: {count: 1},mutations: {mutationsAdd(state, preload) {state.count += preload}},modules: {cCount: {namespaced:true,state: {count: 1},mutations: {mutationsAdd(state, preload) {state.count += preload}},},}
}
页面中就可以使用 $store.commit('aCount/mutationsAdd',1) 调用 aCount 下的 mutationsAdd
$store.commit('aCount/cCount/mutationsAdd',1) 调用 cCount 下的 mutationsAdd
在安装模块的时候,通过检测模块是否定义 namespaced 为 true,来给安装的模块的 actions,mutations 添加命名空间前缀
function getNameSpace(modules,path){let root = modules.root// 传入的是 根模块// 当 path 是[],返回空字符串// 2、当 path 是 [aCount] ,根据path,取得根模块下的对应的 aCount , modules.getChild(aCount)// 然后判断 aCount 模块下是否定义namespaced,有则返回 aCount/// 当 path 是 [aCount,cCount] ,重复2,然后根据步骤2 取得的子模块,再往下找子模块cCount,// 然后判断 cCount 模块下是否定义namespaced,有则返回 aCount/cCount/// [] => '' [aCount] => 'aCount/' [aCount,cCount] => 'aCount/cCount'return path.reduce((module,current) =>{root = root.children[current]return root.namespaced?(module+current+'/'):'' },'')
}function installModules(store,modules,path,root){···// 所以这里先根据path取得设置了 namespaced 的模块名字,拼接后注册到 mutations,actions的名字上const namespace = getNameSpace(root,path)console.log(namespace)if(modules._raw.mutations){forEachValue(modules._raw.mutations,(mutations,key) =>{if(!store._mutations[namespace + key]){store._mutations[namespace + key] = []}store._mutations[namespace + key].push((preload) =>{ // store.commit(key,preload)mutations.call(store,getCurrentState(store.state,path),preload)})})}}
在 getNameSpace 方法中,传入的参数 path 表示的是有父子关系的模块名组成的数组,通过 path 找到对应的模块,判断是否定义 namespaced 为 true。最终返回命名空间字符串
严格模式
要设置严格模式,在根节点上指定 strict:true 即可。
const store = createStore({// ...strict: true
})
设置了严格模式,没有通过 mutations 改变状态都会弹出一个报错信息(即通过 $store.state.count++ 直接改变状态)
并且官方建议不要在发布环境下启用严格模式
那么这里可以创建一个变量 isCommiting ,用来判断是否通过 mutations 改变状态,只要是执行了 mutations 方法的都去改变 isCommiting。
store.commit = (type,preload) =>{this.withCommit(() =>{if(store._mutations[type]){store._mutations[type].forEach((fn) =>{fn(preload)})}})
}
withCommit(fn){this.isCommiting = truefn()this.isCommiting = false
}
上面执行 mutations 之前,isCommiting 为 true,此时只需要知道,当状态变化的时候 isCommiting 不为 true,则提示报错。
这里每个数据状态变化都需要知道 isCommiting 的值,所以需要深度监听整个状态。深度监听会带来一定性能损耗,所以严格模式不建议在生产环境使用。
if(store.strict){watch(() =>store._store.data,() =>{console.assert(store.isCommiting,'do not mutate vuex store state outside mutation handlers.')},{deep:true,flush:'sync'})
}
效果如下:
数量(根模块):{{$store.state.count}} <button @click="$store.commit('mutationsAdd',1)">增加</button><button @click="$store.state.count++">错误增加</button>

插件模式
Vuex 的插件实际上是一个函数,store 作为这个函数的唯一参数。定义插件即在 createStore 中定义 plugins 选项,选项是数组格式,可包含多个插件函数。这些插件函数会在创建 store 时依次执行
const plugins1 = (store) => {// 当 store 初始化后调用store.subscribe((mutation, state) => {// 每次 mutation 之后调用// mutation 的格式为 { type, payload }})
}const store = createStore({// ...strict: true,plugins:[plugins1],
})
在插件中可以调用 subscribe 方法,参数是当前调用的 mutation 和调用 mutation 后的 state ,此时的 state 是最新的。并且 subscribe 中的函数都是在每次 mutation 之后调用。
根据这些,来实现 subscribe 方法
class Store {constructor(options) {const store = this// ...store._subscribe = []store.subscribe = (fn) =>{store._subscribe.push(fn)}const plugins = options.pluginsplugins.forEach(fn => {fn(store)});}
定义 _subscribe 私有变量数组,用来存储插件中 subscribe 的函数。如果定义了 plugins 选项,那么依次执行选项中的插件函数。
store.commit = (type,preload) =>{this.withCommit(() =>{if(store._mutations[type]){store._mutations[type].forEach((fn) =>{fn(preload)})store._subscribe.forEach(fn => {fn({type:type,preload:preload},store.state)});}})
}
在调用完 mutation 后,循环调用 _subscribe 的函数。这样每个函数中的 state 参数都是最新的
现在来实现一个持久化存储的插件,将状态存储在 sessionStorage 中,页面刷新后,从 sessionStorage 中取出并替换为最新状态。
const customPlugin = (store) =>{const local = sessionStorage.getItem('vuexState')if(local){store.replaceState(JSON.parse(local))}store.subscribe((mutation,state) =>{sessionStorage.setItem('vuexState',JSON.stringify(state))})
}
在 store.subscribe 参数函数中,每次调用 mutation 后,将状态存储在 sessionStorage 中。store.replaceState 则是一个替换状态的方法。
replaceState(newState){this.withCommit(() =>{this._store.data = newState})
}
效果如下:

总结
Vuex 在项目中用了很久,只知其然不知其所以然,故研究学习并实现出来。
从理解思路到手写出来,然后将实现过程记录下来就有了这篇文章,这个过程断断续续持续了大概一个月,项目和文章基本都是利用下班时间写的,确实挺累的,不过实现出来后往回看,还是学到很多东西,还挺欣慰的;文章有不足的地方还请各位大佬指正;
