Vuex原理

129 阅读5分钟

Vuex的用法

安装Vuex

先在Vue 中安装Vuex

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

配置Vuex

安装好Vuex 后需要实例化一个Store 对象,用于管理我们的数据。Store 的配置项包括:

  • state 管理的数据信息
  • mutations 存放用于操作数据的方法
  • actions 也是用于存放操作数据的方法,区别在于:
    • actions 不直接操作state 里面的数据,而是通过commit 方法来调用mutations 里面的方法进行操作数据,可以理解为封装了一层mutations 方法调用。
    • actions 可以异步操作,并且可以多次commit 调用,以修改不同的state ,相当于一个mutations
  • getters 类似于Vue 中的计算属性,会将该值缓存起来,只有其依赖的数据改变后,才会重新去计算该值
  • modules 将数据进行模块分层,每个module 都拥有自身的 statemutationsactionsgettersmodulues
new Vuex.Store({
	state: { // state -> 等同于data
    name: 'zxl',
    age: 10
  },
  // commit调用
  mutations: { // 等同于methods
    changeAge(state, payload) {
      console.log('state is:', state)
      state.age += payload
    }
  },
  // dispatch调用action
  actions: { // 异步操作,可以多次commit mutation
    changeAge(store, payload) {
      setTimeout(() => {
        store.commit('changeAge', payload)
      }, 1000)
    }
  },
  getters: { // 等同于计算属性
    myAge(state) {
      // 多次调用getter,只要不更新值都不会再次调用
      console.log('ok')
      return state.age + 10
    }
  },
  modules: { // 模块分割
    // 子模块的名字不能和父模块中的state属性重名,不然会覆盖父模块中的state属性
    // 分层
    a: {
      /**
       * // 命名空间,解决模块间同名getters冲突的问题,默认会将模块里面个getters合并到根上面去
       * namespaced为false的话,getters会被覆盖挂载到根的getters上, mutations回push到_mutations数组里面
       *
       */
      namespaced: true,
      state: {
        name: 'dajuan',
        age: 23
      },
      getters: {
        /**
         * 注意!!! 所有modules里面的getters会被合并挂载到Store对象的getters属性上
         * 所以同名会被覆盖
         * 可以添加一个namespaced属性来定义同名的getters
         * @param state
         * @returns {number}
         */
        myAge(state) {
          // 多次调用getter,只要不更新值都不会再次调用
          console.log('ok')
          return state.age + 101
        }
      },
      mutations: {
        changeName(state, payload) {
          state.name = payload
        },
        changeAge(state, payload) {
          state.age += payload
        }
      }
    },
    b: {
      state: {
        name: 'dd',
        age: 13
      },
      mutations: {
        changeName(state, payload) {
          state.name = payload
        },
        changeAge(state, payload) {
          state.age += payload
        }
      },
      modules: {
        c: {
          // 这里的b没有namespaced,要调用mutations的话,依然直接使用commit('c/changeAge')就行了
          namespaced: true,
          state: {
            age: 1001
          },
          mutations: {
            changeAge(state, payload) {
              state.age += payload
            }
          }
        }
      }
    }
  }
})

使用Vuex

虽然vuex中的数据可以直接通过$store.state访问到并且修改,但是不推荐这样去修改,这样直接修改的话会让数据流向变得很混乱。

💡 `Vuex`所有修改`state`的操作都应该是在`mutation`中去修改,`action`修改`state`是间接调用`mutation`去修改的`state`
<template>
  <div id="app">
    <h1>哈哈</h1>
    <h1>{{ this.$store.state.name }}</h1>
    <div>
      <h4>Age is:</h4>
      <span>{{ this.$store.state.age }}</span>
      <span>{{ this.$store.getters.myAge }}</span>
      <span>{{ this.$store.getters.myAge }}</span>
    </div>

    <button @click="() => {this.$store.state.age++}">更改年龄</button>
    <button @click="() => {this.$store.commit('changeAge', 8)}">更改年龄2</button>
    <button @click="() => {this.$store.dispatch('changeAge', 8)}">异步更新年龄</button>

    <hr />

    <p>a的信息:</p>
    {{ this.$store.state.a.name }} ----- {{ this.$store.state.a.age }}
    a的getters年龄:{{ this.$store.getters['a/myAge'] }}
    <button @click="() => {this.$store.commit('a/changeAge', 8)}">更改a的年龄2</button>

    <p>b的信息:</p>
    {{ this.$store.state.b.name }} ----- {{ this.$store.state.b.age }}

    <p>c的信息:</p>
    {{ this.$store.state.b.c.age }}
    <button @click="() => {this.$store.commit('c/changeAge', 8)}">更改c的年龄2</button>

    <hr>
    <h3>D模块:</h3>
    开始D模块不存在,后续手动创建,动态注册模块
    <p> name is: {{ $store.state.d && $store.state.d.name }}</p>
    <p> gender is: {{ $store.state.d && $store.state.d.gender }}</p>
    <p> getters.nameGender is: {{ $store.getters.nameGender }}  </p>
    <button @click="registerModule">动态创建模块</button>
  </div>
</template>

<script>
import HelloWorld from './components/HelloWorld.vue'
import store from './store'

export default {
  name: 'App',
  components: {
    HelloWorld
  },
  methods: {
    registerModule() {
      store.registerModule('d', {
        state: {
          name: 'this is d module',
          gender: 'man'
        },
        getters: {
          nameGender(state) {
            return state.name + '------' +state.gender
          }
        }
      })
    }
  }
}
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

手撸Vuex原理

在使用Vuex的过程中,我们需要先使用Vue.use(Vuex)说明需要导出一个install的方法让Vue进行安装Vuex,同时我们也会new一个Store的实例对象用来存储我们的数据,并将其挂载到Vue的根app上,所以Vuex包需要导出一个install方法用于安装Vuex和一个Store类,用于实例化一个实例来存储数据。

在使用Vuex的时候,我们第一步是使用use方法来安装Vuex,然后实例化一个Strore对象,之后将这个Store实例挂载到Vue的根组件上面。但是在我们的使用过程中,不管是在根组件还是在子组件中我们都是通过this.$store来访问store的,这就说明我们实例化出来的Store实例是挂载到了每个组件上面,所以这一点就是在使用Vue.use(Vuex)时主要进行的事情:

💡 将`Store`实例挂载到每个组件上面,同时也会在`install.js`中导出`Vue`,这样就可以确保在整个`Vuex`包中都用的是同一个`Vue`

安装阶段

importinstallfrom"./install";
importStorefrom"./store";

export default{
	install,
  Store
}

index.js就是导出一个install方法和一个Store类。如上文所说,install()方法是Vue在调用Vue.use()时会调用的,而Store就是Vuex的核心,用于管理我们的数据状态。

export let Vue;

export default function install(_Vue) {
  Vue = _Vue;
  Vue.mixin({
    beforeCreate() {
			/**
			* 每个子组件都会调用这个混入的beforeCreate钩子函数,所以在实例化子组件的时候,
			* this就是指向的当前这个子组件
			*/
      let options = this.$options;
      if (options.store) {
        this.$store = options.store;
      } else {
        if (this.$parent && this.$parent.$store) {
          this.$store = this.$parent.$store;
        }
      }
    },
  });
}

install.js导出一个Vuex全局使用的Vue,这个Vue是在Vue.use()调用install方法时传入的Vue类。然后默认导出一个install方法,在这个install方法中使用mixin方法混入一个beforeCreate的钩子方法,在每个Vue组件被实例化的时候都会触发我们定义的这个钩子函数。在钩子函数里面通过$options字段可以获取到我们实例Vue组件时传入的参数,如果存在store字段的话,就将store字段挂载到实例的$store上;如果不存在store字段的话,就去找父组件,父组件存在并且父组件存在$store属性的话,就将父组件的$store属性赋值给子组件的$store字段上。由此操作就可以让所有的组件都同一个Store实例。

核心Store

Store的实现主要实现步骤如下:

  • 注册Module;注册Module就是将传入的options进行格式化,有点像转成AST,这样子方便Vuex对配置项进行处理。
  • 安装Module;安装Module就是处理Module中的配置属性:gettersstatemutationsactionsmodulenamespacedstrict
  • 创建Vue实例;将state挂载到Vue实例的$$data属性上,将getters里面的属性挂载到computed属性上。
  • 处理plugin插件;如果用户在配置项中传入了plugin参数,则调用plugin方法。

Vuex中可以通过modules字段来按照模块的方式对数据进行分层来管理,所以可以把一个Store实例理解为一个大的模块,然后每个modules下面的又是一个子模块。

注册模块

通过实例化一个ModuleCollection来实例化一个实例:

class Store {
	constructor(options) {
		// 注册模块
		this._modules = new ModuleCollection(options)
	}
	...
}

ModuleCollection类接受一个参数options参数,即是用户new Store时传入的配置,也可以理解为一个Module的配置。ModuleCollection类在构造函数类会创建一个this.root的属性用来表示是根Module,然后去调用自身的register方法,register(path, rowModule)方法接受两个参数,第一个参数是Module的路径,第二个参数是Module自身的配置(Module自身的配置也是一个Store实例的配置)。例如模块a下面的模块b下面的模块c,那么模块c的路径就是[a, b, c]register方法中会根据传入的Module配置实例化一个Module类。Module类有三个属性:

  • _row_row表示原始的Module的配置
  • _children_children表示的是当前模块下的子模块
  • state: 表示当前模块的state数据

并且Module类也会提供以下几个方法:

  • getChild(childName): 获取子模块
  • addChild(childName, child)): 新增子模块
  • forEachGetter(cb):获取getters
  • forEachMutation(cb):获取mutations
  • forEachAction(cb):获取actions
  • forEachChild(cb): 获取children

以及一个namespacedgetter方法:

get namespaced() { // namespaced用于标识是否写了namespaced
	return !!this._row.namespaced
}

Module类的构造函数里将传入的Store模块的配置赋值给_row属性,并将配置的state赋值给Modulestate属性。

register方法中实例化了一个Module类后,将该实例挂载到Store子模块的配置属性上。

💡 这样做的目的是了在之后动态添加`Store`模块方法`registerModule`中可以访问到创建的子模块实例。

.判断一下path的长度,如果长度为0的话,说明当前注册的Store模块的是Store的根模块,将Module的是实例赋值给ModuleCollectionroot字段。如果path的长度不为0,则说明不是根模块,而是子模块,则需要找到他的父模块,然后将当前模块的实例挂载到父模块实例的_children字段上(通过调用父模块的addChild(childName, child)方法):

// [a], [a,b], [a,b,c]
// 找父亲
// 拆分路径,从根路径开始查询子模块

let parent = path.slice(0, -1).reduce((memo, current) => {
  return memo.getChild(current)
}, this.root)

parent.addChild(path[path.length - 1], newModule)

在注册完当前的Store模块后,通过modules字段来判断是否存在子模块,如果存在子模块的话,再注册子模块:

if (rawModule.modules) { // 如果传入配置里面有modules
    forEach(rawModule.modules, (module, key) => {
        console.log(`模块${key} is:`, module)
        this.register(path.concat(key), module)
    })
}

同时ModuleCollection类还提供了一个getNamespace(path)方法,用来返回一个Store模块路径的字符串:

/**
 * 获取namespac
 * @param path 路径
 */
getNamespace(path) {
    // 返回一个字符串 a/b/c或者空字符串''
    let root = this.root
    let pathStr = path.reduce((ns, key) => {
        let module = root.getChild(key)
        root = module
        return module.namespaced ? ns + key + '/' : ns
    }, '')
    return pathStr
}

完整的module-collection.js代码如下:

/**
 * this.root = {
 *     _raw: '用户定义的模块',
 *     state: '当前模块的状态',
 *     _children: [ // 子模块列表
 *          a: {
 *               _raw: '用户定义的模块',
 *               state: '当前模块的状态',
 *               _children: [ // 子模块列表
 *          }
 *     ]
 * }
 */
import {forEach} from "../utils";
import Module from "./Module";

export class ModuleCollection {
    constructor(options) {
        // 数据格式化操作
        this.root = null;
        console.log('options is:', options);
        this.register([], options);
    }

    register(path, rawModule) {

        let newModule = new Module(rawModule)

        rawModule.ModuleInstance = newModule

        if (path.length === 0) {
            this.root = newModule
        } else {
            // [a], [a,b], [a,b,c]
            // 找父亲
            // 拆分路径,从根路径开始查询子模块
            let parent = path.slice(0, -1).reduce((memo, current) => {
                return memo.getChild(current)
            }, this.root)
            parent.addChild(path[path.length - 1], newModule)
        }

        // 注册完当前模块,注册子模块
        if (rawModule.modules) { // 如果传入配置里面有modules
            forEach(rawModule.modules, (module, key) => {
                console.log(`模块${key} is:`, module)
                this.register(path.concat(key), module)
            })

        }

    }

    /**
     * 获取namespac
     * @param path 路径
     */
    getNamespace(path) {
        // 返回一个字符串 a/b/c或者空字符串''
        let root = this.root
        let pathStr = path.reduce((ns, key) => {
            let module = root.getChild(key)
            root = module
            return module.namespaced ? ns + key + '/' : ns
        }, '')
        return pathStr
    }
}

安装Module阶段

安装Store的Module就是处理state、getters、mutations等参数配置,调用installModule(store, path, module, rootState)来安装Store的模块。参数的说明如下:

  • storeStore的实例对象
  • pathStore的模块的路径数组
  • moduleStore的已经注册好了的模块
  • rootStateStore根模块的state数据

安装Module主要进行以下几个操作:

  • 处理state
  • 处理getters
  • 处理mutations
  • 处理actions
  • 处理子模块

获取安装模块的路径

let ns = store._modules.getNamespace(path)

通过调用挂在到Store 实例上的getNamespace() 方法,传入当前的Modal 路径数组,就可以得到模块路径的字符串。

处理state

先通过store._modules即是注册好的ModuleCollection实例的getNamespace方法拿到拼接好的路径字符串。判断传入的path长度是否为0,如果不为0的话,说明是子模块,将子模块的状态声明到对应的父模块上去

if (path.length > 0) { // 子模块 [a], [a, b], [a, b, c]
    // 需要找到对应的父模块,将状态声明上去
    //
    let parent = path.slice(0, -1).reduce((memo, current) => {
        return memo[current]
    }, rootState)

    // 新增属性不会触发getter和setter方法,不会变成响应式,不能导致视图更新
    // parent[path[path.length - 1]] = module.state
    store._withCommitting(() => { // 内部的修改,不应该报错
        Vue.set(parent, path[path.length - 1], module.state)
    })
}

截取path的路径可以找到当前安装模块的父路径,然后通过迭代遍历父路径找到当前安装模块的父模块,再调用Vue.set()方法将当前模块的state挂载到父模块的state路径下。例如c模块在a模块下,则挂在根Store的state结构如下:

{
	state: {
		...,
		a: {
			...,
			c: {
				...
			}
		}
	}
}

处理getters

module.forEachGetter((fn, key) => {
        store.wrapperGetters[ns + key] = () => {
            // getters: { // 等同于计算属性
            //     myAge(state) {
            //       // 多次调用getter,只要不更新值都不会再次调用
            //       console.log('ok')
            //       return state.age + 10
            //     }
            // }
            return fn.call(store, getNewState(store, path))
        }
    })

Modal 类上添加一个方法forEachGetter() ,方法接受一个回调方法,回调方法接受一个方法,即是在初始化Vuex时传入的gettes配置里面的方法以及该方法的key 。然后挂在一个方法到Sotre实例的wrapperGetters上面,key为模块的路径字符串加上getters里面配置的key,方法调用getters配置的方法,并用call()来改变其this的指向,使他指向Store实例,并将当前模块的state作为参数传入。

处理Mutations

module.forEachMutation((fn, key) => {
        store.mutations[ns + key] = store.mutations[ns + key] || []
        store.mutations[ns + key].push((payload) => {
            store._withCommitting(() => {
                // 用_withCommitting方法包一层来标记是用mutation来修改的数据
                fn.call(store, getNewState(store, path), payload)
            })
            store._subscribes.forEach(fn => fn({type: ns + key, payload}, store.state))
        })
    })

调用Modal类上面的forEachMutation()方法,从Modal_raw属性(原始的配置属性)上获取到mutations配置,取到key和方法后,然后将方法挂载到store实例的mutations上,键为模块路径加上mutation方法的key。因为是Vuex内部改变了state,因此使用_withCommitting()方法来包裹一层(严格模式下,不是通过commit来改变的state,需要抛出异常);遍历_subscribes字段获取到配置的插件并执行。

处理Actions

module.forEachAction((fn, key) => {
        store.actions[ns + key] = store.actions[ns + key] || []
        store.actions[ns + key].push((payload) => {
            return fn.call(store, store, payload)
        })
    })

同理调用Modal类上的forEachAction()方法获取到配置里面的actions,然后将方法和key挂载到Store类上的actions字段上。

安装子模块

module.forEachChild((child, key) => {
        installModule(store, path.concat(key), child, rootState)
})

通过forEachChild()方法获取到当前模块的子模块,然后子模块调用installModule()方法来安装子模块。

创建响应式

Vuex的数据管理是通过实例化一个Vue对象来管理的,响应式也是依赖于Vue。Vuex的state属性相当于Vue的data属性,而Vuex的getters属性则对应Vue的computed属性。通过遍历store对象上wrapperGetters属性获取到所有模块的的getters(在之前处理getters的时候,已经将getters的键改成了模块路径加key的形式),并且定义store.getters的get方法,当从store.getters里面去取值的时候实际是去Vue实例上面去取值(Vue中会将computed里面的属性挂载到Vue身上)。将state和getters传入Vue并实例化然后挂载到store._vm上。由于可以设置一个strict,如果strict为true的话,则如果不是通过commit来修改state需要抛出异常,所以这里需要watch整个state,并且由于在Vue中watcher是异步的,但是在Vuex是同步的,状态一变化就能监控到,状态变化立即执行,所以需要添加sync属性(Vue官方文档上面没有写,但是Vuex源码中就是这样子调用的)。同时由于可以手动注册新的模块,注册新模块的时候需要重新实例化一个Vue实例,原来的Vue实例就需要给销毁掉。

/**
 * 重置Vue实例
 * @param store
 * @param state
 */
function resetVm(store, state) {
    const oldVm = store._vm
    const computed = {}
    store.getters = {}
    forEach(store.wrapperGetters, (fn, key) => {
        computed[key] = fn

        // 从getters上面取值的时候,
        Object.defineProperty(store.getters, key, {
            get: () => {
                return store._vm[key] // 具备了缓存的功能,因为实际上去获取Vue上面的Computed属性
            }
        })
    })

    store._vm = new Vue({
        data: {
            $$state: state
        },
        computed
    })

    if (store.strict) { // 如果是严格模式,需要监听数据是否是从mutations中去更改的
        store._vm.$watch(() => store._vm._data.$$state, () => {
            // watcher默认是异步的, 但是vuex这里需要是同步的,状态一变化就能监控到,状态变化立即执行
            // 所以需要添加sync属性
            console.assert(store._committing, '要用mutation去修改数据')
        }, {deep: true, sync: true})
    }

    if (oldVm) {  // 如果老的Vue实例存在,需要将其卸载
        Vue.nextTick(() => {
            oldVm.$destroy()
        })
    }
}

安装插件

if (options.plugins) { // 是否传入了插件属性
            options.plugins.forEach((plugin) => {
                return plugin(this)
            })
        }

就是遍历插件,然后调用插件方法。将当前的Store实例作为参数传给插件方法。

Store提供的方法

subscribe(fn)

subscribe(fn) {
    this._subscribes.push(fn)
}

提供给插件使用,在插件中调用此方法,会往_subscribes里面推入一个方法,该方法会在执行mutations后执行。

_withCommitting(fn)

_withCommitting(fn)方法用来判断是否是通过commit来修改的state,因为Store类中默认的this._committingfalse,而在resetVm()中侦听数据改变时,如果this._committingfalse的话就会报错。

_withCommitting(fn) {
        this._committing = true; // 先将_committing改为true
        // mutation修改state是同步的,所以当mutation修改state后,会直接触发Vue的watcher,由于设置了watcher是同步更新的,所以在watcher里面获取
        // 到的store._committing是true
        fn();
        this._committing = false;
    }

replaceState(newState)

replaceState(newState)方法是用来替换最新的state

replaceState(newState) {
        this._withCommitting(() => {
            this._vm._data.$$state = newState; // 替换为最新的state
            // 直接替换_vm._data.$$state的缺陷在于,new Vue是在ModuleCollection以及installModule之后的,就是在Vuex中格式化数据中的state以及模块安装时getter、mutation中的state不一致
        })
    }

dispatch(actionName, payload)

dispatch(actionName, payload)方法用于派发action

dispatch = (actionName, payload) => {
        const actions = this.actions[actionName];
        if (actions) {
            actions.forEach((fn) => {
                fn(payload)
            })
        }
    }

需要注意的是这里获取到的action是在调用installModule()方法安装actions时包装过的方法。

commit(mutationName, payload)

commit(mutationName, payload)方法用于提交修改。

commit = (mutationName, payload) => {
        const mutations = this.mutations[mutationName];
        if (mutations) {
            mutations.forEach((fn) => {
                fn(payload)
            })
        }
    }

同样,这里获取到fn也是在installModule()方法中进行包装过的,在更新完数据后,会遍历_subscribes字段执行里面的方法。

registerModule(path, module)

registerModule(path, module)方法用于注册一个模块。

/**
     * 注册模块
     * @param {String | String[]} path
     * @param {Store} module
     */
    registerModule(path, module) { // path可以传入字符串,或者一个路径数组,但是最终都会转成一个路径数组
        if (typeof path === 'string') {
            path = [path]
        }
        this._modules.register(path, module) // 注册模块

        installModule(this, path, module.ModuleInstance, this.state) // 安装模块

        resetVm(this, this.state)
    }

一样的套路,调用register()方法来将新的Module添加到AST树中,然后调用installModule()方法来安装该模块,最后因为安装新的模块,stategetters信息可能有所改动,所以再重置一下Vue实例。

辅助方法

mapStates(stateList)

function mapStates(stateList) {
    const obj = {};
    stateList.forEach((state) => {
        obj[state] = function () {
            return this.$store.state[state]
        }
    })

    return obj;
}

mapGetters(getterList)

function mapGetters(getterList) {
    const obj = {};
    getterList.forEach((getter) => {
        obj[getter] = function () {
            return this.$store.getters[getter]
        }
    })

    return obj;
}

mapMutations(mutationList)

function mapMutations(mutationList) {
    const obj = {};
    mutationList.forEach((mutation) => {
        obj[mutation] = function (payload) {
            return this.$store.commit(mutation, payload)
        }
    })

    return obj;
}

mapActions(actionList)

function mapActions(actionList) {
    const obj = {};
    actionList.forEach((action) => {
        obj[action] = function (payload) {
            return this.$store.dispatch(action, payload)
        }
    })

    return obj;
}

mapMutationsmapActions的使用

new Vue({
	...mapMutations(['mutationsName']),
  ...mapActions(['actcionName'])
})

mapMutationsmapActions也还是相当于包装了一层方法,方法接受payload参数,然后将这个参数传入mutations或者actions中去。