前言
在前端工程化开发的今天,vuex
、redux
成为了我们项目中状态管理的上上之选。关于如何使用它,相信这已经成为前端开发者的必备技能之一了。今天,我们来一起尝试进阶一下,自己实现一个状态管理器来管理我们的项目,让我们可以在以后的开发过程中可以更加迅捷的定位问题,可以在遇到面试官提出(您好,可以描述下vuex
的实现原理吗?)类似问题的时候可以更加从容的回答。
实际使用
相信大多数同学在日常开发中会这样使用vuex
// 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
我们在引入vuex
之后主要做了以下两步操作
Vue.use(Vuex)
此处说明我们的vuex
必须得向外面暴露一个install
方法,这个install
方法可以帮助我们在vue
原型上注册我们的功能。
new Vuex.Store()
看到new
了,顾名思义我们的vuex
不仅需要暴露出install
方法,同样还需要暴露出一个store
的类,上面挂载了我们使用到的state、muations、actions、getters
等参数以及commit、dispatch
等方法
开始搭建自己的vuex
实现vue.use
通过上面的简要分析我们可以了解到我们需要创建一个install
函数和一个store
的类,然后暴露出来
新建my-vuex.js
// my-vuex.js
let Vue
const install = _Vue => {
// vue.use()执行的时候,会将vue作为参数传入进来,这里我们用一个变量接收 vue
Vue = _Vue
}
class Store {
}
export default {
install,
Store
}
vuex
基本的结构我们已经搭建好,接下来我们来继续完善install
函数。install
函数应该是一个实现挂载全局$store
的过程。
// 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
里面的功能
实现state
我们通常会在组件中使用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
}
}
实现getters
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>
实现mutation和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)
}
}
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和dispatch方法
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
的功能
首先我们一起来看一下我们一般怎样使用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
,则需要全部都执行。下面运用两个步骤进行模块化实现
1. 格式化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)
})
}
}}
2. 安装状态树
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
附赠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,
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。