Vuex 学习
本期内容主要为
Vuex
基础与实现简易mini-vuex
,Pinia
的学习后续更新!
最后,望本文能对您有所帮助!☀️
- 前置基础
-
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 状态管理模式
- 状态自管理应用通常包含
state
、view
和action
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
提供唯一的公共数据源,所有共享数据都要统一放入Store
的State
中进行存储
// 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
可视化工具中无法查询到state
中count
数据更新记录 - 更改
Vuex
的Store
的状态的唯一方法就是提交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.m1
或this.$store.state.m2
调用模块的状态 - 模块的局部状态及使用
- 对于模块内部的
mutations
和getters
接收第一个参数是模块的局部状态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
是注册在全局命名空间 -
但会出现两种问题
- ① 当不同模块中有相同命名的
mutations
和actions
时,不同模块对同已mutations
或actions
作出响应 - ② 当一个项目中
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.defineProperty
向getters
上定义属性 -
新建
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 的实现
- 实现思路:使用发布订阅模式,将定义的
mutations
和actions
先保存起来(订阅),调用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>
*/
- 上述示例会出现问题:子模块
a
和b
的mutations
的changeCount
被调用。默认模块是没有作用域,也就是说,当不同模块中有相同命名的mutations
和actions
时,不同模块对同一mutations
或actions
作出响应
来个套娃 😵
// 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
模块中状态d
跟b
模块的子模块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);
}
}
vuex
中state
输出结果
- 验证结果
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.e
和store.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()
})
}
}
- 问题:注册空模块时会报错,因为
state
为undefined
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._committing
为true
- 由
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._committing
为false
,再修改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
学习内容就这么多啦,如果您觉得内容不错的话,望您能关注🤞点赞👍收藏❤️一键三连!