【造轮子系列】面试官问:你能手写Vuex吗 (二)?

415 阅读2分钟

freysteinn-g-jonsson-s94zCnADcUs-unsplash.jpg

书接上文:【造轮子系列】面试官问:你能手写Vuex吗(一)? 中,实现了 Vuex 的基础功能,现在继续对其进行完善,实现模块化的状态管理。模块化可以帮助我们更好地组织和管理复杂的应用状态,使得状态的结构更加清晰和可维护。

格式化参数

将参数模块格式化为模块嵌套的树形结构(如下),方便我们后续的操作。

//根模块
this.root = {   // 模块的配置:包含当前模块的 state、getters、mutations、actions
    _raw: xxx,
    _children: { // 子模块
      a模块: {
        _raw: xxx,
        _children: {},
        state: xxx.state
      },
      b模块: {
        _raw: xxx,
        _children: {},
        state: xxx.state
      }
    },
    state: xxx.state
}

Module 类

创建 Module 类,通过 new Module 便可以得到格式化后的树形数据结构。

// module.js
export default class Module {
    constructor(rootModule) {
        this._raw = rootModule,
        this._children = {}
        this.state = rootModule.state
    }
}

ModuleCollection 类

在这个类中,实现将用户传入的参数转化为格式化后的结果,代码如下:

// module-collection.js
import forEachValue from "./utils";
import Module from './module.js'
export default class ModlueCollection {
    constructor(options) {
        // 注册模块 []表示路径 递归注册模块
        this.register([], options)
    }
    register(path, rootModule) {
        const newModlue = new Module(rootModule)

        if (path.length == 0) { // 根模块
            this.root = newModlue
        } else {
            const parent = path.slice(0, -1).reduce((pre, next) => {
                return pre.getChild(next)
            }, this.root)
            parent.addChild(path[path.length - 1], newModlue)
        }
        // 注册子模块
        if (rootModule.modules) {
            forEachValue(rootModule.modules, (moduleValue, moduleName) => {
                // 递归
                this.register([...path, moduleName], moduleValue)
            })
        }
    }
}

forEachValue

其中 forEachValue 方法提取为工具方法,方便后续复用

const forEachValue = (obj = {}, fn) => {
    Object.keys(obj).forEach(key => {
        fn(obj[key], key)
    })
}
export default forEachValue

getChild、addChild

增加获取子模块和追加子模块方法,便于调用

// module.js
export default class Module {
    ...
    // 获取子模块
    getChild(key) {
        return this._children[key]
    }
    // 追加子模块
    addChild(key, module) {
        this._children[key] = module
    }
}

至此完成模块格式化为模块嵌套的树形结构,接下来重构 Store,实现 state、getter、commit、dispatch等

installModule

installModlue方法:将创建的树形结构上的状态、方法安装到 Store 实例上,就可以通过$store方式获取到对应的数据。

import ModuleCollection from './module-collection.js'
import forEachValue from './utils'
let Vue
function installModule(store, rootState, path, module) {
    // 收集所有模块的状态
    if (path.length > 0) { // 如果是子模块 就需要将子模块的状态定义到根模块上
        let parent = path.slice(0, -1).reduce((pre, next) => {
            return pre[next]
        }, rootState)
        // 将属性设置为响应式 可以新增属性
        Vue.set(parent, path[path.length - 1], module.state)
    }
    module.forEachChild((child, key) => {
        installModule(store, rootState, path.concat(key), child)
    })
}
class Store {
    constructor(options) {
        this._modules = new ModuleCollection(options)
        // 注册所有模块到Store实例上 
        // this当前实例、根状态、路径、根模块
        const state = this._modules.root.state
        installModule(this, state, [], this._modules.root)
    }
}

forEachChild

遍历安装当前模块的子模块

// module.js
export default class Module {
    ...
    // 遍历当前模块的child
    forEachChild(fn) {
      forEachValue(this._children, fn)
    }
}

resetStoreVm

实现 state 数据响应式响应式

function resetStoreVm(store, state) {
    const wrappedGetters = store._wrappedGetters
    const computed = {}
    store.getters = Object.create(null)
    // 通过使用vue的computed实现缓存
    forEachValue(wrappedGetters, (fn, key) => {
        computed[key] = () => {
            return fn()
        }
        // 代理
        Object.defineProperty(store.getters, key, {
            get: () => { return store._vm[key] }
        })
    })
    // 将状态实现响应式
    store._vm = new Vue({
        data() {
            return {
                $$state: state
            }
        },
        computed
    })
}

class Store {
    constructor(options) {
        ...
        let state = this._modules.root.state
        ...
        //实现state响应式
        resetStoreVm(this, state)
    }

    get state() {
        return this._vm._data.$$state
    }
    ...
}

forEachGetters

扩展 Module 类,遍历 getters

// module.js
export default class Module {
    ...
    // 遍历当前模块的getters
    forEachGetters(fn) {
        if (this._raw.getters) {
            forEachValue(this._raw.getters, fn)
        }
    }

}

getter

function installModule(store, rootState, path, module) {
    ...
    module.forEachGetters((getters, key) => {
        // 同名计算属性会覆盖 所以不用存储
        store._wrappedGetters[key] = () => {
            return getters(module.state)
        }
    })
  ...
}

class Store {
    constructor(options) {
        ...
        this._wrappedGetters = Object.create(null)  // 存放所有模块的getters
        ...
    }
}

commit(mutations)

function installModule(store, rootState, path, module) {
    ...
    module.forEachMutations((mutations, type) => {
        // 收集所有模块的mutations 存放到 实例的store._mutations上
        // 同名的mutations和 actions 并不会覆盖 所以要有一个数组存储 {changeAge: [fn,fn,fn]}
        store._mutations[type] = (store._mutations[type] || [])
        store._mutations[type].push((payload) => {
            // 函数包装 包装传参是灵活的
            // 使this 永远指向实例 当前模块状态 入参数
            mutations.call(store, module.state, payload)
        })
    })
    ...
}

class Store {
    constructor(options) {
        ...
        this._mutations = Object.create(null)   // 存放所有模块的mutation
        ...
    }
    commit = (type, payload) => {
        // 触发commit会触发_mutations里面的方法
        this._mutations[type].forEach(fn => fn(payload))
    }
}

forEachMutations

// module.js
export default class Module {
    ...
    // 遍历当前模块的mutations
    forEachMutations(fn) {
        if (this._raw.mutations) {
            forEachValue(this._raw.mutations, fn)
        }
    }

}

dispatch(actions)

function installModule(store, rootState, path, module) {
    ...
    module.forEachActions((actions, type) => {
        store._actions[type] = (store._actions[type] || [])
        store._actions[type].push((payload) => {
            actions.call(store, store, payload)
        })
    })
    ...
}

class Store {
    constructor(options) {
   			...
        this._actions = Object.create(null)     // 存放所有模块的actions
        ...
    }
    
    // dispatch
    dispatch = (type, payload) => {
        this._actions[type].forEach(fn => fn(payload))
    }
}

forEachActions

// module.js
export default class Module {
    ...
    // 遍历当前模块的actions
    forEachActions(fn) {
        if (this._raw.actions) {
            forEachValue(this._raw.actions, fn)
        }
    }

}

namespace

默认情况下(或 namespace: false),模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。

如果想要模块具有更高的封装度和复用性,可以通过添加 namespaced: true 的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名

getNamespaced

// module-collection.js
export default class ModlueCollection {
    ...
     // 获取命名空间
    getNamespaced(path) {
        let root = this.root
        return path.reduce((pre, next)=>{
            // 获取子模块 查看是否有namespaced属性
            root = root.getChild(next)
            // 拼接上有namespace属性的路径
            return pre + (root.namespaced ? next + '/' :'')
        }, '')
    }
}
// module.js
export default class Module {
    ...
    get namespaced () {
        return this._raw.namespaced
    }
}

添加 namespace

function installModule(store, rootState, path, module) {
    // 获取命名空间
    const namespaced  = store._modules.getNamespaced(path)
    
    module.forEachMutations((mutations, type) => {
        // 添加 namespace
        store._mutations[namespaced + type] = (store._mutations[namespaced + type] || [])
        store._mutations[namespaced + type].push((payload) => {
            mutations.call(store, module.state, payload)
        })
    })
    module.forEachActions((actions, type) => {
      // 添加 namespace
        store._actions[namespaced + type] = (store._actions[namespaced + type] || [])
        store._actions[namespaced + type].push((payload) => {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key) => {
        store._wrappedGetters[namespaced + key] = () => {
            return getters(module.state)
        }
    })
  	...
}

严格模式

在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

接收 strict 配置项,添加 mutation 提交状态标识

class Store {
    constructor(options) {
        ...
        // strict mode
        const { strict = false } = options
        this.strict = strict
        // 添加 committing 状态
      	this._committing = false
        //实现状态响应式
        resetStoreVm(this, state)
    }
    // 在 mutation 之前,设置 _committing = true, 调用 mutation 之后更改状态
    // 如此当状态变化时,_committing 为 true,说明是同步更改,false 说明是非 mutation 提交
    _withCommit = (fn) => {
        var committing = this._committing;
        this._committing = true;
        fn();
        this._committing = committing;
    }
  	// 修改 commit 方法,使用 _withCommit 调用 mutation
  	commit = (type, payload) => {
        if (!this._mutations[type]) {
            throw new Error(`Mutation "${type}" not found`);
        }
        const entry = this._mutations[type]
        this._withCommit(() => {
            entry.forEach((handler) => {
                handler(payload)
            })
        })
    }
}
function resetStoreVm(store, state) {
    ...
    // 如果开启了严格模式,则调用 enableStrictMode
    if (store.strict) {
        enableStrictMode(store)
    }
}

enableStrictMode

监听 state 数据变化,判断 _committing 如果是 true 表示是同步执行,如果为 false,则会抛出错误提示

function enableStrictMode(store) {
    store._vm.$watch(function () { return this._data.$$state }, () => {
        console.assert(store._committing, `[vuex] do not mutate vuex store state outside mutation handlers.`)
    }, { deep: true, sync: true })
}

以上只是基于vuex源码实现了最核心的功能,但它帮助我们更好地理解了 Vuex 的模块化的实现原理。本来计划在这篇中实现 map辅助函数,但发现模块化的写起来实在太多了,所以将 mapState、 mapGetters、 mapMutations、 mapActions 放到下篇讲解。希望这篇文章对你有所帮助!更多详细信息请参考 vuex 源码

完整代码

系列文章

我的更多前端资讯

欢迎大家技术交流 资料分享 摸鱼 求助皆可 —链接