手把手讲解:Vuex 剖析与简单实现

更多个人博客:https://github.com/zenglinan/blog 目录 install 方法 Store 类 State Mutations Actions 小结一下 Modules 闲话不多说,让我们开始实现一个简单的 Vuex 首先,回想一下 Vuex 的使用方法,这里给出一个简单的使用例子: import Vuex from 'vuex' Vue.use(Vuex) const store = new Vuex.Store({ state: { a: 1 }, getters: { aPlus(state){ return state.a + 1 } }, mutations: { addA(state, payload){ state.a += payload } }, actions: { asyncAddA({commit, state}, payload){ setTimeout(() => { commit('addA', payload) }, 1000) } }, modules: { a: { modules: { c: { state: { c1: 'c1' } } } }, b: { state: { b: 1 }, mutations: { bPlus(state, payload){} }, getters: { bPlus(state){} } } } }) // 根组件实例 new Vue({ store }) 可以看到,'vuex' 导出来的应该是一个对象,上面有至少两个属性:install 和 Store,install 方法在 Vue.use(Vuex) 时会执行,Store 是一个类,我们先把大体的架子写出来: let Vue // 后面要用到 class Store{ constructor(options){ // ... } } function install(){} export { install, Store } 1. install 方法 接下来,我们看一下 install 方法的实现: 用过 Vuex 的都知道:当在根组件注入 store 后,每个子组件都能访问到这个 store,这其实就是 install 帮我们做到的,先来看看这个方法的实现: let Vue function install(_Vue){ // 重复安装 Vuex if(Vue) throw new Error('Vuex instance can only exist one!') Vue = _Vue // 将 vm.$options.$store 注入到每个子组件的 vm.$store 中 Vue.mixin({ beforeCreate() { if(this.$options && this.$options.store){ this.$store = this.$options.store } else { this.$store = this.$parent && this.$parent.$store } } }) } Vue.use(Vuex) 会将 Vue 传入 install 方法,这个全局变量可以用来判断是否已经被 use 过了,重复则报错。 如果没有重复,使用 Vue.mixin 混入一段代码,这段代码会在 beforeCreate 这个钩子函数执行之前执行 核心代码是这几行: if(this.$options && this.$options.store){ this.$store = this.$options.store } else { this.$store = this.$parent && this.$parent.$store } this 指向当前组件实例,如果当前组件实例的 $options 上有 store 属性,说明该实例是注入 store 的根实例,直接往 $store 上挂载 store 反之,else 中会去检查当前实例的父组件: $parent 上有没有 $store,有则在实例上挂载该属性 因为组件的生命周期顺序是:父组件先创建,然后子组件再创建,也就是说子组件执行 beforeCreate 钩子函数时,父组件的 store 已经注入了,所以可以实现循环注入。 2. Store 类 接下来,我们实现一下 Store 类: Store 中有四部分需要我们实现:State、Getter、Mutation、Action,我们先来实现 State: (1) State 通过 $store.state.xxx 即可访问到 vuex 中的 state 状态 class Store{ constructor(options){ this._state = new Vue({ data:options.state }) } get state(){ return this._state } } 这里我们不直接通过 store.state 提供访问,而是通过访问器形式提供访问,可以避免 state 被外部直接修改。 另外要注意的一个点是:我们借用 Vue 让 state 变成响应式数据了。这样的话,当 state 变化的时候,依赖的地方都能得到更新通知 以上,我们简单实现了 State 状态 (2) Getters 接下来实现 Getters,Getters 是通过 store.getters.x 的形式访问的 首先,将 options 中的 getters 遍历,将每个属性逐个挂载到 store.getters 上。 因为 getters 的特点是:访问属性,返回函数执行,很容易想到可以用访问器实现。 constructor(options){ // ... this.getters = {} Object.keys(getters).forEach(k => { Object.defineProperty(this._getters, k, { get: ()=>{ // 箭头函数保证 this 能够访问到当前 store 实例 return getters[k](this.state) } }) }) } (3) Mutations mutations 通过 store.commit(type, payload) 触发,commit 内部会通过 type 取到 this._mutations 上对应的 mutation,将 payload 传入并执行 我们需要将 options 上的 mutations 上进行遍历,定义到 this._mutations 上,之所以这样重新定义一遍是为了能够在 mutation 函数外面封装一层,方便传入 state constructor(options){ // ... this._mutations = {} Object.keys(mutations).forEach(k => { this._mutations[k] = (payload) => { mutations[k](this.state, payload) // 注意 state 参数的传入 } }) } commit = (type, payload) => { // 箭头函数,保证 this 指向 store return this._mutations[type](payload) } (4) Actions actions 的实现和 mutations 相似,就不再赘述了。 不同点在于: (1) 执行回调传入的参数不同 (2) dispatch 返回的应该是一个 Promise constructor(options){ // ... this._actions = {} Object.keys(actions).forEach(k => { this._actions[k] = (payload) => { actions[k](this, payload) // 这里直接将整个 store 传入了 } }) } dispatch = (type, payload) => { return new Promise((resolve, reject) => { try{ resolve(this._actions[type](payload)) } catch (e){ reject(e) } }) } (5) 小结一下 OK,现在让我们把上面的这些代码片段写到一块 let Vue class Store{ constructor(options){ this._state = new Vue({ data: { state: options.state } }) // 生成 Getters、Mutations、Actions this.generateGetters(options.getters) this.generateMutations(options.mutations) this.generateActions(options.actions) } generateGetters(getters = {}){ this.getters = {} Object.keys(getters).forEach(k => { Object.defineProperty(this.getters, k, { get: ()=>{ return getters[k](this.state) } }) }) } generateMutations(mutations = {}){ this._mutations = {} Object.keys(mutations).forEach(k => { this._mutations[k] = (payload) => { mutations[k](this.state, payload) } }) } generateActions(actions = {}){ this._actions = {} Object.keys(actions).forEach(k => { this._actions[k] = (payload) => { actions[k](this, payload) } }) } commit = (type, payload) => { return this._mutations[type](payload) } dispatch = (type, payload) => { return new Promise((resolve, reject) => { try{ resolve(this._actions[type](payload)) } catch (e){ reject(e) } }) } get state(){ return this._state.state } } function install(_Vue){ if(Vue) throw new Error('Vuex instance can only exist one!') Vue = _Vue Vue.mixin({ beforeCreate() { if(this.$options && this.$options.store){ this.$store = this.$options.store } else { this.$store = this.$parent && this.$parent.$store } } }) } export default { install, Store } (6) Modules 接下来,我们要实现 Modules。准备好,前面的代码要发生变动了 我们先贴一下前面的使用例子: new Vuex.Store({ // ... modules: { a: { modules: { c: { state: { c1: 'c1' } } } }, b: { state: { b: 1 }, mutations: { bPlus(state, payload){} }, getters: { bPlus(state){} } } } }) 先总结一下各个属性的访问方式: 对于 c 模块中的状态 c1,访问方式为:store.state.a.c.c1 对于模块的 getters、mutations、actions,都被定义到 store 上了,通过形如 store.getters.xxx 这种方式访问 好了,现在目标明确了,接下来就把上面提到的两个点实现: 首先,我们需要将 store 上 getters、mutations、actions 进行初始化成空对象 我们不再需要用 generateGetters 等方法对 getters、mutations、actions 这些属性进行处理了,这部分代码可以删掉了。在 installModule 的过程中我们会对这些属性进行统一处理。 不过不用担心,我们前面讲到的各个属性的处理方式的核心代码,后面依旧用得上! constructor(options){ this._state = new Vue({ data: { state: options.state } }) // init this.getters = {} this.mutations = {} this.actions = {} // this.generateGetters(options.getters) // this.generateMutations(options.mutations) // this.generateActions(options.actions) let modules = options // 这里取到的 options,实际上就是根模块 installModule(this, this.state, [], modules) // 对各个子模块进行处理的函数,重点关注! } 重点关注一下 installModule 方法,在这个方法里我们实现了 state、getters 等属性的收集处理 先解释一下传给这个函数的四个参数: store 实例 store 上的 state path:在 installModule 中需要递归来安装处理子模块,所以用 path 数组来表示模块的层级关系。数组的最后一位为当前要处理的模块的模块名,最后一位前面的都是当前模块的祖先模块。举个栗子:如果是根模块,传入的 path 为 [],如果传入 [a, c],说明当前处理模块名为 c,模块层级为:根模块 > a模块 > c模块 module 是当前要处理的模块 我们先来把这个函数的基本架子写好,因为模块的嵌套层数是未知的,所以必须用递归进行模块处理安装。 function installModule(store, state, path, module) { // 如果当前模块还有 modules(即还有子模块),递归进行模块安装 if(module.modules){ // 遍历当前模块的子模块 eachObj(module.modules, (modName, mod)=>{ // 将传入的 path 拼接当前要处理的的模块名,得到模块层级数组并传入,进行子模块安装 installModule(store, state, path.concat(modName), mod) }) } } 注意这里的 path.concat(modName) 是为了将祖先模块名拼接到 path 中,这个模块层级数组在后面需要用到,我们后面会讲 另外,这里把遍历对象的方法封装到了 eachObj 中,让代码看起来简洁一点: function eachObj(obj, callback){ Object.keys(obj).forEach(k => { callback(k, obj[k]) }) } 接下来我们把模块的 getters、mutations、actions 挂载到根模块的相应属性上,这三者的处理方式大同小异,要注意的点就是: 同名 mutations、actions 不会被覆盖,他们会被依次执行。所以 store.mutations.xxx 和 store.actions.xxx 应该是一个数组,但 getters 不允许同名,直接挂载到 store.getters 上即可 function installModule(store, state, path, module) { let getters = module.getters || {} // 将模块的 getters 定义到 store.getters 上 eachObj(getters, (k, fn) => { Object.defineProperty(store.getters, k, { get(){ return fn(module.state) // 注意这里传入的 state 是当前模块的局部 state } }) }) let mutations = module.mutations || {} eachObj(mutations, (k, fn) => { const rootMut = store.mutations // 根模块的 mutations // 先检查 rootMut[k] 是否被初始化了,没有的话初始化为空数组 if(!rootMut[k]) { rootMut[k] = [] } rootMut[k].push((payload)=>fn(module.state, payload)) }) // actions 类似 mutations 的实现 let actions = module.actions || {} eachObj(actions, (k, fn) => { const rootAct = store.actions if(!rootAct[k]){ rootAct[k] = [] } rootAct[k].push((payload)=>fn(store, payload)) }) if(module.modules){ // 递归处理模块 eachObj(module.modules, (modName, mod)=>{ installModule(store, state, path.concat(modName), mod) }) } } 接下来我们要实现 state 的挂载,这部分代码相对上面难理解一点: function installModule(store, state, path, module) { let parent = path.slice(0, -1).reduce((state, cur) => { return state[cur] }, state) Vue.set(parent, path[path.length - 1], module.state) // 省略处理 getters、mutations、actions 的代码 // 省略递归处理模块的代码 } 为了保证后面的思路不会乱掉,这里还是要再强调一下 path 的含义: path 数组来表示模块的层级关系,如果是根模块,传入的 path 为 [],如果传入 [a, c],说明当前处理模块名为 c,模块层级为:根模块 > a模块 > c模块 接下来剖析代码: path.slice(0, -1) 返回去除了最后一个元素的数组(注意:数组本身不会被修改),这个数组剩下的元素其实就是当前处理模块的祖先模块们,将这个数组进行 reduce 处理,累计值初始为 state(也就是 store.state),最后返回父链。 举个栗子: 如果 path 为 [],即当前处理模块为根模块,经过 reduce 后返回 state 如果 path 为 [a, c],经过 reduce 后返回 state.a,最终 c 模块的 state 会被挂载在 state.a.c.state 上面 挂载代码如下: Vue.set(parent, path[path.length - 1], module.state) 对于上面的例子,即:Vue.set(state.a, 'c', c.state) 另外,使用 Vue.set 是为了保证数据响应式。 以上,我们的简易版 Vuex 就实现完了。 另外,值得一提的是:在我们的代码中,直接通过 let modules = options 取得了根模块,而在 Vuex 源码中实际还有一个模块收集的过程,这个方法会将模块收集成一个如下的树结构 { _raw: {...}, _children: {...}, state: {...} } _raw 表示 options 传入的模块的原生形式,_children 中包含了该模块的子模块,state 为该模块的 state 状态 感兴趣的话可以翻阅 Vuex 源码,或者看看笔者的实现。 感谢阅读,若以上讲述有所纰漏还望指正。

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):