近万字!手摸手从0实现一个ts版的Vuex4 🎉🎉
hey🖐! 我是小黄瓜😊😊。不定期更新,期待关注➕ 点赞,共同成长~
写在前面
本文涉及到的代码已经上传到github,顺便安利一下tiny-ts-tools
,这个项目会使用Typescript
来实现一些经典的前端库,相关库核心逻辑的最简实现,以及分步骤实现的文章。接下来会实现mini-nest依赖注入、axios等欢迎star! 地址:Tiny-Blog
Vuex是什么?
我相信经常使用vue进行日常开发的小伙伴肯定不会陌生,按照vue官方文档上的说法来解释:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
其实就是一个数据管理的容器,对不同组件中所需要依赖的数据进行管理。举个例子来说:
如果我们在父子组件中进行数据传递,很简单,我们可以在组件上定义一个自定义数据,然后在子组件中通过props
进行获取。如果组件中有多层级的嵌套,他们之间进行数据共享怎么做呢,难道要一层层的进行传递?这也太麻烦了。
由此vuex的作用就显现出来了,其实在很多框架都有类似的状态管理工具,比如vue提供的vuex,react的redux、mobx,都是解决类似的问题。 说回到vuex,这个状态自管理应用包含以下几个部分:
- 状态,驱动应用的数据源;
- 视图,以声明方式将状态映射到视图;
- 操作,响应在视图上的用户输入导致的状态变化。
整个的"单向数据流"理念的简单示意图:
而整个vuex的数据流向是这样的:
页面数据发生更改 -> 调用
dispatch
来分发actions
,在actions
调用commit
方法来触发mutations
进行数据更改,更改 Vuex
的 store
中的状态的唯一方法是提交 mutation
,而获取state
中的数据可以使用store.getters.xx
或者store.state.xx
。
store
主要由actions
、mutations
、state
、getters
、module
这几部分构成。
- state:数据源,可以使用一个对象来保存状态
state: {
name: '小黄瓜',
foodList: ['瓜', '苹果', '橘子']
}
actions
:用于提交mutation
,可以用于操作异步,而不直接变更state
的状态
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
// 触发 mutations 中的方法
context.commit('increment')
}
}
})
mutations
:使用接收到的数据用于更新state
,一般为同步操作
const store = createStore({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
getters
:提供组件的 获取 响应式 state 数据 的对象,并做一些额外的操作
const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
// 调用getters 中的 doneTodos 函数获取state中的数据
doneTodos (state) {
return state.todos.filter(todo => todo.done)
}
}
})
module
:模块,每个模块拥有自己的actions
、mutations
、state
、getters
,嵌套子模块
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... },
// moduleC为moduleA的子模块
modules: {
c: moduleC
}
}
const moduleB = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
const moduleC = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... }
}
初始化
vuex是如何初始化的呢?回想一下在使用vuex的时候,先使用npm/yarn
安装vuex,然后在项目的main.js
文件下注册插件:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from '@/store/index'
// 使用use进行注册
createApp(App).use(store).use(router).mount('#app')
而store
文件,就是我们的vuex仓库定义的根目录:
// 这里已经被替换为我们自己的vuex源码文件
import { createStore } from '../vuex/indexSingle'
// 使用createStore创建仓库
export default createStore({
state: {
navList: ['这是一个测试', 'ok']
},
getters: {
showNavList(state) {
return state.navList
}
},
mutations: {
modifyNavList(state, navList) {
return state.navList = navList
}
},
actions: {
handlerNavList({ commit }) {
setTimeout(() => {
const navList = ['a', 'b', 'c']
commit('modifyNavList', navList)
}, 600)
}
}
})
可以看到,我们的store
文件是由createStore
函数生成。
接下来就开始实现手写vuex的初始化逻辑:
其中StoreOptions
这个类型约束将会在下文实现。
首先使用createStore
函数创建Store
类,Store
类是我们实现vuex的核心,所有的数据都会被保存到此类中。Store
类中的install
方法是vue注册插件要求提供的方法,install是开发插件的方法,这个方法的第一个参数是 Vue 构造器,在install
方法使用provide
将Store
类的实例绑定到全局,然后在页面中使用useStore
方法调用inject
来获取Store
类。
const injectKey = "store"
// 页面组件调用
export function useStore<S>(): Store<S> {
return inject(injectKey) as any
}
// vuex/index生成Store类
export function createStore<S>(options: StoreOptions<S>) {
return new Store<S>(options)
}
// Store类
class Store<S = any>{
constructor(options: StoreOptions<S>) {}
install(app: App) {
app.provide(injectKey, this)
}
test() {
return "我是store";
}
}
provide
可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject
来接收 provide
提供的数据或方法。
例如现在有父组件和子孙组件,通过 provide
和 inject
来进行数据传递:
<template>
<div id="parent">
<p>父组件</p>
<Child></Child>
</div>
</template>
<script>
import Child from './child.vue'
export default {
components: {
Child
},
// 父组件中返回要传给下级的数据
provide () {
return {
msg: '小黄瓜'
}
},
// 或者 写法如下
// provide: {
// msg: '小黄瓜'
// },
}
</script>
<template>
<div id="child">
<p>子孙组件</p>
</div>
</template>
<script>
export default {
name: "child",
// 子孙组件中接收祖先组件中传递下来的数据
inject: ['dataInfo'],
}
</script>
在初始化vuex的过程中,我们使用provide
来注册根store的数据,在页面中使用inject
来获取数据:
type RootState = {
navList: any
}
export default defineComponent({
name: 'HomeView',
setup() {
const store = useStore<RootState>()
return {
store
}
}
});
整个过程如下:
类型定义
首先来定义Store
类中接接收的参数,也就是options
,其实也就是定义的store
结构,一般是这样子的:
const store = {
state: {},
actions: {
xxx() {}
},
mutations: {
xxx() {}
},
getters: {
xxx() {}
}
}
由此就可以定义出options
的接口类型:
interface StoreOptions<S> {
state?: S;
getters?: GetterTree<S, S>;
mutations?: MutationTree<S>;
actions?: ActionTree<S, S>
}
actions
actions
的约束:
actions
接收一个对象,key
为函数名,value
为函数体。
其中函数的第一个参数可以调用commit
等方法对mutations
中的函数进行操作。
S代表当前state
R代表根state
interface ActionTree<S, R> {
[key: string]: Action<S, R>
}
type Action<S, R> = (context: ActionContext<S, R>, payload?: any) => any
interface ActionContext<S, R> {
dispatch: Dispatch;
commit: Commit;
state: S;
}
// dispatch方法
type Dispatch = (type: string, payload?: any) => any
// commit方法
type Commit = (type: string, payload?: any) => any
mutations
mutations
与actions
相似,同样是函数对象,不同的是,mutation
函数的第一个参数接收state
第二个参数为新值,用于更新数据:
interface MutationTree<S> {
[key: string]: Mutation<S>
}
type Mutation<S> = (state: S, payload?: any) => void
getters
getters
主要获取state
中的数据,并增加一些额外的处理逻辑,getter
函数的第一个参数为当前state
(当前store中),第二个参数为当前getters
对象(当前store
中),第三个和第四个参数分别为根state
和getters
。
interface GetterTree<S, R> {
[key: string]: Getter<S, R>
}
type Getter<S, R> = (state: S, getters: any, rootState: R, rootGetters: any) => any
现在我们已经可以在页面中调用store
中的方法了,上面我们定义了一个test
方法,现在就执行一下:
<template>
<div class="home">
{{ store.test() }}
</div>
</template>
<script lang="ts">
export default defineComponent({
name: 'HomeView',
setup() {
const store = useStore<RootState>()
return {
store
}
}
});
</script>
页面中已经成功显示出来了:
现在我们已经实现了一下单模块的
store
,但是实际的情况往往不是这么简单的,比如我们想要将不同的模块进行拆分,这就需要用到module
,相应的,也会继续在下文实现commit
和dispatch
两个方法。
注册Module
首先还是先来定义modules
的类型,在StoreOptions
中增加modules
属性,他的类型约束与StoreOptions
类似,只是多了一个namespaced
布尔类型,标识是否为独立命名空间。
interface StoreOptions<S> {
state?: S,
getters?: GetterTree<S, S>,
mutations?: MutationTree<S>,
actions?: ActionTree<S, S>,
// 增加ModuleTree类型
modules?: ModuleTree<S>
}
interface ModuleTree<R> {
[key: string]: Module<any, R>
}
export interface Module<S, R> {
namespaced?: boolean,
state?: S,
getters?: GetterTree<S, R>,
mutations?: MutationTree<S>,
actions?: ActionTree<S, R>,
modules?: ModuleTree<R>
}
然后先顶一下一下我们需要使用的store
结构,这里我是定义了home
和mine
两个模块作为根store
的子模块:
export default createStore<RootState>({
modules: {
mineModule: mineModule,
homeModule: homeModule
}
})
然后将food
模块作为mine
的子模块,现在的模块结构是这样的:
- 根模块
- mineModule
- foodModule
- homeModule
接下来定义homeModule
文件:
interface IMsg {
id: number,
code: string,
name: string,
addr: string
}
interface IHomeList {
[key: number]: IMsg
}
interface IState {
'homeList': IHomeList
}
const state: IState = {
homeList: [{
id: 0,
code: '123d1',
name: '红太阳',
addr: '幸福路1号'
},
{
id: 1,
code: '123a2',
name: '金浪漫',
addr: '浪漫路2号'
},
{
id: 2,
code: '123d6',
name: '红彤彤',
addr: '喜悦路23号'
},
{
id: 3,
code: '123c5',
name: '喜洋洋',
addr: '快乐路1号'
},
{
id: 4,
code: '123p0',
name: '大草原',
addr: '搞笑路1号'
},
{
id: 5,
code: '123h3',
name: '红太郎',
addr: '幸福路24号'
}
]
}
export const homeModule: Module<IState, RootState> = {
namespaced: true,
state,
getters: {
showNavList(state) {
return state.homeList
}
},
mutations: {
modifyHomeList(state, newList) {
return state.homeList = newList
}
},
actions: {
handlerNavList({ commit }) {
setTimeout(() => {
const navList = ['a', 'b', 'c']
commit('modifyHomeList', navList)
}, 600)
}
}
}
RootState
代表根state
的空对象,作为根state
的类型定义:
export type RootState = {}
定义mineModule
文件:
interface IState {
'typeList': string[]
}
const state: IState = {
typeList: ['猛烈', '温柔', '触宝', '精神病']
}
export const mineModule: Module<IState, RootState> = {
namespaced: true,
state,
getters: {
showNavList(state) {
return state.typeList
}
},
mutations: {
modifyNavList(state, navList) {
return state.typeList = navList
}
},
modules: {
foodModule: foodModule
},
actions: {
handlerNavList({ commit }) {
setTimeout(() => {
const navList = ['a', 'b', 'c']
commit('modifyNavList', navList)
}, 600)
}
}
}
定义mineModule
模块的子模块foodModule
:
interface IState {
[key: string]: number[]
}
export const foodModule: Module<IState, RootState> = {
namespaced: true,
state: {
foodList: [1, 2, 3]
},
getters: {
getFoodList(state) {
return state.foodList
}
},
mutations: {
modifyNavList(state, navList) {
return state.foodList = navList
}
},
actions: {
handlerNavList({ commit }) {
setTimeout(() => {
const navList = [4, 5, 6]
commit('modifyNavList', navList)
}, 600)
}
}
}
现在可以在store
类中打印下,看看我们传入的是一个什么样子的结构?
// 为了区分默认的Store,将我们的store类改为TinyStore
class TinyStore<S = any> {
constructor(options: StoreOptions<S>) {
console.log(options) // 打印传入的options
}
install(app: App) {
app.provide(injectKey, this)
}
test() {
return 'is a test'
}
}
可以看到,打印的结果完全符合我们的预期。
在开始处理module
之前先来了解一下两个类。
ModuleCollection
封装和管理所有模块的类,类成员,说白了就是对所有的模块统一进行管理,包括我们接下来要做的酱所有的模块进行统一包装和注册。 属性:
root
——> 根模块属性
方法:
register
——> 注册根模块和子模块的方法 【注册就是添加】get
——> 获取子模块方法
ModuleWrapper
封装和管理某一个模块的类,注意和**ModuleCollection**
类的区别**,****ModuleWrapper**
只专注于某一个具体的模块的处理。相当于对每个模块的扩展,为每个模块穿上了一身装备。
属性:
children
——> 保存当前模块下子模块rawModule
——> 保存当前模块的属性state
——> 保存当前模块的 state 的属性namespaced
——> 判断 当前模块是否有命名空间的属性
方法:
addChild
—— 添加子模块到当前模块中getChild
—— 获取子模块
ModuleCollection
和ModuleWrapper
与Module
之间关系如下图:
ModuleCollection
类专注于处理所有的模块,所谓的处理,是指将所有的模块,递归进行遍历,包括最深的层级,然后使用ModuleWrapper
类对每个模块进行包裹,按照层级关系添加到每个ModuleWrapper
类的children
属性中,这就是所谓的模块注册。
所以吗,模块注册的逻辑应当被添加到TinyStore
类的constructor
函数中执行:
class TinyStore<S = any> {
// 保存处理后的结果
moduleCollection: ModuleCollection<S>
constructor(options: StoreOptions<S>) {
// 执行ModuleCollection类,传入options
this.moduleCollection = new ModuleCollection<S>(options)
}
install(app: App) {
app.provide(injectKey, this)
}
test() {
return 'is a test'
}
}
先来实现一下ModuleWrapper
这个工具类,在递归深层遍历模块时,被传入ModuleWrapper
类的模块是原始模块,而处理之后保存到children
属性中的是包装后的模块类。这一点也可以在两个属性的类型约束中可以看出来:
children
:Record<string, ModuleWrapper<any, R>> = {}
rawModule
:Module<any, R>
class ModuleWrapper<S, R> {
// 保存使用ModuleWrapper包装后的模块对象
children: Record<string, ModuleWrapper<any, R>> = {}
// 保存包装前的模块
rawModule: Module<any, R>
// 保存state,数据源
state: S
// 定义是否具有独立命名空间?直接获取原模块的namespaced属性
namespaced: boolean
constructor(rawModule_: Module<any, R>) {
this.rawModule = rawModule_
// state 如果不存在,初始化为空对象
this.state = rawModule_.state || Object.create(null)
this.namespaced = rawModule_.namespaced || false
}
addChild(key: string, moduleWrapper: ModuleWrapper<any, R>) {
this.children[key] = moduleWrapper
}
getChild(key: string) {
return this.children[key]
}
}
addChild
方法使用传入的key
和包装后的模块类保存到children
属性中,而getChild
通过key
来获取模块类。
此时的两个概念:
- 模块
模块指我们在store
中定义的原始的模块,例如:
foodModule: {
actions: {}
mutations: {}
...
}
- 模块类
此时的模块类是经过ModuleWrapper
包装的类,而已经不是单纯的原对象了。
const newModule = new ModuleWrapper(rawModule)
接下来就可以开始在ModuleCollection
类中实现将所有的子模块类添加到父级模块类的children
属性中。整理逻辑是这样的,我们需要为每个模块路径都维护一个path
数组,便于对每条父子级关系可追溯。就那我们上面的例子来说,他们对应的path
数组的关系为:
1: ['homeModule']
2: ['mineModule', 'foodModule']
从每个模块的分叉出进行拆分,每个路径都创建独立的数组,便于对每个模块进行索引取值。
定义ModuleCollection
类,并且开始执行register
方法从根模块开始注册:
一开始传入的是根模块,此时执行register
方法,创建根模块的模块类,path
为空数组,长度为0,所以保存根模块到root
属性中,因为在js中对象都是引用类型,所以后续在处理完整个模块对象后,可以通过root
访问整个处理后的模块对象。如果path
的长度不为0,那么说明此时处于某一层的子模块当中,这时我们就需要将当前的子模块类添加到其父模块类的children
属性中,这部分逻辑暂时略过。
先来看一下模块递归的逻辑:如果当前模块依然拥有modules
属性,说明还需要向下遍历,取出modules
属性,这里使用了一个额外的工具类,用来作为通用的遍历方法,Util
的forEachValue
为静态方法,接受一个数组和处理函数,将处理函数作用于数组的每一项。这里我们将子模块数组传入,处理函数中的key
和modules
此时分别为模块名和模块对象。,此时调用register
方法将path
添加模块名后当作新的path
传入,第二个参数自然就是遍历的每个模块对象。
class ModuleCollection<R> {
root!: ModuleWrapper<any, R>
constructor(rawRootModule: Module<any, R>) {
this.register([], rawRootModule)
}
register(path: string[], rawModule: Module<any, R>) {
// 创建模块类
const newModule = new ModuleWrapper(rawModule)
if (path.length === 0) {
// 根模块
this.root = newModule
} else {
// 子模块
}
if (rawModule.modules) {
// 取出当前模块下的子模块
const { modules: sonModules } = rawModule
Util.forEachValue(sonModules, (key: string, modules: Module<any, R>) => {
// 递归模块,拼接path
this.register(path.concat(key), modules)
})
}
}
}
class Util {
// 接收数组与处理函数
static forEachValue(obj: Record<string, any>, fn: (...args: any) => void) {
Object.keys(obj).forEach(key => {
fn(key, obj[key])
})
}
}
接下来就是添加的逻辑,上文中已经说过了,处于当前的模块中需要做的就是将自己的模块类添加到父级模块类的children
属性中,那么怎么能找到所属的父级类呢?答案还是path
。
因为在递归过程中我们已经保存了path
(模块名)的路径,所以在寻找父级的时候按照模块名就好了,如果我们当前处于foodModule
模块,直接在path
数组中截取0-当前模块的前一位不就寻找到整个祖先路径可以了吗!再使用root
根据截取的path
数组遍历就可以寻找到父级的模块类。
如果我们的path
保存的是一条这样的模块路径:(这只是一个为了展示路径截取的例子,并不是我们当前代码实现所使用的模块路径)
person
-> man
-> teacher
-> student
如果我们当前处于student
模块中,那么此时截取的path
:
接下来的实现代码就水到渠成了:
class ModuleCollection<R> {
root!: ModuleWrapper<any, R>
constructor(rawRootModule: Module<any, R>) {
// this.root = new ModuleWrapper(rawRootModule)
// this.register([], this.root)
this.register([], rawRootModule)
}
register(path: string[], rawModule: Module<any, R>) {
const newModule = new ModuleWrapper(rawModule)
if (path.length === 0) {
this.root = newModule
} else {
// 添加子模块 - > 父模块 children
// 获取父级的moduleWrapper对象
const parentModule: ModuleWrapper<any, R> = this.get(path.slice(0, -1))
// 添加到父级模块的children
parentModule.addChild(path[path.length - 1], newModule)
}
if (rawModule.modules) {
const { modules: sonModules } = rawModule
Util.forEachValue(sonModules, (key: string, modules: Module<any, R>) => {
this.register(path.concat(key), modules)
})
}
}
// 获取父级
get(path: string[]) {
const module = this.root
// 以根模块为初始循环模块
return path.reduce((moduleWrapper: ModuleWrapper<AnalyserNode, R>, key: string) => {
// 循环获取模块的children
return moduleWrapper.getChild(key)
}, module)
}
}
打印一下TinyStore
,发现模块类已经成功被添加到children
中:
这里有一个地方可能会不大好理解,就是
path
的收集过程,首先打印一下每次循环path
数组的值:
可以看到,当递归到最深的层级后,也就是收集完
foodModule
模块后,我们的path
将已经收集到的模块名弹出了,又重新开始收集homeModule
。这里我们对循环的处理是先循环外层模块,然后再每个模块进行递归,递归的时候拼接path
数组。
这张图就很明显了,虽然path
在递归的时候每次拼接模块名,但是在下一次循环的时候当次循环依然还是初始的数组。
实例方法 Commit & Dispatch
在Vuex中可以通过实例直接进行dispatch
分发action
,或者使用commit
提交mutation
未来我们需要将actions
、mutations
、getters
都作为TinyStore
类的实例属性保存,所以调用dispatch
和 commit
时,也在实例属性中取值。
class TinyStore<S = any> {
moduleCollection: ModuleCollection<S>
// mutations
mutations: Record<string, any> = {}
// actions
actions: Record<string, any> = {}
// getters
getters: GetterTree<any, S> = {}
commit: Commit
dispatch: Dispatch
constructor(options: StoreOptions<S>) {
this.moduleCollection = new ModuleCollection<S>(options)
// store和ref变量都为本实例,分别定义不同的名字,语义化
const store = this
const ref = this
// 赋值实例方法commit_
const commit = ref.commit_
// 赋值实例方法dispatch_
const dispatch = ref.dispatch_
this.commit = function boundCommit(type: string, payload: any) {
commit.call(store, type, payload)
}
this.dispatch = function boundDispatch(type: string, payload: any) {
dispatch.call(store, type, payload)
}
}
install(app: App) {
app.provide(injectKey, this)
}
test() {
return 'is a test'
}
commit_(type: string, payload: any) {
if (!this.mutations[type]) throw new Error('[vuex] unknown mutations type: ' + type)
this.mutations[type](payload)
}
dispatch_(type: string, payload: any) {
if (!this.actions[type]) throw new Error('[vuex] unknown actions type: ' + type)
this.actions[type](payload)
}
}
这两个方法都是通过type
也就是方法名在mutations
和 actions
中查找处理函数,然后传入值执行。
值得注意的是,这两个方法目前并不能成功获取到处理函数,未来我们将所有的相关处理函数都挂载到mutations
和 actions
之后,才能成功执行。
注册State
目前在store
中的数据源也就是state
位置分散在各个模块中,在这一节我们就将不同层级的state
收集在ModuleWrapper
模块类中各个层级的state
中,类似于对module
的做法。
定义installModule
函数,它的作用是对每个模块的注册操作,初始化根模块,递归注册所有子模块,包括state
、以及后续的actions
、mutations
、getters
等。
installModule
函数的参数如下:
store
:TinyStore
类的实例rootState_
:根state
path
:模块路径module
:当前模块类
function installModule<R>(store: TinyStore<R>, rootState_: R, path: string[], module: ModuleWrapper<any, R>) {
const isRoot = !path.length
// 如果不是根模块
if (!isRoot) {
// 1. 拿到父级的state
const parentState: any = getParentState(rootState_, path.slice(0, -1))
// 2. 拿到当前模块的state和当前模块名合成一个对象,加到父级的state上
parentState[path[path.length - 1]] = module.state
}
// 为每个模块递归installModule
module.forEachChild((child, key) => {
installModule(store, rootState_, path.concat(key), child)
})
}
获取父级state
:
function getParentState<R>(rootState: R, path: string[]) {
return path.reduce((state, key) => {
return (state as any)[key]
}, rootState)
}
收集state
的逻辑同module
类似,通过path
逐级寻找每个层级的模块,而进入某一层的遍历函数forEachChild
定义在ModuleWrapper
模块类中:
class ModuleWrapper<S, R> {
// 省略...
forEachChild(fn: ChildMdleWraperToKey<R>) {
// 遍历children执行fn函数
Util.forEachValue(this.children, fn)
}
}
// 参数fn约束为接收模块类和key的函数
type ChildMdleWraperToKey<R> = (moduleWrapper: ModuleWrapper<any, R>, key: string) => void
那么installModule
函数需要在那里开始执行呢?当然是TinyStore
的 constructor
中:
class TinyStore<S = any> {
moduleCollection: ModuleCollection<S>
mutations: Record<string, any> = {}
actions: Record<string, any> = {}
commit: Commit
dispatch: Dispatch
constructor(options: StoreOptions<S>) {
this.moduleCollection = new ModuleCollection<S>(options)
const store = this
// 省略...
// 注册state模块
const rootState = this.moduleCollection.root.state
installModule(store, rootState, [], this.moduleCollection.root)
console.log('注册完成', rootState)
}
// 省略...
}
执行完毕,打印下我们的根模块类的
state
,因为在js中对象是引用类型,所以后续的变动也直接反映在根对象中。
注册Getters
getters
中的函数的注册与state
略有不同,在保存getters
函数时,key
应当使用路径来保存,因为我们需要将actions
、mutations
、getters
保存在TinyStore
类中,所以需要维护路径。
例如:
getters: {
'mineModule/foodModule/handlerNavList': (payload)=> {
// ...
}
}
收集getters
函数的操作依然在installModule
函数中处理。
function installModule<R>(store: TinyStore<R>, rootState_: R, path: string[], module: ModuleWrapper<any, R>) {
const isRoot = !path.length
// 获取当前层级的路径key
const nameSpace = store.moduleCollection.getNameSpace(path)
// 省略...
module.forEachGetter((getter, key) => {
const nameSpaceType = nameSpace + key
// store.getters[nameSpaceType] = getter
Object.defineProperty(store.getters, nameSpaceType, {
get() {
// getter接收当前模块的state
return getter(module.state)
}
})
})
}
当获取到路径组成的key
后,拼接函数名,注册到TinyStore
实例中的getters
属性中,这里使用Object.defineProperty
进行注册,因为我们希望访问到getters
函数后立即执行,直接返回state
数据。
获取模块路径的函数放置在ModuleCollection
类中,实现思路比较简单,获取根模块类,逐层遍历,获取模块的key
,拼接在一起。
class ModuleCollection<R> {
root!: ModuleWrapper<any, R>
// 省略...
getNameSpace(path: string[]) {
// 获取根模块类
let moduleWrapper = this.root
// 遍历,逐层查找key
return path.reduce((nameSpace, key) => {
moduleWrapper = moduleWrapper.getChild(key)
// 拼接
return nameSpace + (moduleWrapper.namespaced ? key + '/' : '')
}, '')
}
}
遍历模块下面getters
对象的操作,还是放到模块类ModuleWrapper
中:
type GetterToKey<R> = (getter: Getter<any, R>, key: string) => any
class ModuleWrapper<S, R> {
children: Record<string, ModuleWrapper<any, R>> = {}
rawModule: Module<any, R>
state: S
namespaced: boolean
// 省略...
forEachGetter(fn: GetterToKey<R>) {
if (this.rawModule.getters) {
Util.forEachValue(this.rawModule.getters, fn)
}
}
}
getter
函数已经成功绑定。
注册Mutations
注册mutations
与上文的getter
逻辑基本一致,不过多赘述了。
function installModule<R>(store: TinyStore<R>, rootState_: R, path: string[], module: ModuleWrapper<any, R>) {
const isRoot = !path.length
// 获取当前层级的路径key
const nameSpace = store.moduleCollection.getNameSpace(path)
// 省略...
module.forEachMutation((matation, key) => {
const nameSpaceType = nameSpace + key
store.mutations[nameSpaceType] = (payload: any) => {
matation.call(store, module.state, payload)
}
})
}
type MutationToKey<S> = (mutation: Mutation<S>, key: string) => any
class ModuleWrapper<S, R> {
children: Record<string, ModuleWrapper<any, R>> = {}
rawModule: Module<any, R>
state: S
namespaced: boolean
// 省略...
forEachMutation(fn: MutationToKey<S>) {
if (this.rawModule.mutations) {
Util.forEachValue(this.rawModule.mutations, fn)
}
}
}
注册Actions
注册actions
也基本与上文一致,只不过传递给action
的函数的参数不同于mutations
和getter
函数,actions
函数的第一个参数接受一个对象,可以结构出来commit
、dispatch
等方法(这里只实现commit
),需要我们将TinyStore
中的commit
方法传递过来。
function installModule<R>(store: TinyStore<R>, rootState_: R, path: string[], module: ModuleWrapper<any, R>) {
const isRoot = !path.length
// 获取当前层级的路径key
const nameSpace = store.moduleCollection.getNameSpace(path)
// 省略...
module.forEachAction((action, key) => {
const nameSpaceType = nameSpace + key
store.actions[nameSpaceType] = (payload: any) => {
// 传递commit,用于触发mutation
action.call(store, { commit: store.commit }, payload)
}
})
}
type ActionToKey<S, R> = (action: Action<S, R>, key: string) => any
class ModuleWrapper<S, R> {
children: Record<string, ModuleWrapper<any, R>> = {}
rawModule: Module<any, R>
state: S
namespaced: boolean
// 省略...
forEachAction(fn: ActionToKey<S, R>) {
if (this.rawModule.actions) {
Util.forEachValue(this.rawModule.actions, fn)
}
}
}
截止到现在我们的模块注册已经接近尾声了,各个模块已经全都注册成功。🎉🎉
一个小问题🐛
其实我们在处理actions
函数传入的commit
函数时是有问题的,来看一下我们是怎么做的:
module.forEachAction((action, key) => {
const nameSpaceType = nameSpace + key
store.actions[nameSpaceType] = (payload: any) => {
action.call(store, { commit: store.commit }, payload)
}
})
这传入commit
函数时我们是直接将TinyStore
类中的commit
方法直接传入的,那么相应的,我们在使用是就会这样写:
const foodModule = {
actions: {
handlerNavList({ commit }) {
setTimeout(() => {
const navList = [4, 5, 6]
commit('modifyNavList', navList)
}, 600)
}
}
}
在foodModule
模块中我们定义的action函数
,直接使用mutation
的函数名modifyNavList
作为key
来触发,但是还记得我们注册后的mutations
的对象是怎么定义的吗?
{
'homeModule/modifyHomeListayload': () => {}
'mineModule/foodModule/modifyNavList': () => {}
'mineModule/modifyNavList': () => {}
}
函数的key
已经被我们全部更改为了路径地址,所以我们直接拿着函数名是无法触发函数的。
如果能够自动拦截commit
,并且拼接上路径就好了,那么如何做呢?还记得我们的注册actions
是在那个函数中进行的吗?installModule
!而且installModule
函数是会递归调用的,也就是说我们在installModule
函数可以直接使用path
来获取整个路径地址!
interface ActionContext<S, R> {
dispatch?: Dispatch,
commit: Commit,
state?: S
}
function installModule<R>(store: TinyStore<R>, rootState_: R, path: string[], module: ModuleWrapper<any, R>) {
const isRoot = !path.length
// 获取路径地址,当前的path包含当前模块的模块名
const nameSpace = store.moduleCollection.getNameSpace(path)
// 解决直接使用commit,路径错误,重置commit
const actionContext: ActionContext<any, R> = makeLocalContext(store, nameSpace)
module.forEachAction((action, key) => {
const nameSpaceType = nameSpace + key
store.actions[nameSpaceType] = (payload: any) => {
// 替换为拦截后的actionContext
action.call(store, { commit: actionContext.commit }, payload)
}
})
}
在makeLocalContext
函数中我们做了一层拦截,如果nameSpace
不为空,此时就不是位于根模块,直接使用一个新函数进行替换,新的函数中使用nameSpace
拼接key
,然后再执行原来的commit
方法。
function makeLocalContext<R>(store: TinyStore<R>, nameSpace: string) {
const noNameSpace = nameSpace === ''
const actionContext: ActionContext<any, R> = {
// 是否为根模块
commit: noNameSpace ? store.commit : (type, payload) => {
type = nameSpace + type
store.commit(type, payload)
}
}
return actionContext
}
大功告成啦!!! 🥳🥳
写在最后 ⛳
未来可能会更新typescript
和react
基础知识系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳
转载自:https://juejin.cn/post/7209625823581831224