Vuex 基础与原理实现

Vuex 学习

本期内容主要为 Vuex 基础与实现简易 mini-vuexPinia 的学习后续更新!
最后,望本文能对您有所帮助!☀️

  • 前置基础
    • vue 基础
    • 了解或学习过 vue源码(熟悉相关的 API


1. Vuex 概述

  • 在实际项目中,经常会出现多个组件之间需要共享一些状态数据的问题。例如:多个视图依赖于同一状态来自不同视图的行为需要变更同一状态等。
  • 常用的解决办法:
    • ① 将数据以及操作数据的行为都定义在父组件;
    • ② 将数据以及操作数据的行为传递给需要的子组件(可能需要多级传递)
  • 这样的解决在组件嵌套层次太多时,十分麻烦,易导致数据不一致等问题。所以 Vuex 为此而生,很好地解决了多组件之间的状态,保证数据的一致性。

1.1 温故知新

  • 组件之间共享数据方式
    • 父组件向子组件传值
      • v-bind 属性绑定,子组件使用 props 接收父组件传值
    • 子组件向父组件传值
      • 父组件通过 v-on 指令侦听子组件触发的自定义事件
      • 子组件通过 $emit 函数触发自定义事件
      • 父组件使用 v-model 指令绑定数据,子组件使用 this.$emit('input', this.子组件属性)
    • 兄弟组件之间共享数据EventBus
      • $on 接收数据的组件
      • $emit 发送数据的组件

1.2 Vuex 定义

  • Vuex 是实现组件全局状态管理的一种机制,可以方便的实现组件之间的数据共享
1.2.1 使用 Vuex 统一管理状态优点
  • 能够在 vuex集中管理共享的数据,易于开发和后期维护
  • 能够高效地实现组件之间的数据共享,提高开发效率
  • 存储在 vuex 中的数据都是响应式的,能够实时保持数据与页面的同步
1.2.2 状态管理模式
  • 状态自管理应用通常包含 stateviewaction
    • state (状态):驱动应用的数据源(就是组件的 data
    • view (视图):以声明方式将 state 映射到视图
      • {{ count }}
    • action(行为):响应在 view 上的用户输入导致的状态变化,实际上就是函数

2. Vuex 的基本使用

2.1 安装

npm init -y
npm install vuex --save
  • 导入 vuex
import Vuex from 'vuex'
// 注册 Vuex
Vue.use(Vuex)
  • 创建 store 对象
export default new Vuex.Store({
    // state 中存放的就是全局共享数据
    state: { count: 0 }
})
  • store 对象挂载到 vue 示例中
import store from './store'
new Vue({
    el: '#app',
    // render 渲染根组件
    render: h => h(app),
    router,
    // 将默认的导出的 store 对象注册到根实例中
    store
})
  • 搭建 vuex-demo 项目
vue create vue-demo # check the features need for your project 记得选 Vuex

2.2 项目结构搭建

// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

// src/main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

Vue.config.productionTip = false

new Vue({
  store,
  render: h => h(App)
}).$mount('#app')
  • 根组件 App.vue
<template>
  <div>
    <my-addition></my-addition>
    <my-subtraction></my-subtraction>
  </div>
</template>

<script>
// 加法组件
import Addition from './components/Addition.vue'
// 减法组件
import Subtraction from './components/Subtraction.vue'
    
export default {
  data() {
    return {}
  },
  // 局部注册组件
  components: {
      'my-addition': Addition,
      'my-subtraction': Subtraction
  }
}

</script>
  • src/components 目录下创建两个子组件
    • Addition.vue
    • Subtraction.vue
<!-- Addition.vue -->
<template>
  <div>
      <h3>当前最新的 count 值为:</h3>
      <button> +1 </button>
  </div>
</template>

<script>

export default {
  data() {
    return {}
  }
}

</script>


<!-- Subtraction.vue -->
<template>
  <div>
      <h3>当前最新的 count 值为:</h3>
      <button> +1 </button>
  </div>
</template>

<script>

export default {
  data() {
    return {}
  }
}

</script>

2.3 Vuex 核心概念

  • Vuex 和单纯的全局对象区别
    • Vuex 的状态存储是响应式的。当 Vue 实例/组件从 store 中读取状态时,若 store 中状态变更,那么相应的组件和实例也会更新。
    • ② 用户不能直接改变 store 中的状态。改变的 store 的状态的唯一途径就是显式提交 mutations
  • Vuex 工作原理
    在这里插入图片描述
2.3.1 state
  • State 提供唯一的公共数据源,所有共享数据都要统一放入 StoreState 中进行存储
// src/store/index.js
// todo...
export default new Vuex.Store({
  state: {
      // count 作为公共数据源
      count: 0
  },
  // todo...
})
  • 组件访问 State 中数据的方式

    • this.$store.state.全局数据名称
<!-- src/components/Addition.vue 和 Subtraction.vue -->
<template>
    <div>
        <!-- this.$store.state.count this 在插值语法中可以省略 -->
        <h3>当前最新的 count 值为:{{ $store.state.count }}</h3>
        <button> +1 </button>
    </div>
</template>
    • 通过 computed 计算属性获取 Vuex 状态
<template>
	<div>
    	<h3>当前最新的 count 值为:{{ count }}</h3>
    </div>
</template>
<script>
export default {
    computed: {
        count() {
        	// mapState 的实现类似于此,后续细说
            return this.$store.state.count
		}
    }
}
</script>
  • 通过 mapState() 辅助函数获取 Vuex 状态,将当前组件需要的全局数据,映射为当前组件的 computed 计算属性
  • mapState() 函数返回是一个对象,用于获取多个状态。mapState() 函数可以接收 {}[] 作为参数。
  • {} 为键值对形式,即 key: value,其中 key 为计算属性,value 为函数,参数为 store.state,返回需要的 state
  • 当映射的计算属性的名称与 state 子节点相同时,可以向 mapState 传入一个字符串数组。
<template>
	<div>
    	<h3>当前最新的 count 值为:{{ count }} </h3>
    </div>
</template>
<script>
import { mapState } from 'vuex'
export default {
    /*
    第一种方式:
    computed: mapState({
    	// key: value
    	count: state => state.count
    })
    第二种方式:
    computed: mapState({
    	count(state) {
    		// 可以使用 this 
    		return state.count /* + this.xxx */
    /*	}
    })
    */
    computed: {
        // 使用对象展开运算符(...)
        // 映射 this.count 为 store.state.count
        ...mapState(['count']),
    }
}
</script>

2.3.2 Mutation
  • 错误示例
<template>
    <div>
        <h3>当前最新的 count 值为:{{ $store.state.count }}</h3>
        <button @click="btnHandler"> +1 </button>
    </div>
</template>

<script>
export default {
    data() {
        return {}
    },
    methods: {
        btnHandler() {
            this.$store.state.count++
        }
    }
}
</script>
  • 此时点击 +1 按钮即可更新 count 值,但是这样的更新方式是错误的,在浏览器中打开 Vue Devtools,在 Vuex 可视化工具中无法查询到 statecount 数据更新记录
  • 更改 VuexStore 的状态的唯一方法就是提交 mutation,也就是说只能通过 mutation 变更 Store 中的数据,不可以直接操作 Store 中数据
  • mutation 类似于事件,每一个 mutation 都有一个字符串的事件类型(type)和回调函数 handler,接收 state 作为第一个参数。
// src/store/index.js
// todo...
export default new Vuex.Store({
  	state: {
    	count: 0
  	},
  	mutations: {
        // increment 为事件类型 type
        // state 作为参数
    	increment(state) {
      		state.count++
    	}
  	},
})
  • 触发 mutation的第一种方式
export default {
    methods: {
        btnHandler() {
            // 触发 mutation 的第一种方式
            this.$store.commit('increment')
        }
    }
}
  • 在触发 mutations 时可以传递额外的参数,即 mutations 的载荷(payload
//... 定义 mutations
mutations: {
    increment(state, n) {
        state.count += n
    }
}

//... 触发 mutations 时
// 自定义每次点击增加数值
this.$store.commit('increment', 2)
  • 良好的开发风格

    • 使用常量替代 mutations 事件类型,通常将这些常量保存在单独的文件中,让项目所包含的 mutations 一目了然,便于项目合作者查看使用
  • 建议多人合作的大项目最好使用常量形式处理 mutations ,对于小项目可以不需要

// 创建文件 mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
// src/store/index.js
import { SOME_MUTATION } from './mutation-types.js'
// todo...
mutations: {
    [SOME_MUTATION](state) {
        // todo...
    }
}
  • 载荷为对象情况
mutations: {
    increment(state, payload) {
        state.count += payload.n
    }
}
  • 对象风格的提交方式
this.$store.commit({
    type: 'increment',
    n: 1
})
  • 修改 state 对象方法
// 使用 Vue.set 为 obj 对象添加属性 newProp 且值为 1
Vue.set(obj, 'newProp', 1)
state.obj = { ...state.obj, newProp: 1}
// 既保存了 state.obj 原先的属性,又添加新属性 newProp 且值为 1
  • 触发 mutations 的第二种方式
    • 使用 mapMutations() 辅助函数将组件中的 methods 映射为 store.commit() 方法调用
// 从 vuex 中按需导入 mapMutations 函数
import { mapMutations } from 'vuex'
// 通过刚导入的 mapMutations 函数,将需要的 mutations 函数映射为当前的组件的 methods 方法
methods: {
    ...mapMutation(['increment']),
    // 将 'this.increment()' 映射为 'this.$store.commit'
    btnHandler() {
        // mapMutations 支持载荷 payload
        this.increment()
        // this.increment(3) 映射为 this.$store.commit('increment', 3)
    }
}
  • Mutation 必须是同步函数
mutations: {
    increment(state) {
        setTimeoout(() => {
            state.count += 1
        }, 1000)
    }
}
  • 不要在 mutations 函数中执行异步操作。上示例中当 mutation 触发时,回调函数还没有被调用,devtools 无法捕获操作行为,实质上任何在回调函数中进行的状态的改变都是不可追踪的。
  • 创建 store 的时候传入 strict: true 来开启严格模式。在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

2.3.4 Action
  • Action 用于处理异步操作
  • 如果通过异步操作变更数据,则必须使用 Action,而不能在 Mutation 中处理异步逻辑,但是在 Action 中还是需要通过触发 Mutation 的方式间接变更数据。
  • action 函数接收一个与 store 实例具有相同方法和属性的 context 对象
// todo...
mutations: {
    increment(state) {
        state.count++
    }
},
actions: {
    incrementAsync(context) {
        setTimeout(() => {
            // 触发 mutations (increment)
            context.commit('increment')
        }, 1000)
    }
    /*
    	使用解构方式
    	incrementAsync({commit}) {
    		setTimeout(() => {
    			commit('increment')
    		}, 1000)
    	}
    */
}
  • actions 通过 store.dispatch() 方法触发 mutations
methods: {
    btnHandler() {
        // 触发 actions 的第一种方式
        this.$store.dispatch('incrementAsync')
    }
}
  • actions 支持同样的载荷形式和对象形式进行分发
methods: {
    btnHandler() {
        // payload 形式
        this.$store.dispatch('incrementAsync', {
            n: 1
        })
        // 对象形式
        /*
        	this.$store.dispatch({
        		type: 'incrementAsync',
        		n: 1
        	})
        */
    }
}
  • actions 中可以使用辅助函数 mapActions() 将组件的 methods 映射为 store.dispatch() 方法调用
import { mapActions } from 'vuex'
export default {
    methods: {
        ...mapActions(['incrementAsync']),
        // 将 'this.incrementAsync' 映射为 'this.$store.dispatch('incrementAsync')'
        btnHandler() {
            this.incrementAsync()
            // this.incrementAsync(2)
            // 将 'this.incrementAsync(2)' 映射为 'this.$store.dispatch('incrementAsync', 2)'
        }
    }
}
2.4.5 Getter
  • Getter 用于对 Store 中的数据进行加工处理形成新的数据,类似于计算属性

  • Getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生改变时才会被重新计算

  • Getter 的使用:通过属性访问

    • getters 会暴露出 store.getters 对象
      • this.$store.getters.名称
// src/store/index.js
// 定义 Getter
const store = new Vue.Store({
    state: {
        count: 0
    },
    getters: {
        showNum: state => {
            return '当前最新的 count 值为:' + state.count
        }
    }
})
// Addition.vue
<template>
    <div>
        <h3>{{ $store.getters.showCount }}</h3>
    </div>
</template>
  • mapGetters() 辅助函数访问
import { mapGetters } from 'vuex'
// todo...
computed: {
    // 将 this.showCount 映射为 this.$store.getters.showCount
    ...mapGetters(['showCount'])
}

/*
<!-- Addition.vue -->
<template>
	<h3>{{ showCount }}</h3>
</template>
*/

2.4.6 modules
  • 由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。

  • 为了解决以上问题,Vuex 允许我们将 store 分割成模块。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块

// src/store/module1.js
const module1 = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... },
  getters: { ... }
}
export default module1

// src/store/module2.js
const module2 = {
  state: () => ({ ... }),
  mutations: { ... },
  actions: { ... }
}
export default module2

// src/store/index.js
import { module1 } from './module1.js'
import { module2 } from './module2.js'
const store = new Vuex.Store({
  // 注册 modules
  modules: {
    m1: module1,
    m2: module2
  }
})

store.state.m1 // -> moduleA 的状态
store.state.m2 // -> moduleB 的状态
  • 组件内可以通过 this.$store.state.m1this.$store.state.m2 调用模块的状态
  • 模块的局部状态及使用
    • 对于模块内部的 mutationsgetters 接收第一个参数是模块的局部状态 state
    • 对于模块内部的 action ,局部状态通过 context.state 暴露,根节点状态则为 context.rootState
    • 对于模块内部的 getter,根节点状态会作为第三个参数
const module1 = {
  state: () => ({
    count: 0
  }),
  mutations: {
    increment (state) {
      // 这里的 state 对象是模块的局部状态
      state.count++
    }
  },
  getters: {
    // getters 接收其他的 getters
    // rootState 根节点状态会作为第三个参数暴露
    ShowCount (state, getter, rootState) {
      return '当前最新的 count 值为:' + state.count
    }
  },
  actions: {
      // 解构 context
      incrementIfOddOnRootSum ({ state, commit, rootState }) {
      // 模块中可能存在嵌套关系
      if ((state.count + rootState.count) % 2 === 1) {
        commit('increment')
      }
    }
  } 
}
  • 命名空间 nampspaced

  • 默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间

  • 但会出现两种问题

    • ① 当不同模块中有相同命名的 mutationsactions 时,不同模块对同已 mutationsactions 作出响应
    • ② 当一个项目中 store 被分割为很多模块时,在使用辅助函数 mapState()、mapGetter()、mapMutations()、mapActions() 时,查询引用的 state、getters、mutations、actions 来自哪个模块将会非常困难,而且不便于后期维护
  • 为了提高模块的封装度和复用性,可以通过添加 namespaced: true 的方式将该模块变为带命名空间的模块。当模块被注册后,其中所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。

const moduleA = {
    namespaced: true,
    state: { /*...*/ },
    mutations: { /*...*/ },
    actions: { /*...*/ },
    getters: { /*...*/ },
}
  • 设置命名空间后,在使用辅助函数 mapState()、mapGetter()、mapMutations()、mapActions() 会增加第一个参数,即模块名,用于限定命名空间,第二个参数为对象或数组中的属性,都映射到当前命名空间中。
// src/components/Addition.vue
export default {
    // todo...
    methods: {
        // 不使用命名空间
        ...mapActions(['increment','decrement']),
        // 使用命名空间 需要增加模块参数
        ...mapActions('m1',['incrementAsync']),
        ...mapActions('m2',['decrementAsync']),
        btnHandler() {
            this.$store.dispatch('m1/incrementAsync')
        }
    }
}

3. Vuex 源码实现

  • 实现一个简易的 vuex 状态管理工具 mini-vuex

3.1 基本结构搭建

vue create vue-source-learn
# check the features needed for your project: Babel Vuex
cd src && mkdir mini-vuex 
cd vuex && touch index.js
3.1.1 install 方法
// 修改原配置导入文件
import Vue from 'vue'
import Vuex from '../vuex'
Vue.use(Vuex)
  • src/store/index.js 导出(目标)
// 先导出 Store 类 再实例化对象 注意:点运算符比 new 运算符优先级高
// 以下是我们基本实现目标
export default new Vuex.Store({
  state: {
  },
  getters: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})
  • mini-vuex/index.js 作为主文件一般用于整合操作
import { Store } from './store'
  • 使用 Vue.use(Vuex) 说明内部使用 install 方法

vue.use() 用于注册全局插件,接收函数或者一个包含install 属性的对象为参数,本质上就是执行注册插件的内部的 install 方法

// mini-vuex/index.js
import { Store, install } from './store'
export default {
    Store,
    install
}
  • 新建 store.js 文件(或新建 store 目录下新建 index.js 文件)
// store.js
// 搭建基本结构
let Vue;
class Store {

}

const install = (_Vue) => {
    /* _Vue 保存用户传递的配置参数 */
    Vue = _Vue
}

export {
	Store,
    install
}

export default {
    Store,
    install
}
// 默认导出 同时也可以解构导出
  • Store 类结构搭建
// store.js
let Vue;
class Store {
	constructor(options/*用户传递配置选项*/) {
        console.log(options)
    }
}
const install = (_Vue) => {
    Vue = _Vue
    // Vue 保存的是 Vue 的构造函数
    console.log('install')
}

在这里插入图片描述

3.2 混入模式

  • Vuex 调用 install 方法的目的是注册全局组件,注册原型方法,然后我们需要调用 mixin(混入模式)store 实例绑定给所有的组件
// store.js
import applyMixin from './mixin'
let Vue;
const install = (_Vue) => {
    Vue = _Vue
    console.log('install')
    applyMixin(Vue)
}
  • vuex 目录下创建 mixin.js 文件用于编写 applyMixin 方法
  • Vue.mixin 提供混入 Vue 实例的方法,当创建混入对象后,自定义的方法或属性可以很轻松挂载到 Vue 实例
  • 一般来说插件混入是在 beforeCreate 时进行
    • 注意:组件的创建过程是创建组件创建组件
const applyMixin = (Vue) => {
	Vue.mixin({
        // 完成 vuex 初始化
        beforeCreate: vuexInit
    })    
}
function vuexInit() {
    // 获取用户配置选项参数 -- 在根实例上 new Vue({ ... })
    const options = this.$options
    // 完成所有组件 store 的挂载
    if(options.store) {
        // 有 options.store 表明是根实例上的 Store 的实例化对象 挂载到 vue 实例上 $store
        this.$store = options.store
    } else if(options.parent && options.parent.$store) {
        // 否则是子组件
        this.$store = options.parent.$store
    }
}
export default applyMixin
  • 验证(查看 Vue 实例 $store

在这里插入图片描述

  • 查看组件上 $store
// App.vue 
export default {
    mounted() {
        console.log(this)
    }
}

在这里插入图片描述

  • vuex 给每一个组件都定义了一个 $store 属性(指向的都是同一个,也就是意味着所有组件都共享一个 $store

3.3 vuex 中 state 的实现

  • 我们知道最终是要获取 Store 的实例,需要实现 store.state.xxx 的状态获取
// store.js
class Store {
    constructor(options/*用户传递配置*/) {
        // console.log(options)
        // 获取用户传递过来的状态
        this.state = options.state
    }
}
  • 但是如果直接将 state 定义在实例上,后续状态发生变化,视图不会更新
  • vue 中定义数据,若属性名是通过 $xxx 命名,则不会代理到 vue 的实例上,但是可以通过创建实例的 _data 中获取
// store.js
let Vue;
class Store {
    constructor(options/*用户传递配置*/) {
        // console.log(options)
        // 获取用户传递过来的状态
        let state = opitons.state
        // 为什么 Vue 有值?还记得 Vue 保存的 Vue 的构造函数嘛? 因为先调用 install 方法将 Vue 暂存
        this._vm = new Vue({
            data: {
                // $$ 表示内部的状态
                $$state: state
            }
        })
    }
}

在这里插入图片描述

  • 使用类的属性访问器获取 $$state
    • this.$store.state 代理到 this._vm._data.$$state
// store.js
class Store {
    constructor(options) {
        // todo...
    }
    // 使用类的属性访问器 --> 获取 $$state
    get state() {
        return this._vm._data.$$state
    }
}
// App.vue
/*
	<template>
		<div id="app">
			count 的值为 {{ $store.state.count }}
		</div>
	</template>
*/
  • 页面显示 count 的值为 0

3.4 vuex 中的 getters 实现

let Vue;
class Store {
    constructor(options) {
        let state = options.state
        this._vm = new Vue({
            data: {
                $$state: state
            }
        })
        // 获取 getters
        // let getters = options.getters
    }
    // todo...
}
  • store 中 定义的 getters 可以认为是 store计算属性

  • 可以使用 Object.definePropertygetters 上定义属性

  • 新建 util.js 作为工具函数的封装文件

// util.js
export const forEach = (obj = {}, fn) => {
    // 获取 getters 的键 key 的数组且遍历 fn 
    // fn 用于为 getters 定义新属性
	Object.keys(obj).forEach((key, index) => {
        // obj[key] 为值(getters 上定义的函数)
		fn(obj[key], key) //执行
    })
}
  • 使用自定义工具函数 forEach
import { forEach } from './util'
// todo...
class Store {
    constructor(options/*用户传递配置*/) {
        // console.log(options)
        let state = options.state
        this._vm = new Vue({
            data: {
                $$state: state
            }
        })
        this.getters = {}
        forEach(options.getters, (fn, key) => {
        	// 定义到 this.getters 身上
            Object.defineProperty(this.getters, key, {
            	// 读取 执行 getters 方法 记得传入 state 参数
                get:() => fn(this.state)
            })
        })
    }
    // todo...
}
  • 使用 getters 验证
<template>
  <div id="app">
    count 的值为 {{ $store.state.count }} <br>
    {{ $store.getters.showCount }}
  </div>
</template>

在这里插入图片描述

  • 验证成功!

  • 但有些问题:当页面重新渲染,$store.getters.count 就会重新取值(重新执行定义的 getter 方法)但要求的是就像计算属性那样,getters 的 返回值应该是会根据他的依赖被缓存起来,且只有它的依赖值发生改变才会被重新计算

  • 实现缓存功能

import { forEach } from './util'
// todo...
class Store {
    constructor(options) {
        // todo...
        const computed = {}
        forEach(options.getters, (fn, key) => {
            computed[key] = () => {
                // 通过计算属性 实现缓存
                return fn(this.state)
            }
            Object.defineProperty(this.getters, key, {
                // 此时 fn 函数就不会每次执行,去计算属性上取值
                get: () => this._vm[key]
            })  
        })
        this._vm = new Vue({
            data: {
                $$state: state,
            },
            computed // 计算属性会将自己的属性放到实例上
        })
    }
}

3.5 vuex 中的 actions 和 mutations 的实现

  • 实现思路:使用发布订阅模式,将定义的 mutationsactions 先保存起来(订阅),调用 commit 时(发布)就找订阅的 mutations 方法,或调用 dispatch 就找对应的 actions 方法
import { forEach } from './util'
// todo...
class Store {
    constructor(options) {
        // todo...
        this._vm = new Vue({ /* ... */ })
        this._mutations = {}
        this._mutations = {}
        forEach(options.mutations, (fn, type) => {
            this._mutations[type] = (payload) => {
                fn.call(this, this.state, payload)
            }
        })
    }
    commit = (type, payload) => {
        this._mutations[type](payload)
	}
    // todo...
}
  • 验证 mutations 每次点击按钮 count 值增加 2
<template>
  <div id="app">
    count 的值为 {{ $store.state.count }} <br>
    <button @click="btnHandler">点击</button>
  </div>
</template>

<script>
export default {
  methods: {
    btnHandler() {
      this.$store.commit('changeCount', 2)
    }
  }
}
</script>
  • actions 实现(跟 mutations 类似)
    • 注意 mutations 必须是同步函数,actions 中可以包含异步操作
class Store {
    constructor(options) {
        // todo...
        this._vm = new Vue({ /* ... */ })
        // todo: mutations ...
        this._actions = {}
        forEach(options._actions, (fn, type) => {
            this._actions[type] = (payload) => {
                fn.call(this, this, payload)
            }   
        })
    }
    // todo: commit ...
    dispatch = (type, payload) => {
        this._actions[type](payload)
    }
    // todo...
}

模块知识补充

  • 模块使用示例回顾
// todo...
export default new Vuex.Store({
  state: { /*...*/ },
  getters: { /*...*/ },
  mutations: {
  	changeCount(state, payload) {
      	state.count += payload
    }
  },
  actions: { /*...*/ },
  modules: {
    a: {
		state: {
            c: 100
        },
        mutations: {
            changeCount(state, payload) {
                console.log('c 更新')
            }
        }
    },
    b: {
        state: {
            d: 100
        },
        mutations: {
            changeCount(state, payload) {
                console.log('d 更新')
            }
        }
    }
  }
})

// App.vue
/*
<template>
  <div id="app">
    count 的值为 {{ $store.state.count }} <br>
    <!-- {{ $store.getters.showCount }} -->
    <button @click="btnHandler">点击</button>
    <div>
      <p>--------------------------</p>
      {{ $store.state.a.c }}
      {{ $store.state.b.d }}
    </div>
  </div>
</template>
*/
  • 上述示例会出现问题:子模块 abmutationschangeCount 被调用。默认模块是没有作用域,也就是说,当不同模块中有相同命名mutationsactions 时,不同模块对同一 mutationsactions 作出响应

来个套娃 😵

// todo...
export default new Vuex.Store({
  mutations: {
  	changeCount(state, payload) {
      	state.count += payload
    }
  },
  modules: {
    a: {
		state: {
            c: 100
        },
        mutations: {
            changeCount(state, payload) {
                console.log('c 更新')
            }
        }
    },
    b: {
        state: {
            d: 100
        },
        mutations: {
            changeCount(state, payload) {
                console.log('d 更新')
            }
        }modules: {
            d: {
                state: {
                    e: 100
                }
            }
        }
    },
  }
})
  • 上述示例中 b 模块中状态 db 模块的子模块d 重名,那么问题来了:访问 this.$store.b.d 获取的状态 d 还是子模块 d 呢?结果是模块覆盖掉状态(为了避免这种问题出现,状态不要和模块重名)
  • 带有命名空间 namespaced: true 会将该模块的属性 state、getters、mutations、actions、modules 都封装至这个模块下作用域下
export default new Vuex.Store({
  state: {
    count: 0
  },
  getters: {
    showCount(state) {
      return 'count 的值为 ' + state.count
    }
  },
  mutations: {
    changeCount(state, payload) {
      state.count += payload
    }
  },
  actions: {
    changeCountAsync({ commit }, payload) {
      setTimeout(() => {
        commit('changeCount', payload)
      }, 1000)
    }
  },
  modules: {
    a: {
      namespaced: true,
      state: {
        c: 100
      },
      mutations: {
        changeCount(state, payload) {
          console.log('c 更新')
        }
      }
    },
    b: {
      namespaced: true,
      state: {
        d: 200
      },
      mutations: {
        changeCount(state, payload) {
          console.log('d 更新')
        }
      },
      modules: {
        namespaced: true,
        c: {
          state: {
            e: 300
          },
          mutations: {
            changeCount(state, payload) {
                console.log('e 更新')
            }
          }
        }
      }
    }
  }
})
  • 如果模块(未设置命名空间)的父级模块有设置命名空间,那么该模块也存在于父级模块的作用域中,那么就会出现不同模块具有相同命名的 mutations 同时做出响应
<script>
export default {
  mounted() {
    console.log(this)
  },
  methods: {
    btnHandler1() {
      this.$store.commit('a/changeCount', 2)
      // c 更新
    },
    btnHandler2() {
      this.$store.commit('b/changeCount', 2)
      // d 更新 【注意】: 如果 b 模块的子模块 c 没有设置命名空间 namespaced: true 结果是输出 d 更新 和 e 更新
    },
    btnHandler3() {
      this.$store.commit('a/b/changeCount', 2)
      // e 更新
    },
    btnHandler4() {
      this.$store.commit('changeCount', 2)
      // 每次点击 count 加 2 
    }
  }
}
</script>

3.6 vuex 中 module 的实现

3.6.1 moduleCollection 实现( Store 类需要重构)
  • 格式化传入参数
    • 格式化成树形结构(集合)(直观、便于操作)
// src/vuex/store.js
import ModuleCollection from './module/module-collection'
// todo...
let Vue
class Store {
    constructor(options) {
        this._modules  = new ModuleCollection(options)
    }
    // todo...
}
  • src 目录下创建 module 目录并新建文件 module-collection.js
export default class ModuleCollection {
    constructor(options) {
		  // console.log(options)
    }
}
// 格式化树形结构 (如下所示)
// 根模块(节点)
/* 	this.root = {
    	// _rawModules 为原生配置选项
    	_rawModules: xxx,
    	// 子模块(节点)
    	_children: {
        	a: {
            	_rawModules: xxx,
            	state: a.state
        	},
        	b: {
            	_rawModules: xxx,
            	_children: {
                	// ...
            	},
            	state: b.state
        	}
    	},
    	state: xxx.state
	}
*/
  • 注册模块(使用递归
    • 编写一个方法 register 用于生成树形结构的 modules 集合
// module/module-collection.js
export default class ModuleCollection {
    constructor(options) {
		  // console.log(options)
          // 注册模块
          // [] 表示根模块,options 把配置参数传入
          this.register([], options)
    }
    register(path, rootModule) {
        // 分配模块对象
        let newModule = {
            // _raw 表示原生配置参数
            _rawModule: rootModule,
            // 子节点
            _children: {},
            // 状态
            state: rootModule.state
        }
        if(path.length == 0) {
            // path 长度为 0 注册根模块 []
            this.root = newModule
        } else {
            // [b,c] 表明 c 是 b 的子模块 当为 c 注册子模块时应该是注册在 b 的 _children 中
            // path.slice(0, -1) ⇒ path.slice(0, path.length - 1)
            let parent = path.slice(0, -1).reduce((prec, cur) => {
               return prec._children[cur]
            }, this.root)
            // 每次将注册的子模块加入相应的【父】节点的 _children 中
            parent._children[path[path.length - 1]] = newModule
        }
        // 有子模块嵌套
        if(rootModule.modules) {
            // rootModule.modules ==> 对象  module 为 子模块属性的值 moduleName 子模块属性的键
            forEach(rootModule.modules, (module, moduleName) => {
                this.register([...path, moduleName], module)
            })
		}
	}
}
  • 验证一下
class Store {
    constructor(options) {
        this._modules = new ModuleCollection(options)
        console.log(this._modules);
    }
}

在这里插入图片描述

3.6.2 抽离 module 类(优化)
  • 新建文件 module.js 用于生成模块实例
export default class Module {
    constructor(rootModule) {
		this._raw = rootModule
        this._children = {}
        this.state = rootModule.state
    }
    // 获取子模块的值 { ... }
    getChild(key) {
        return this._children[key]
    }
    // 添加子模块
    addChild(key, module) {
        this._children[key] = module
    }
}

// module-collection.js
import Module from '../module/module';
export default class ModuleCollection {
    // todo: constructor
    register(path, rootModule) {
        let newModule = new Module(rootModule)
        if (path.length == 0) {
            this.root = newModule
        } else {
            let parent = path.slice(0, -1).reduce((prec, cur) => {
                return prec.getChild(cur)
            }, this.root)
            parent.addChild(path[path.length - 1], newModule)
        }
        // todo...
    }
}

在这里插入图片描述

3.6.3 递归安装模块
  • 获取树形结构的模块集合后,将树形结构上的属性挂载到 store
// store.js
class Store {
    constructor(options) {
        // 收集模块转换成“树”
        this._modules = new ModuleCollection(options)
        // 获取根的状态
        let root = this._modules.root
        // 安装模块 将模块上的属性定义在 store 中
        this._mutations = {}
        this._actions = {}
        this._wrappedGetters = {}
        // _mutations、_actions、_wrappedGetters 存放着所有模块的对应属性
        installModule(this, root.state, [], root)
        // todo...
    }
    // todo...
}
function installModule(store, rootState, path, module) {
    // todo... 
}
  • Module 实例添加遍历相应的方法
    • 用于后续处理 module 中的 mutations、actions、getters 和子模块
// module/module.js
import { forEach } from "../util"
export default class Module {
    // todo...
    forEachMutations(fn) {
        if (this._rawModule.mutations) {
            forEach(this._rawModule.mutations, fn)
        }
    }
    forEachActions(fn) {
        if (this._rawModule.actions) {
            forEach(this._rawModule.actions, fn)
        }
    }
    forEachGetters(fn) {
        if (this._rawModule.getters) {
            forEach(this._rawModule.getters, fn)
        }
    }
    forEachChild(fn) {
        if (this._children) {
            forEach(this._children, fn)
        }
    }
}
  • installModule 实现(暂不考虑命名空间)
    • 使用发布订阅模式
      • store 实例上添加相应的 _mutations、_actions、_wrappedGetters 属性,对于相同命名的属性进行订阅
// store.js
function installModule(store, rootState, path, module) {
	// 处理 mutations
    module.forEachMutations((mutation, type) => {
        store._mutations[type] = (store._mutations[type] || [])
        store._mutations[type].push((payload) => { // 包装函数
            mutation.call(store, module.state, payload)
        })
    })
    module.forEachActions((actions, type) => {
        store._actions[type] = (store._actions[type] || [])
        store._actions[type].push((payload) => {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key) => {
        // 如果 getters 重名会覆盖 所有模块的 getters 会定义到根模块上 
        store._wrappedGetters[key] = function (params) {
            return getters(module.state)
        }
    })
    module.forEachChild((child, key) => {
        // 递归处理子模块
        installModule(store, rootState, path.concat(key), child)
    })
}
  • 状态 state 实现
// store.js
function installModule(store, rootState, path, module) {
    if (path.length > 0) { // 如果是子模块 需要将子模块的状态定义到根模块上
        let parent = path.slice(0, -1).reduce((prev, cur) => {
            return prev[cur]
        }, rootState)
        // Vue.set() 可以新增属性 如果本身对象不是响应式的会直接复制,所以该方法可以区分是否是响应式数据
        Vue.set(parent, path[path.length - 1], module.state)
    }
    // todo: forEachOperation...
}
class Store {
    constructor(options) {
        this._modules = new ModuleCollection(options)
        let root = this._modules.root
        this._mutations = {}
        this._actions = {}
        this._wrappedGetters = {}
        installModule(this, root.state, [], root)
        console.log(root.state);
    }
}
  • vuexstate 输出结果

在这里插入图片描述

  • 验证结果
    • root.state(大致上是没有问题,只不过现在 state 不是响应式的状态数据

在这里插入图片描述


3.6.4 优化与响应式实现
  • commit dispatch 作为发布行为优化
commit = (type, payload) => {
    // this._mutations[type](payload)
    this._mutations[type].forEach(fn => {
        fn(payload)
    })
}
dispatch = (type, payload) => {
    // this._actions[type](payload)
    this._actions[type].forEach((fn) => {
        fn(payload)
    })
}
  • 响应式处理 state
    • 为了将状态变为响应式数据,将状态 state 挂载到 Vue 实例
    • getters 实现

类似 3.3 与 3.4 节实现

// todo...
class Store {
    constructor(options) {
        // todo...
        // 递归安装模块
        installModule(this, root.state, [], root)
        // 将状态放到 vue 实例上
        resetStoreVm(this, root.state)
    }
    // todo...
}
function resetStoreVm(store, state) {
    // Getters 作为计算属性 computed 处理
    const _wrappedGetters = store._wrappedGetters
    let computed = {}
    store.getters = {}
    // getters 作为计算属性的处理
    forEach(_wrappedGetters, (fn, key) => {
        computed[key] = function() {
            return fn(store.state)
        }
        // 将计算属性代理给 store.getters
        Object.defineProperty(store.getters, key, {
            get: () =>  store._vm[key]
        })
    })
    store._vm = new Vue({
        data: {
            // state 变为响应式数据
            // get state() 访问 this._vm._data.$$state
            $$state: state
        },
        computed
    })
}
// todo...
3.6.6 命名空间的计算
  • namespaced 的处理
  • vuex 中添加 namespaced: true
export default new Vuex.Store({
  namespaced: true,
  state: {
    count: 0
  },
  modules: {
    a: {
      namespaced: true,
      state: {
        count: 1,
        c: 100
      },
      mutations: {
        changeCount(state, payload) {
          console.log('c 更新')
        }
      }
    },
    b: {
      namespaced: true,
      state: {
        d: 200
      },
      mutations: {
        changeCount(state, payload) {
          console.log('d 更新')
        }
      },
      modules: {
        c: {
          namespaced: true,
          state: {
            e: 300
          },
          mutations: {
            changeCount(state, payload) {
              console.log('e 更新')
            }
          }
        }
      }
    }
  }
})
  • vue 实例输出结果
    • 可以发现添加 namespaced: true_moduleNamespaceMap 从空对象增加了一些属性,属性值是对应的模块,_mutations 对象中属性名是带有命名空间的模块名(实现目标

在这里插入图片描述

  • 在安装模块时,需要注册对应的命名空间,我们可以通过 path 计算命名空间。
// store.js
function installModule(store, rootState, path, module) {
    let namespace = store._modules.getNameSpace() 
    // todo: state and forEachOperation...
}

// module-collection.js
// 为什么在该文件添加 getNameSpace 方法 ? 该文件获取模块集合且可对路径 path 进行处理
export default class ModuleCollection {
    // todo: constructor
    // register
    getNameSpace(path) { // 获取命名空间
        let root = this.root
        return path.reduce((namespace, key) => {
            root = root.getChild(key)
            return namespace + (root.namespaced ? key + '/' : '')
        },'')
    }
}

// module.js
export default class Module {
    // todo constructor...
    get namespaced() { // 属性访问器
        return this._rawModule.namespace
    }
    // todo...
}
  • actions、getters、mutations 优化改写(添加命名空间的处理)
function installModule(store, rootState, path, module) {
    let namespace = store._modules.getNameSpace(path)
    // todo...
    module.forEachMutations((mutation, type) => {
        store._mutations[namespace + type] = (store._mutations[namespace + type] || [])
        store._mutations[namespace + type].push((payload) => { // 包装函数
            mutation.call(store, module.state, payload)
        })
    })
    module.forEachActions((actions, type) => {
        store._actions[namespace + type] = (store._actions[namespace + type] || [])
        store._actions[namespace + type].push((payload) => {
            actions.call(store, store, payload)
        })
    })
    module.forEachGetters((getters, key) => {
        // 如果 getters 重名会覆盖 所有模块的 getters 会定义到根模块上 
        store._wrappedGetters[namespace + key] = function (params) {
            return getters(module.state)
        }
    })
    // todo: forEachChild...
}
  • 打印 store 实例验证结果

在这里插入图片描述

3.6.7 模块的动态注册实现
  • 使用示例
// store/index.js
import Vue from 'vuex'
import Vue from 'vue'
Vue.use(Vuex)
const store = new Vue.Store({ /*选项配置参数,按照上述示例*/ })
// 注册模块 'moduleA'
store.registerModule('moduleA', {
    state: {
        e: 'xxx'
    }
})
// 注册嵌套模块 'moduleA/moduleB'
store.registerModule(['moduleA', 'moduleB'], {
  	state: {
    	f: 'xxx'
  	},
    // todo...
})
// 动态卸载新添加的模块
// store.unregisterModule(moduleName)
export default store

在这里插入图片描述

  • 在模块创建之后,可以使用 store.registerModule 方法注册新模块,可以通过 store.state.moduleA.estore.state.ModuleA.moduleB.f 访问模块的状态
  • 使用 store.unregisterModule(moduleName)动态卸载模块。不能使用该方法卸载静态模块(即创建 store 时声明的模块)可以通过 store.hasModule(moduleName) 方法检查该模块是否已经被注册到 store

  • 模块动态注册实现
    • registerModule 实现动态添加
      • 就是完成模块注册、安装模块和重设 store._vm
    • 动态注册模块的 getters 实现
// module-collection.js
export default class ModuleCollection {
    // todo constructor
    register(path, rootModule) {
		let newModule = new Module(rootModule)
    	rootModule.newModule = newModule
    }
}
// store/index.js
class Store {
    constructor(options) {
      // todo...
    }
    // todo...
    registerModule(path, rawModule) {
      if(typeof path === 'string') {
          path = [path]
      }
      this._modules.register(path, rawModule)
      // 返回一个树形结构 模块注册
      // 安装模块
      installModule(this, this.state, path, rawModule.newModule)
      // 安装注册完成之后需要对 getters 处理
      // resetStorevm 需要优化一下 因为此前已生成实例 _vm,所以需要销毁实例
      // 重新定义 Getters
      resetStoreVm(this, this.state)
    }
}
function resetStorevm(store, state) {
    // ...
    let oldVm = store._vm
    //...
    store._vm = new Vue({ /**/ })
    if(oldVm) {
        // 在下次 DOM 更新循环结束之后删除之前旧实例 vm
		Vue.nextTick(() => {
            oldVm.$destroyed()
        })
    }
}
  • 问题:注册空模块时会报错,因为 stateundefined
function installModule(store, rootState, path, module) {
    let namespace = store._modules.getNameSpace(path)
    if (path.length > 0) {
        let parent = path.slice(0, -1).reduce((prev, cur) => {
            return prev[cur]
        }, rootState)
        // 状态为 undefined 时 默认空对象
        module.state = module.state || {}
        Vue.set(parent, path[path.length - 1], module.state)
    }
    // todo...
}
  • 验证结果(此时注册空模块不会出现报错信息)

在这里插入图片描述


3.6.8 vuex 实现持久化插件
  • 注册第三方插件 plugins
  • 使用示例
    • Vuex 插件使用
    • 注意Vuex 插件是函数,接收唯一参数 store,在 Vuex.Store 构造器选项中 plugins 引入
// store/index.js
let store = new Vuex.Store({
  plugins: [
      // todo...
  ],
})
// 或者 新建文件 store/plugins.js
export function createPlugin(options = {}) {
    // 注意返回的是一个函数
    return function(store) {
        // todo...
    }
}
// store/index.js
import { createPlugin } from './plugins.js'
let store = new Vuex.Store({
  plugins: [ createPlugin() ],
})
  • 实现持久化插件函数 persists()
    • 插件按注册顺序依次执行
// 内置 logger 有兴趣可以实现一下哦!
import logger from 'vuex/dist/logger'
function persists(store) {
    // store 为当前实例
    store.subscribe(() => {
		// 订阅
        console.log('状态改变就执行')
    })
}
let store = new Vuex.Store({
    plugins: [persists, logger()],
    // todo....
})

在这里插入图片描述

  • 使用 localStorage
function persists(store) {
    let local = localStorage.getItem('VUEX:STATE')
    if(local) {
        // 有 local 需要替换
        store.replaceState(JSON.parse(local))
    }
    store.subscribe((mutations, state) => {
        // 如果频繁操作 需要考虑防抖节流
        // 状态进行本地存储
        localStorage.setItem('VUEX:STATE', JSON.stringify(state))
    })
}
let store = new Vuex.Store({
    plugins: [persists],
    // todo....
})

在这里插入图片描述

function persists(store) {
    let local = localStorage.getItem('VUEX:STATE')
    if(local) {
        // 有 local 需要替换
        store.replaceState(JSON.parse(local))
    }
    store.subscribe((mutations, state) => {
        // 状态进行本地存储
        localStorage.setItem('VUEX:STATE', JSON.stringify(state))
    })
}

在这里插入图片描述

  • 实现目标
    • subscribe
    • replaceState
    • plugins
class Store {
    constructor(options) {
        this._subscribers = []
        // todo...
        // 插件实现
        options.plugins = options.plugins || []
        options.plugins.forEach(plugins => plugins(this))
    }
    subscribe(fn) {
        // 订阅
        this._subscribers.push(fn)
    }
    replaceState() {
        
    }
}
  • 状态变更时订阅的事件函数执行
function installModule(store, rootState, path, module) {
    // todo...
    module.forEachMutations((mutation, type) => {
        store._mutations[namespace + type] = (store._mutations[namespace + type] || [])
        store._mutations[namespace + type].push((payload) => { // 包装函数
            mutation.call(store, module.state, payload) // 更改状态
            // 执行订阅事件
            store._subscribers.forEach(sub => { sub({ mutation, type }, store.state) })
        })
    })    
    // todo...
}
  • replaceState 实现
replaceState(newState) {
    this._vm._data.$$state = newState
}
  • 问题:无法获取新状态
    • mutations、getters 参数 state 问题
    • 因为变更状态时,module.state 状态可能不是最新状态
function installModule(store, rootState, path, module) {
    // todo...
    module.forEachMutations((mutation, type) => {
        store._mutations[namespace + type] = (store._mutations[namespace + type] || [])
        store._mutations[namespace + type].push((payload) => {
            // getState 用于获取最新状态
            mutation.call(store, getState(store, path), payload) // 更改状态
            store._subscribers.forEach(sub => { sub({ mutation, type }, store.state) })
        })
    })
    // getters 同样处理
    module.forEachGetters((getters, key) => {
    // 如果 getters 重名会覆盖 所有模块的 getters 会定义到根模块上 
    store._wrappedGetters[namespace + key] = function (params) {
        return getters(getState(store,path))
    }
})    
    // todo...
}
// 获取最新状态
function getState(store, path) {
 	return path.reduce((newState, current) => {
        return newState[current]
    }, store.state)   
}
  • 严格模式处理 strict: true
    • 严格模式下只能通过 mutations 更改状态
    • 无论何时发生了状态变更且不是mutation 函数引起的,将会抛出错误
class Store {
    constructor(options) {
        // todo...
        this._strict = options.strict
        this._committing = false
        // todo...
    }
}
  • 需要使用同步 watcher 监听 state 状态
function resetStoreVm(store, state) {
    // todo...
    store._vm = new Vue({
        data: {
            $$state: state
        },
        computed
    })
    if (store._strict) {
    // 严格模式
    store._vm.$watch(() => store._vm._data.$$state,
        // 监控 state 且深度监听和同步执行
        () => {
            // 只要状态改变就会立即执行,在状态变化后同步执行
            // _commiting 为 false 执行回调
            console.assert(store._committing, '在 mutations 之外更新状态')
        }, { deep: true, sync: true })
    // todo...
    }
}

// 调用该方法就允许 state 状态更改且不会执行 console.assert (除了异步的mutations)
_withCommitting(fn) {
    let _committing = this._committing
    this._committing = true 
    // 函数调用前为 标识 _committing 为 true,fn 函数内进行修改 state 是不会出现报错
    fn()
    this._committing = _committing
}

function installModule(store, rootState, path, module) {
    // todo...
    module.forEachMutations((mutation, type) => {
        store._mutations[namespace + type] = (store._mutations[namespace + type] || [])
        store._mutations[namespace + type].push((payload) => { // 包装函数
        	// 调用 mutations 函数允许更改状态
            store._withCommitting(() => {
              mutation.call(store, getState(store, path), payload)
            })
            store._subscribers.forEach(sub => { sub({ mutation, type }, store.state) })
        })
    })
    // todo: forEachOperation 
}
  • 此前修改 state 地方需要处理
    • mutations 函数修改 state 设置 this._committingtrue
function installModule(store, rootState, path, module) {
    // todo namespaced ...
    if (path.length > 0) {
        let parent = path.slice(0, -1).reduce((prev, cur) => {
            return prev[cur]
        }, rootState)
        module.state = module.state || {}
        // 这里会设置状态 调用 withCommitting 设置 _committing 为 true 因为这里的业务为安装模块
        store._withCommitting(() => {
            Vue.set(parent, path[path.length - 1], module.state)
        })   
    }
}

class Store {
    replaceState(newState) {
        // 更新 state 同理
        this._withCommitting(() => {
            this._vm._data.$$state = newState
        })
    }    
}
  • 此时如果 mutations 中有异步逻辑,当执行 mutation 中异步逻辑,那么同步逻辑先执行,此时 this._committingfalse,再修改 state 状态会报错,正好完成需要的逻辑!

3.6.9 辅助函数的实现
  • mapState() 实现原理
    • 配合 computed 使用
<template>
  <div id="app">
    count 的值为 {{ $store.state.count }} <br>
    count 的值为 {{ count }}
    <button @click="btnHandler">点击</button>
  </div>
</template>

<script>
const mapState = (arrList) => {
  let obj = {}
  for(let i = 0; i < arrList.length; i++) {
      let stateName = arrList[i];
      obj[stateName] = function() {
          return this.$store.state[stateName]
      }
  }
  return obj
}
export default {
  methods: {
    btnHandler() {
      this.$store.commit('changeCount', 2)
    },
  },
  computed: {
      ...mapState(['age']),
      /*
      // 基于下述
      count() {
      	return this.$store.state.count
      }
      */
  }
}
</script>
  • 优化

  • vuex 目录下新建文件 helpers.js

export const mapState = (arrList) => {
    let obj = {}
    for (let i = 0; i < arrList.length; i++) {
        let stateName = arrList[i];
        obj[stateName] = function () {
            return this.$store.state[stateName]
        }
    }
    return obj
}

// vuex/index.js
import { Store, install } from './store'
import { mapState } from './index'
export {
    mapState,
    // todo...
}

export default {
    mapState,
    // todo...
}
  • mapGetters 实现

mapState 类似

export const mapGetters = (arrList) => {
    let obj = {}
    for (let i = 0; i < arrList.length; i++) {
        let stateName = arrList[i];
        obj[stateName] = function () {
            return this.$store.getters[stateName]
        }
    }
    return obj
}
  • mapMutations 实现
export const mapMutations = (arrList) => {
    let obj = {}
    for (let i = 0; i < arrList.length; i++) {
        let mutationName = arrList[i]
        obj[mutationName]  = function(payload) {
            return this.$store.commit(mutationName, payload)
        }
    }
    return obj
}
  • mapActions 实现
export const mapActions = (arrList) => {
    let obj = {}
    for (let i = 0; i < arrList.length; i++) {
        let actionName = arrList[i]
        obj[actionName]  = function(payload) {
            return this.$store.dispatch(actionName, payload)
        }
    }
    return obj
}
// vuex/index.js
// 暴露
import { mapState, mapGetters, mapMutations, mapActions } from '../vuex/helpers'
export default {
    // todo...
    mapState,
    mapGetters,
    mapMutations,
    mapActions
}
export {
	// todo... 同理
}

mini-vuex 的实现不算太难,可能在模块的实现需要下些功夫,手写 vuex 的学习目标更多是学习优秀的设计思想!那么关于 Vuex 学习内容就这么多啦,如果您觉得内容不错的话,望您能关注🤞点赞👍收藏❤️一键三连!


  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值