Vuex源码解析
- Vuex的使用:
只涉及最基础的挂载,具体api的使用,请自行学习。
-
- 引入Vuex
-
- 挂载到Vue上
-
- 创建一个store实例
-
- 把store实例挂载到vue实例上.就是放入main.js中的vue实例上
-
// 下面代码是在store文件下的index.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(vuex)
export default new Vuex.Store({}) // 通常我们把实例Vuex的代码,放到store文件下。
// 下面代码是在main.js
import store from "./store";
let v = new Vue({
router,
store, // 把store放到Vue的options中,这样vuex就能把store挂载到每一个vue实例下的$store属性
render: (h) => h(App),
}).$mount("#app");
Vuex的挂载流程以及挂载原理
- 首先实现Vuex基础版本之前,先知道Vuex挂载到vue实例上的流程。
- 第一步:通过
Vue.use(vuex)
挂载插件到Vue上。注意Vue.use()
的使用一定 是在new Vue({})
之前的 - 第二步:执行插件或者执行插件中的install函数。
Vue.use
会查看插件有没有暴露install
函数,有的话,会执行插件中的install
函数。如果没有的话就执行该插件
(这种情况下:插件默认暴露出来的要是一个方法)。Vue.use()
具体实现代码如下
function initUse(Vue) { Vue.use = function (plugin) { var installedPlugins = // 这个this代表的是Vue实例 // 首先判断Vue构造函数上有没有_installedPlugins属性 // 这个属性代表Vue实例注册的插件 // 如果没有的_installedPlugins属性的话,就给它创建并赋值为[] this._installedPlugins || (this._installedPlugins = []); // 查看Vue.use(xx)传进来的插件有没有注册过 // 如果有注册过的话,返回Vue构造函数 if (installedPlugins.indexOf(plugin) > -1) { return this; } // 如果没有注册过的话 // 获取Vue.use(xxx,xxx,xxx)中的除第一个参数以外的参数 var args = toArray(arguments, 1); // 然后追加一个this到第一个参数 args.unshift(this); // 如果插件中存在install属性,且是方法的话就执行这个方法 if (typeof plugin.install === "function") { // 执行方法,并使install指向插件本身 plugin.install.apply(plugin, args); } else if (typeof plugin === "function") { // 直接执行 plugin.apply(null, args); } // 然后在installedPlugins注册组件 installedPlugins.push(plugin); // 返回Vue构造函数 return this; }; } // 将list转换为数组,并删除list的前start个元素 function toArray(list, start) { start = start || 0; var i = list.length - start; var ret = new Array(i); while (i--) { ret[i] = list[i + start]; } return ret; }
install
执行的时候,第一个参数一定是Vue
。这是在Vue.use()
中处理的。而vuex
中的install
的功能就是通过Vue.mixin()
混入一个beforeCreatd
钩子函数,该钩子函数的作用就是判断main.js
中有没有传入store
对象。如果有的话就把store混入
到所有vue实例的$store
.这样我们就能在任何vue实例中使用$store
了
export const install = function (_Vue) { Vue = _Vue; // 使用vue提供的方法 Vue.mixin混入 方法和数据 Vue.mixin({ beforeCreate() { let options = this.$options; // console.log(this.$options.store); if (options.store) { this.$store = options.store; } else { this.$store = this.$parent && this.$parent.$store; } }, }); };
- 第三步: 就是实例化一个store对象
export default new Vuex.Store({})
- 第四部: 就是把这个store挂载到vue实例上。
import store from "./store"; let v = new Vue({ router, store, // 把store放到Vue的options中,这样vuex就能把store挂载到每一个vue实例下的$store属性 render: (h) => h(App), }).$mount("#app");
- 第一步:通过
简易版Vuex的实现
- 由以上Vuex挂载流程我们可以知道,引入Vuex时会执行install以及暴露一个Store类。
- 这样我就通过index.js向外暴露一个Store类以及install函数
import { Store, install } from "./store"; export default { Store, install, };
- 由以上
index.js
可知store.js
下默认暴露Store, install
Store类的实现
- 首先Store类,接收传递进来的options对象
- state:
store中state
的处理是利用vue中data
进行处理的。因为它是响应式的。我们通过this.$store.state
读取数据的时候,实际走的时Store
下的get state()
方法得到vue实例下的data
- getters:
store中getters的处理
是利用vue的computed
属性处理的。因为vuex希望使用getter
的时候,如果getter中所依赖的state不变
的话,不需要重复
执行方法。所以就利用了computed缓存
机制。通过遍历options.getters
定义和getter同名的computed
,然后在通过Object.defineProperty
劫持store中的getters
,使其读取getter
的时候,实际读取的是vue实例下
的同名的comuted
属性 - mutations:vuex中
mutations
的调用,是通过commit
函数去调用store
中的mutations
。vuex中通过遍历mutations
,给store
重新定义mutations。然后通过commit调用this._mutations[key] = (payload) => { fn.call(this, this.state, payload); }; commit = (type, payload) => { this._mutations[type](payload); };
- actions: vuex中
actions
的实现原理和mutations
的实现原理一致。不过需要注意actions
实际执行中的第一个参数
应该是store
实例,而不是state
。这是和mutations是有区别的。this._actions = {}; foreach(options.actions, (fn, key) => { this._actions[key] = (payload) => { fn.call(this, this, payload); }; }); dispatch = (type, payload) => { this._actions[type](payload); };
- 简易版本的vuex具体代码如下:
let Vue; // 这个Vue会在install赋值。这样我们就可以使用new Vue({})创建实例了 const foreach = (obj = {}, cb) => { Object.keys(obj).forEach((key) => cb(obj[key], key)); }; class Store { constructor(options) { let state = options.state; this.getters = {}; const computed = {}; foreach(options.getters, (fn, key) => { computed[key] = () => { return fn(this.state); }; Object.defineProperty(this.getters, key, { get: () => { return this._vm[key]; }, }); }); this._mutations = {}; foreach(options.mutations, (fn, key) => { this._mutations[key] = (payload) => { fn.call(this, this.state, payload); }; }); this._actions = {}; foreach(options.actions, (fn, key) => { this._actions[key] = (payload) => { fn.call(this, this, payload); }; }); this._vm = new Vue({ data: { $$state: state, }, computed, }); } get state() { return this._vm._data.$$state; } commit = (type, payload) => { this._mutations[type](payload); }; dispatch = (type, payload) => { this._actions[type](payload); }; } const install = (_Vue) => { Vue = _Vue; // 使用vue提供的方法 Vue.mixin混入 方法和数据 Vue.mixin({ beforeCreate() { let options = this.$options; // console.log(this.$options.store); if (options.store) { this.$store = options.store; } else { this.$store = this.$parent && this.$parent.$store; } }, }); }; export { Store, install };
正式版vuex@3的实现
- 正式版的vuex插件的挂载与简易版的一致,只是Store类的实现不一致
- 把具有模块化的vuex实现分为下面几步:
- 我们通过
new Vuex.Store({{options})
初始化store
实例 - 然后
Store
类中的constructor中
通过调用ModuleCollection
类进行模块收集,把用户传递的具有树形结构的options
转换为具有树形结构的ModuleCollection
实例,并且挂载到this.$store
上去。我们可以通过this.$store._modules
查看经过ModuleCollection
处理过的数据类型。 - ModuleCollection中主要是递归调用register函数进行收集,
register(path,module)
函数接收两个参数,表示为path
下添加子模块,path是一个数组。如['a','c']
,表示为a模块
添加一个子模块c
。第一次register时,path为[]
表示为根模块
添加。ModuleCollection
类下有一个getNameSpace
函数,获取当前path
的命名空间前缀。比如传递了一个['a','b']
,那么它会根据a模块和b模块是否存在namespaced
属性,有的话 返回值就是a/b/
import { foreach } from "../../MVuex/utils"; import Module from "./module"; export default class ModuleCollection { constructor(options) { this.register([], options); } register(path, rootModule) { let newModule = new Module(rootModule); // 把当前注册的模块上添加一个属性,这个属性指向自己 rootModule.rawModule = newModule; if (path.length == 0) { this.root = newModule; } else { // 下面就是通过reduce寻找父模块并把path参数的最后一项,添加到它的父级中去 // 找到父级 let parent = path.slice(0, -1).reduce((memo, current) => { // return memo._children[current]; return memo.getChild(current); }, this.root); // 给父级添加子模块 // parent._children[path[path.length - 1]] = newModule; parent.addChild(path[path.length - 1], newModule); } if (rootModule.modules) { foreach(rootModule.modules, (module, moduleName) => { // 这个时候register中的path就是 ['b'] ,['b','c']这种 // ['b']代表root下的b模块 // ['b','c']代表的就是b下的c模块 this.register([...path, moduleName], module); }); } } // 获取命名空间 getNameSpace(path) { let root = this.root; return path.reduce((namespace, key) => { root = root.getChild(key); return namespace + (root.namespaced ? key + "/" : ""); }, ""); } }
register
函数中通过实例化Module
类,创建一个一个子模块。Module类存在几个方法getChild
:获取子元素就是获取module._chidlrenaddChild
添加子模块,以上两个方法是在register
调用的 。forEachMutation forEachAction forEachGetters forEachChild
就是遍历Module的mutation actions getters children属性。在installModule中调用。
// let newModule = { // _raw: options, // _children: {}, // state: options.state, // }; import { foreach } from "../utils"; export default class Module { constructor(rootModule) { this._rawModule = rootModule; this._children = {}; this.state = rootModule.state; } // 获取当前模块的namespaced属性 get namespaced() { return this._rawModule.namespaced; } // 获取_children属性 getChild(key) { return this._children[key]; } // 添加_children属性 addChild(key, module) { this._children[key] = module; } forEachMutation(fn) { if (this._rawModule.mutations) { foreach(this._rawModule.mutations, fn); } } forEachAction(fn) { if (this._rawModule.actions) { foreach(this._rawModule.actions, fn); } } forEachGetters(fn) { if (this._rawModule.getters) { foreach(this._rawModule.getters, fn); } } forEachChild(fn) { foreach(this._children, fn); } }
// 这是没有经过ModuleCollection类处理的结构 state: { name: "home", age: 18, }, modules: { a: { state: {}, modules: { c: { state: {}, }, }, }, b: { state: {}, }, }, // 下面是经过ModuleCollection处理过的数据类型 root = { _raw: rootModule, // rootModule就是原始的modules对象 state: rootModule.state, _children: { a: { _raw: rootModule, _children: { c: { _raw: rootModule, _children: {}, state: rootModule.state, }; }, state: rootModule.state, }; }, };
- 经过上面过程之后,就把用户
new Vuex.Store({options})
中的options
变成了具有树形结构的ModuleCollection
对象。并且该对象在store实例
上通过_module
属性展示
- 我们通过
-
然后通过
installModule()
把module的属性定义到store上。与上面不同的是,上面只是把一个树形结构_module挂载到store上
,而这个方法是把state,mutation,action
这些属性挂载到store上。也是通过递归调用installModule
实现.注意这个地方收集之后是没有模块结构的。相同名称的mutation,action
放到了一个数组里面(前提是没有命名空间) -
挂载之后我们调整
commit.dispath
函数。让其遍历store实例上的mutations以及actions
.循环调用相同名称的mutation以及action -
经过上面过程之后
mutation action getters
都已经挂载到store实例上了。后面通过resetStoreVm
创建vm实例让其挂载到store上。vm实例中处理state以及getters
。当我们读取state
的时候,其实读取的就是this.$store._vm._data.$$state.
而我们读取getter
的时候就是读取的vm实例上的computed
属性import applyMixin from "./mixin"; import ModuleCollection from "./module/module-collections"; import { foreach } from "../MVuex/utils"; let Vue; function getState(store, path) { return path.reduce((newState, current) => { return newState[current]; }, store.state); } // 安装模块 function installModule(store, rootState, path, module) { // module实例就是通过Module类创建的实例 let namespace = store._modules.getNameSpace(path); // 先把各个子模块的state也以树形结构放到根模块的state上 if (path.length > 0) { //如果是子模块就需要把子模块的状态定义到根模块上.也是具有树形结构的 let parent = path.slice(0, -1).reduce((memo, current) => { return memo[current]; }, rootState); Vue.set(parent, path[path.length - 1], module.state); } // 遍历当前模块的mutations,把mutation添加store实例上 module.forEachMutation((mutation, type) => { // 判断store._mutations下有没有相同的mutation,有的话就push store._mutations[namespace + type] = store._mutations[namespace + type] || []; store._mutations[namespace + type].push((payload) => { store._withCommitting(() => { mutation.call(store, getState(store, path), payload); }); store._subscribers.forEach((sub) => sub({ mutation, type }, store.state)); }); }); // 遍历当前模块的actions,把mutation添加store实例上 module.forEachAction((action, type) => { store._actions[namespace + type] = store._actions[namespace + type] || []; store._actions[namespace + type].push((payload) => { action.call(store, store, payload); }); }); // 遍历当前模块的getters,把mutation添加store实例上 module.forEachGetters((getter, key) => { // getter不存在模块化,所有模块的getter都是定义到根模块上 // 具有相同名字的getter会覆盖 store._wrappedGetters[namespace + key] = function () { return getter(getState(store, path)); }; }); // 安装子模块 module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child); }); } function resetStoreVm(store, state) { let oldVm = store._vm; const wrappedGetters = store._wrappedGetters; let computed = {}; store.getters = {}; foreach(wrappedGetters, (fn, key) => { computed[key] = function () { return fn(); }; Object.defineProperty(store.getters, key, { get: () => store._vm[key], }); }); store._vm = new Vue({ data: { $$state: state, }, computed, }); if (store.strict) { store._vm.$watch( () => store._vm._data.$$state, () => { console.log(store._committing, "数据改变了"); console.assert(store._committing, "在mutation之外更改了状态"); }, { deep: true, sync: true } ); } if (oldVm) { Vue.nextTick(() => { oldVm.$destroy(); }); } } class Store { constructor(options) { // 格式化用户传入的参数,格式化成树形结构,更直观一些,后续模块画也更好操作 // 1. 将模块转化为一棵树 this._modules = new ModuleCollection(options); // 2. 安装模块 将模块上的属性,定义到我们的store中 let state = this._modules.root.state; this._mutations = {}; this._actions = {}; this._wrappedGetters = {}; this._subscribers = []; this.strict = options.strict; //是否是严格模式 this._committing = false; // this指当前store实例 state是指根state installModule(this, state, [], this._modules.root); // 将状态放到vue实例中 resetStoreVm(this, state); // 如果存在插件的话就执行插件 if (options.plugins) { options.plugins.forEach((fn) => { fn(this); }); } } get state() { return this._vm._data.$$state; } commit = (type, payload) => { this._mutations[type].forEach((fn) => { fn(payload); }); }; dispatch = (type, payload) => { this._actions[type].forEach((fn) => { fn(payload); }); }; //动态注册模块的函数 registerModule(path, module) { console.log("注册模块", path, module); if (typeof path == "string") path = [path]; // 先注册模块,注册后store._module就有了该模块 this._modules.register(path, module); // 安装模块 installModule(this, this.state, path, module.rawModule); // 重新创建一个实例 // 原因是新加的模块中的state以及getters并不是响应式的 resetStoreVm(this, this.state); } // subscribe(fn) { this._subscribers.push(fn); } // 替换状态 replaceState(newState) { this._withCommitting(() => { this._vm._data.$$state = newState; }); } _withCommitting(fn) { console.log("数据改变之前", this._committing); let committing = this._committing; this._committing = true; //在函数调用前标识_committing为true fn(); this._committing = committing; //在函数调用后标识_committing为false console.log("数据改变之后", this._committing); } } const install = (_Vue) => { Vue = _Vue; applyMixin(Vue); }; export { Store, install };
-
经过以上过程我们的vuex就基本实现了