在前端工程化开发的今天,vuex
、redux
成为了咱们项目中状态管理的上上之选。关于如何使用它,相信这已经成为前端开发者的必备技能之一了。今天,咱们来一块儿尝试进阶一下,本身实现一个状态管理器来管理咱们的项目,让咱们能够在之后的开发过程当中能够更加迅捷的定位问题,能够在遇到面试官提出(您好,能够描述下vuex
的实现原理吗?)相似问题的时候能够更加从容的回答。前端
本文共4000余字,阅读本篇大概须要28分钟。若有不足之处,恳请斧正
相信大多数同窗在平常开发中会这样使用vuex
vue
// store.js import Vue from "vue" import Vuex from "vuex" Vue.use(Vuex) export default new Vuex.Store({ state: { text: "Hello Vuex" }, getters: {}, mutations: {}, actions: {}, modules: {} )} 复制代码
咱们在引入vuex
以后主要作了如下两步操做面试
Vue.use(Vuex)
vuex
vuex
必须得向外面暴露一个install
方法,这个install
方法能够帮助咱们在vue
原型上注册咱们的功能。 new Vuex.Store()
redux
看到new
了,顾名思义咱们的vuex
不只须要暴露出install
方法,一样还须要暴露出一个store
的类,上面挂载了咱们使用到的state、muations、actions、getters
等参数以及commit、dispatch
等方法前端工程化
经过上面的简要分析咱们能够了解到咱们须要建立一个install
函数和一个store
的类,而后暴露出来bash
新建my-vuex.js
markdown
// my-vuex.js let Vue const install = _Vue => { // vue.use()执行的时候,会将vue做为参数传入进来,这里咱们用一个变量接收 vue Vue = _Vue } class Store { } export default { install, Store } 复制代码
vuex
基本的结构咱们已经搭建好,接下来咱们来继续完善install
函数。install
函数应该是一个实现挂载全局$store
的过程。app
// my-vuex.js let Vue const install = _Vue => { // vue.use()执行的时候,会将vue实例做为参数传入进来,这里咱们用一个变量接收 Vue = _Vue // Vue.mixin帮助咱们全局混入$store Vue.mixin({ beforeCreate(){ // 这里的this指的是vue实例 const options = this.$options if(options.store){ // 判断当前组件内部是否认义了store,若是有则优先使用内部的store this.$store = typeof options.store === 'function' ? options.store() : options.store } else if(options.parent && options.parent.$store){ // 组件内部没有定义store,则从父组件下继承$store方法 this.$store = options.parent.$store } } }) } class Store { } export default { install, Store } 复制代码
上面咱们已经经过vue.use
将$store
实例注入到了vue
上,下面咱们继续完善store
里面的功能异步
咱们一般会在组件中使用this.$store.state
来获取数据,因此这里咱们须要在Store
类上定义获取state
时的方法
my-vuex.js
代码以下
// 省略其他代码 class Store { constructor(options={}){ this.options = options } get state(){ return this.options.state } } export default { install, Store } 复制代码
测试一下
store.js
// store.js import Vue from "vue" import Vuex from "./my-vuex.js" Vue.use(Vuex) export default new Vuex.Store({ state: { text: "Hello Vuex" }, getters: {}, mutations: {}, actions: {}, modules: {} }) 复制代码
App.vue
<template> <div id="app"> <h1>{{getState}}</h1> </div> </template> <script> export default{ computed:{ getState(){ return this.$store.state.text } } } </script> 复制代码
运行代码后会发现展现出了预期的 Hello Vuex
可是在这里有一个小问题,咱们都知道vue的数据是响应式的。若是咱们以下去操做:
// App.vue <template> <div id="app"> <h1>{{getState}}</h1> </div> </template> <script> export default{ computed:{ getState(){ return this.$store.state.text } }, mounted(){ setTimeout(() => { console.log('执行了') this.$store.state.text = 'haha' }, 1000) } } </script> 复制代码
代码运行后会咱们发现页面的数据并无变化,因此这里咱们要将state
改形成响应式的数据。这里提供两种方法
vue
自身提供的data
响应式机制// my-vuex.js // 省略多余代码 class Store { constructor(options={}){ this.options = options this.vmData = new Vue({ data: { state: options.state } }); } get state(){ return this.vmData._data.state } } 复制代码
vue
2.6.0新增的Vue.observable()
实现// my-vuex.js // 省略多余代码 class Store { constructor(options={}){ this.options = options this.vmData = { state:Vue.observable(options.state || {}) } } get state(){ return this.vmData.state } } 复制代码
my-vuex.js
代码以下
// my-vuex.js // 省略多余代码 class Store { constructor(options={}){ this.options = options this.vmData = { state:Vue.observable(options.state || {}) } // 初始化getters this.getters = {} // 遍历store上的getters Object.keys(options.getters).forEach(key=>{ //为getters里全部的函数定义get时执行的操做 Object.defineProperty(this.getters,key,{ get:()=>{ return options.getters[key](this.vmData.state) } }) }) } get state(){ return this.vmData.state } } 复制代码
测试一下
store.js
import Vue from "vue" import Vuex from "./my-vuex.js" Vue.use(Vuex) export default new Vuex.Store({ state: { text: "Hello Vuex" }, getters: { getText(state){ return state.text } }, mutations: {}, actions: {}, modules: {} }) 复制代码
App.vue
<template> <div id="app"> <h1>{{getState}}</h1> </div> </template> <script> export default{ computed:{ getState(){ return this.$store.getters.getText } } } </script> 复制代码
my-vuex.js
代码以下
// 省略多余代码 class Store { constructor(options={}){ this.options = options this.vmData = { state:Vue.observable(options.state || {}) } // 初始化getters this.getters = {} // 遍历store上的getters Object.keys(options.getters).forEach(key=>{ //为getters里全部的函数定义get时执行的操做 Object.defineProperty(this.getters,key,{ get:()=>{ return options.getters[key](this.vmData.state) } }) }) // 初始化mutations this.mutations = {} // 遍历mutations里全部的函数 Object.keys(options.mutations).forEach(key=>{ // 拷贝赋值 this.mutations[key] = payload=>{ options.mutations[key](this.vmData.state,payload) } }) // commit实际上就是执行mutations里指定的函数 this.commit = (type,param)=>{ this.mutations[type](param) } } get state(){ return this.vmData.state } } 复制代码
测试一下
store.js
import Vue from "vue" import Vuex from "./my-vuex.js" Vue.use(Vuex) export default new Vuex.Store({ state: { text: "Hello Vuex" }, getters: { getText(state){ return state.text } }, mutations: { syncSetText(state,param){ state.text = param } }, actions: {}, modules: {} }) 复制代码
App.vue
<template> <div id="app"> <h1>{{getState}}</h1> </div> </template> <script> export default{ computed:{ getState(){ return this.$store.getters.getText } }, mounted(){ setTimeout(() => { console.log('执行了') this.$store.commit('syncSetText','同步更改数据') }, 1000) } } </script> 复制代码
action与mutations原理相似,一样dispatch实现方法与commit相似
my-vuex.js
代码以下
// 省略多余代码 class Store { constructor(options={}){ this.options = options this.vmData = { state:Vue.observable(options.state || {}) } // 初始化getters this.getters = {} // 遍历store上的getters Object.keys(options.getters).forEach(key=>{ //为getters里全部的函数定义get时执行的操做 Object.defineProperty(this.getters,key,{ get:()=>{ return options.getters[key](this.vmData.state) } }) }) // 初始化mutations this.mutations = {} // 遍历mutations里全部的函数 Object.keys(options.mutations).forEach(key=>{ // 拷贝赋值 this.mutations[key] = payload=>{ options.mutations[key](this.vmData.state,payload) } }) // commit实际上就是执行mutations里指定的函数 this.commit = (type,param)=>{ this.mutations[type](param) } // 初始化actions this.actions = {} Object.keys(options.actions).forEach(key => { this.actions[key] = payload => { options.actions[key](this, payload) } }) this.dispatch = (type,param)=>{ this.actions[type](param) } } get state(){ return this.vmData.state } } 复制代码
测试一下
store.js
import Vue from "vue" import Vuex from "./my-vuex.js" Vue.use(Vuex) export default new Vuex.Store({ state: { text: "Hello Vuex" }, getters: { getText(state){ return state.text } }, mutations: { syncSetText(state,param){ state.text = param } }, actions: { asyncSetText({commit},param){ commit('syncSetText',param) } }, modules: {} }) 复制代码
App.vue
<template> <div id="app"> <h1>{{getState}}</h1> </div> </template> <script> export default{ computed:{ getState(){ return this.$store.getters.getText } }, mounted(){ setTimeout(() => { console.log('执行了') this.$store.dispatch('asyncSetText','异步更改数据') }, 1000) } } </script> 复制代码
目前已经实现了vuex中基本的几个功能,可是上面的代码稍微现得有些冗余,咱们来优化一下,主要从如下两点入手
1.将出现屡次的Object.keys().forEach()
封装成公共的forEachValue
函数
function forEachValue (obj, fn) { Object.keys(obj).forEach(key=>fn(obj[key], key)); } 复制代码
2.把多个初始化从新赋值的部分封装为易读的register
函数
优化后的代码以下
// my-vuex.js // 省略多余代码 class Store { constructor(options={}){ this.options = options this.vmData = { state:Vue.observable(options.state || {}) } // 初始化getters this.getters = {} forEachValue(options.getters,(getterFn,getterName)=>{ registerGetter(this,getterName,getterFn) } ) // 初始化mutations this.mutations = {} forEachValue(options.mutations,(mutationFn,mutationName)=>{ registerMutation(this,mutationName,mutationFn) } ) // 初始化actions this.actions = {} forEachValue(options.actions,(actionFn,actionName)=>{ registerAction(this,actionName,actionFn) } ) // commit实际上就是执行mutations里指定的函数 this.commit = (type,param)=>{ this.mutations[type](param) } this.dispatch = (type,param)=>{ this.actions[type](param) } } get state(){ return this.vmData.state } } // 注册getter function registerGetter(store,getterName,getterFn){ Object.defineProperty(store.getters,getterName,{ get:()=>{ return getterFn.call(store,store.vmData.state) } }) } // 注册mutation function registerMutation(store,mutationName,mutationFn){ store.mutations[mutationName] = payload=>{ mutationFn.call(store,store.vmData.state,payload) } } // 注册action function registerAction(store,actionName,actionFn){ store.actions[actionName] = payload=>{ actionFn.call(store,store,payload) } } // 封装出公共的循环执行函数 function forEachValue (obj, fn) { Object.keys(obj).forEach(key=>fn(obj[key], key)); } export default { install, Store } 复制代码
当咱们项目日益复杂化的时候势必会引入module
进行模块化状态管理,下面咱们来继续实现module
的功能
首先咱们一块儿来看一下咱们通常怎样使用module
的
store.js代码以下
import Vue from "vue" // import Vuex from "./my-vuex.js" import Vuex from "vuex" Vue.use(Vuex) let moduleA = { state:{ nameA:'我是模块A' }, mutations:{ syncSetA(state,param){ state.nameA = param } }, actions:{ asyncSetState({commit},param){ setTimeout(()=>{ commit('syncSetA',param) },1000) } }, getters:{ getA(state){ return state.nameA } } } let moduleB = { state:{ nameB:'我是模块B' }, mutations:{ syncSetB(state,param){ state.nameB = param } }, actions:{ asyncSetState({commit},param){ setTimeout(()=>{ commit('syncSetB',param) },1000) } }, getters:{ getB(state){ return state.nameB } } } export default new Vuex.Store({ modules:{ moduleA,moduleB }, state: { text: "Hello Vuex" }, getters: { getText(state){ return state.text } }, mutations: { syncSetText(state,param){ state.text = param } }, actions: { asyncSetText({commit},param){ commit('syncSetText',param) } } }) 复制代码
App.vue代码以下
<template> <div id="app"> <h1>{{getState}}</h1> A<h2>{{stateA}}</h2> B<h2>{{stateB}}</h2> </div> </template> <script> export default{ computed:{ getState(){ return this.$store.getters.getText }, stateA(){ return this.$store.state.moduleA.nameA }, stateB(){ return this.$store.state.moduleB.nameB } }, mounted(){ setTimeout(() => { this.$store.dispatch('asyncSetState','异步更改数据') }, 1000) } } </script> 复制代码
在不启用nameSpace的状况下,咱们发现咱们获取模块内的state
使用this.$store.state.moduleB.nameA
的方式获取。而触发模块内的mutations
或者action
则是与之前同样,只不过如果两个不一样的模块有重名的mutation
或者action
,则须要所有都执行。下面运用两个步骤进行模块化实现
modules
传来的数据若是咱们的store.js
是这样的
export default new Vuex.Store({ modules:{ moduleA,moduleB }, state: {}, getters: {}, mutations: {}, actions: {} }) 复制代码
咱们能够格式化成下面这种格式,造成一个模块状态树
const newModule = {
// 根模块store
_rootModule:store,
// 子模块
_children:{
moduleA:{
_rootModule:moduleA,
_children:{},
state:moduleA.state
},
moduleB:{
_rootModule:moduleB,
_children:{},
state:moduleB.state
}
},
// 根模块状态
state:store.state
}
复制代码
为此咱们须要新增一个moduleCollection
类来收集store.js
中的数据,而后格式化成状态树
my-vuex.js
代码以下
// my-vuex.js let Vue const install = _Vue => { // 省略部分代码 } class Store { constructor(options={}){ // 省略部分代码 // 格式化数据,生成状态树 this._modules = new ModuleCollection(options) } } class moduleCollection{ constructor(rootModule){ this.register([],rootModule) } register(path,rootModule){ const newModule = { _rootModule:rootModule, // 根模块 _children:{}, // 子模块 state:rootModule.state // 根模块状态 } // path长度为0,说明是根元素进行初始化数据 if(path.length === 0){ this.root = newModule }else{ //利用reduce能够快速的将扁平化数据转换成树状数据 const parent = path.slice(0,-1).reduce((module,key)=>{ return module._children(key) },this.root) parent._children[path[path.length - 1]] = newModule } // 若是含有modules,则须要循环注册内部模块 if(rootModule.modules){ forEachValue(rootModule.modules,(rootChildModule,key)=>{ this.register(path.concat(key),rootChildModule) }) } }} 复制代码
store.js
中的数据已经被咱们递归组装成了状态树,接下来须要将状态树安装进Store
类中 这里主要作了两个改动
新增installModule
函数,installModule
主要帮助咱们将格式化好的状态树注册到Store
类中
从新改造了注册函数(registerMutation、registerGetter
等)以及触发函数(commit、dispatch
)。
my-vuex.js
代码以下
// my-vuex.js // 省略部分代码 class Store { constructor(options={}){ this.options = options // 初始化getters this.getters = {} // 初始化mutations this.mutations = {} // 初始化actions this.actions = {} // 初始化数据,生成状态树 this._modules = new moduleCollection(options) this.commit = (type,param)=>{ this.mutations[type].forEach(fn=>fn(param)) } this.dispatch = (type,param)=>{ this.actions[type].forEach(fn=>fn(param)) } const state = options.state; const path = []; // 初始路径给根路径为空 installModule(this, state, path, this._modules.root); this.vmData = { state:Vue.observable(options.state || {}) } } get state(){ return this.vmData.state } } class moduleCollection{ // 省略部分代码 } // 递归状态树,挂载getters,actions,mutations function installModule(store, rootState, path, rootModule) { // 这儿将模块中的state循环出来设置到根state中去,以便咱们经过this.$store.state.moduleA来访问数据 if (path.length > 0) { const parent = path.slice(0,-1).reduce((state,key)=>{ return state[key] },rootState) Vue.set(parent, path[path.length - 1], rootModule.state) } // 循环注册包含模块内的全部getters let getters = rootModule._rootModule.getters if (getters) { forEachValue(getters, (getterFn, getterName) => { registerGetter(store, getterName, getterFn, rootModule); }); } // 循环注册包含模块内的全部mutations let mutations = rootModule._rootModule.mutations if (mutations) { forEachValue(mutations, (mutationFn, mutationName) => { registerMutation(store, mutationName, mutationFn, rootModule) }); } // 循环注册包含模块内的全部actions let actions = rootModule._rootModule.actions if (actions) { forEachValue(actions, (actionFn, actionName) => { registerAction(store, actionName, actionFn, rootModule); }); } // 若是模块嵌套模块,则须要递归安装 forEachValue(rootModule._children, (child, key) => { installModule(store, rootState, path.concat(key), child) }) } // 这儿的getters中的state是各自模块中的state function registerGetter(store,getterName,getterFn,currentModule){ Object.defineProperty(store.getters,getterName,{ get:()=>{ return getterFn.call(store,currentModule.state) } }) } // 因为各个模块mutation存在重复状况,所以这里使用发布-订阅模式进行注册 function registerMutation(store,mutationName,mutationFn,currentModule){ let mutationArr = store.mutations[mutationName] || (store.mutations[mutationName] = []); mutationArr.push((payload)=>{ mutationFn.call(store,currentModule.state,payload) }) } function registerAction(store,actionName,actionFn){ let actionArr = store.actions[actionName] || (store.actions[actionName] = []); actionArr.push((payload)=>{ actionFn.call(store,store,payload) }) } // 省略其他代码 复制代码
至此,咱们已经实现了vuex
的基本功能,固然其余相似于nameSpace、plugins,store.subscribe
的功能这里并无展开,小伙伴们能够自行扩展。这里建议小伙伴们先要理清楚思路。从vuex
是什么,要实现那些功能?怎样能够更好的实现?若是思路通了,相信你们能够写出更好的vuex
mapState,mapGetters,mapMutations,mapActions
的实现辅助函数的实现原理较为简单,你们自行尝试
const mapState = stateList => { return stateList.reduce((prev,stateName)=>{ prev[stateName] =function(){ return this.$store.state[stateName] } return prev },{}) } const mapGetters = gettersList => { return gettersList.reduce((prev,gettersName)=>{ prev[gettersName] =function(){ return this.$store.getters[gettersName] } return prev },{}) } const mapMutations = mutationsList => { return mutationsList.reduce((prev,mutationsName)=>{ prev[mutationsName] =function(payload){ return this.$store.commit(mutationsName,payload) } return prev },{}) } const mapActions = actionsList => { return actionsList.reduce((prev,actionsName)=>{ prev[actionsName] =function(payload){ return this.$store.dispatch(actionsName,payload) } return prev },{}) } 复制代码
// my-vuex.js let Vue const install = _Vue => { // vue.use()执行的时候,会将vue实例做为参数传入进来,这里咱们用一个变量接收 Vue = _Vue // Vue.mixin帮助咱们全局混入$store Vue.mixin({ beforeCreate(){ // 这里的this指的是vue实例 const options = this.$options if(options.store){ // 判断当前组件内部是否认义了store,若是有则优先使用内部的store this.$store = typeof options.store === 'function' ? options.store() : options.store } else if(options.parent && options.parent.$store){ // 组件内部没有定义store,则从父组件下继承$store方法 this.$store = options.parent.$store } } }) } class Store { constructor(options={}){ this.options = options // 初始化getters this.getters = {} // 初始化mutations this.mutations = {} // 初始化actions this.actions = {} // 初始化数据,生成状态树 this._modules = new moduleCollection(options) // commit实际上就是执行mutations里指定的函数 this.commit = (type,param)=>{ this.mutations[type].forEach(fn=>fn(param)) } this.dispatch = (type,param)=>{ this.actions[type].forEach(fn=>fn(param)) } const state = options.state; const path = []; // 初始路径给根路径为空 installModule(this, state, path, this._modules.root); this.vmData = { state:Vue.observable(options.state || {}) } } get state(){ return this.vmData.state } } // 格式化状态树 class moduleCollection{ constructor(rootModule){ this.register([],rootModule) } register(path,rootModule){ const newModule = { _rootModule:rootModule, // 根模块 _children:{}, // 子模块 state:rootModule.state // 根模块状态 } // path长度为0,说明是根元素进行初始化数据 if(path.length === 0){ this.root = newModule }else{ //利用reduce能够快速的将扁平化数据转换成树状数据 const parent = path.slice(0,-1).reduce((module,key)=>{ return module._children[key] },this.root) parent._children[path[path.length - 1]] = newModule } // 若是含有modules,则须要循环注册内部模块 if(rootModule.modules){ forEachValue(rootModule.modules,(rootChildModule,key)=>{ this.register(path.concat(key),rootChildModule) }) } }} // 递归状态树,挂载getters,actions,mutations function installModule(store, rootState, path, rootModule) { // 这儿将模块中的state循环出来设置到根state中去,以便咱们经过this.$store.state.moduleA来访问数据 if (path.length > 0) { const parent = path.slice(0,-1).reduce((state,key)=>{ return state[key] },rootState) Vue.set(parent, path[path.length - 1], rootModule.state) } // 循环注册包含模块内的全部getters let getters = rootModule._rootModule.getters if (getters) { forEachValue(getters, (getterFn, getterName) => { registerGetter(store, getterName, getterFn, rootModule); }); } // 循环注册包含模块内的全部mutations let mutations = rootModule._rootModule.mutations if (mutations) { forEachValue(mutations, (mutationFn, mutationName) => { registerMutation(store, mutationName, mutationFn, rootModule) }); } // 循环注册包含模块内的全部actions let actions = rootModule._rootModule.actions if (actions) { forEachValue(actions, (actionFn, actionName) => { registerAction(store, actionName, actionFn, rootModule); }); } // 若是模块嵌套模块,则须要递归安装 forEachValue(rootModule._children, (child, key) => { installModule(store, rootState, path.concat(key), child) }) } // 这儿的getters中的state是各自模块中的state function registerGetter(store,getterName,getterFn,currentModule){ Object.defineProperty(store.getters,getterName,{ get:()=>{ return getterFn.call(store,currentModule.state) } }) } // 因为各个模块mutation存在重复状况,所以这里使用发布-订阅模式进行注册 function registerMutation(store,mutationName,mutationFn,currentModule){ let mutationArr = store.mutations[mutationName] || (store.mutations[mutationName] = []); mutationArr.push((payload)=>{ mutationFn.call(store,currentModule.state,payload) }) } function registerAction(store,actionName,actionFn){ let actionArr = store.actions[actionName] || (store.actions[actionName] = []); actionArr.push((payload)=>{ actionFn.call(store,store,payload) }) } function forEachValue (obj, fn) { Object.keys(obj).forEach(key=>fn(obj[key], key)); } // 辅助函数 export const mapState = stateList => { return stateList.reduce((prev,stateName)=>{ prev[stateName] =function(){ return this.$store.state[stateName] } return prev },{}) } export const mapGetters = gettersList => { return gettersList.reduce((prev,gettersName)=>{ prev[gettersName] =function(){ return this.$store.getters[gettersName] } return prev },{}) } export const mapMutations = mutationsList => { return mutationsList.reduce((prev,mutationsName)=>{ prev[mutationsName] =function(payload){ return this.$store.commit(mutationsName,payload) } return prev },{}) } export const mapActions = actionsList => { return actionsList.reduce((prev,actionsName)=>{ prev[actionsName] =function(payload){ return this.$store.dispatch(actionsName,payload) } return prev },{}) } export default { install, Store, } 复制代码