状态管理(store)
Mpx 参考 vuex 设计实现了外部状态管理系统(store),其中的概念与 api 与 vuex 保持一致,为了更好地支持状态模块管理和跨团队合作场景,我们提出多实例 store 作为 vuex 中 modules 的替代方案,该方案在模块拆分及合并上的灵活性远高于 modules。
介绍
Store
是一个全局状态管理容器,能够轻松实现复杂场景下的组件通信需求,store 与简单的全局状态对象主要有以下两点不同:
Store 中存放的状态是响应式的。当用户将 store 中的状态注入到组件以后,若 store 中状态发生变化,那么对应的组件也会得到更新。
你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径是显式地提交变更(commit mutation),这种方式能使整个应用中的状态变更变得可回朔可追踪,同时也更加安全。
创建 store
让我们来创建一个简单 store。创建过程直截了当,仅需要提供一个初始 state 对象和一些 mutation 方法,并调用 Mpx 暴露的 createStore
方法进行创建:
import {createStore} from '@mpxjs/core'
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
export default store
现在,你可以通过 store.state 来获取状态对象,以及通过 store.commit 方法触发状态变更:
store.commit('increment')
console.log(store.state.count) // 1
接下来,我们将会更深入地探讨一些 store 的核心概念。让我们先从 State
State 存放了 store 中的原始状态数据,可以通过 store.state
进行访问。
在组件中获取 state
Mpx 在小程序组件中实现了数据响应,而刚才提到 store 中的状态也是响应式的,组件中获取 store 状态最简单的方法就是建立一个计算属性,在计算属性中访问 store 中需要的状态数据并返回:
// store.js
import {createStore, createComponent} from '@mpxjs/core'
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
createComponent({
computed: {
count () {
return store.state.count
}
}
})
当 store.state.count 发生变化时, 组件中访问了 count 计算属性的 watcher 将得到响应。
于 vuex 中的不同的地方在于,vuex 奉行
单一状态树
,一个应用当中只存在一个 store 示例,用户能够在组件中通过this.$store
隐式地访问到当前应用的 store;而 Mpx 当中为了追求灵活便捷的状态模块化管理及跨团队合作的能力,支持了多实例 store,用户需要显式地引入 store 实例,并通过计算属性将其注入到组件当中。
MapState 辅助函数
当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState
辅助函数帮助我们生成计算属性
import store from '../store'
import {createComponent} from '@mpxjs/core'
createComponent({
computed: store.mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 state => state.count
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
})
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。
import store from '../store'
import {createComponent} from '@mpxjs/core'
createComponent({
computed: store.mapState([
// 映射 this.count 为 store.state.count
'count'
])
})
对象展开运算符
MapState 函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。但是自从有了对象展开运算符,我们可以极大地简化写法:
import store from '../store'
import {createComponent} from '@mpxjs/core'
createComponent({
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将 mapState 返回的对象合并到计算属性中
...store.mapState([
'count',
// ...
])
}
})
使用 store 并不意味着你需要将所有的状态放入 store。如果有些状态严格属于单个组件,最好将其放到组件内部的 data 当中。
Getter
有时候我们需要从 state 中派生出一些状态,例如经过过滤的列表:
createComponent({
computed: {
doneTodos () {
return store.state.todos.filter(todo => todo.done)
}
}
})
如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它,无论哪种方式都不是很理想。
这个时候我们可以在 store 中定义 getter
来完成这个功能,getter 可以简单认为是 store 中的计算属性。同计算属性一样,getter 的返回值会根据它的依赖被缓存起来,只有当它的依赖发生变化时才会被重新计算。
Getter 接受 state 作为第一个参数:
import {createStore} from '@mpxjs/core'
const store = createStore({
state: {
todos: [
{ id: 1, text: 'sth1', done: true },
{ id: 2, text: 'sth2', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
export default store
定义好的 getter 可以通过 store.getters
访问:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getter 也可以接受其他 getters 作为第二个参数, rootState作为第三个参数,rootState是模块化中引入的概念,之后会详细介绍:
getters: {
// ...
doneTodosCount: (state, getters, rootState) => {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1
我们采用与state类似的方法将其注入到组件中进行访问:
computed: {
doneTodosCount () {
return store.getters.doneTodosCount
}
}
MapGetters 辅助函数
MapGetters
的作用与使用方法同上面提到的 mapState
高度类似,唯一的区别在于 mapGetters 用于映射 store 中的 getter:
import store from '../store'
import {createComponent} from '@mpxjs/core'
createComponent({
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...store.mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
})
如果你想将一个 getter 在组件中映射为另一个名字,使用对象形式:
store.mapGetters({
// 映射 this.doneCount 为 store.getters.doneTodosCount
doneCount: 'doneTodosCount'
})
Mutation
更改 store 中的状态的唯一方法是提交 mutation。Mutation 非常类似于事件:每个 mutation 都有一个字符串的类型(type) 和 一个回调函数(handler)。这个回调函数就是我们实际进行状态更改的地方,它接受 state 作为第一个参数:
import {createStore} from '@mpxjs/core'
const store = createStore({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
export default store
当你需要触发某个 mutation 时,你需要使用对应的 type
调用 store.commit
方法,就像触发某个事件一样:
store.commit('increment')
提交载荷(Payload)
你可以向 store.commit
传入额外的参数,作为 mutation 的载荷(payload):
// ...
mutations: {
increment (state, n) {
state.count += n
}
}
store.commit('increment', 10)
在大多数情况下,载荷应该是一个对象,这样可以包含多个字段并且增强 mutation 的可读性:
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
store.commit('increment', {
amount: 10
})
Mutation 必须是同步函数
一条重要的原则就是要记住 mutation 必须是同步函数
在组件中提交 Mutation
你可以在组件中使用 store.commit('increment')
提交 mutation,或者使用 store.mapMutations
辅助函数将组件中名为 increment
的 method
映射为 store.commit('increment')
调用。
import { createComponent } from '@mpxjs/core'
import store from '../store'
createComponent({
// ...
methods: {
...store.mapMutations([
// 将 this.increment() 映射为 store.commit('increment')
'increment',
// mapMutations 也支持载荷:将 this.incrementBy(amount) 映射为 store.commit('incrementBy', amount)
'incrementBy'
]),
// mapMutations同样支持传入对象形式的参数进行别名映射
...store.mapMutations({
// 将 this.add() 映射为 store.commit('increment')
add: 'increment'
})
}
})
Action
Action 类似于 mutation,不同在于:
- Action 不能直接变更状态,但是可以提交 mutation 进行状态变更
- Action 可以包含任意异步操作
让我们来创建一个简单的 action:
import {createStore} from '@mpxjs/core'
const store = createStore({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
export default store
Action 函数接受一个 context
对象,因此你可以调用 context.commit
提交一个 mutation,调用 context.dispatch
触发其他的 action 调用,或者通过 context.rootState
、context.state
和 context.getters
来获取全局state、局部state 和 全局 getters。
实践中,我们会经常用到 ES2015 的参数解构来简化代码:
actions: {
increment ({ commit }) {
commit('increment')
}
}
调用 action
Action 可以通过 store.dispatch
方法进行调用:
store.dispatch('increment')
乍一眼看上去感觉多此一举,我们直接分发 mutation 岂不更方便?实际上并非如此,还记得 mutation 必须同步执行这个限制么?Action 就不受约束!我们可以在 action 内部执行异步操作:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
Action 同样支持载荷:
// 调用载荷
store.dispatch('incrementAsync', {
amount: 10
})
来看一个更加实际的购物车示例,涉及到调用异步 API 和提交多个 mutation:
actions: {
checkout ({ commit, state }, products) {
// 把当前购物车的物品备份起来
const savedCartItems = [...state.cart.added]
// 发出结账请求,然后乐观地清空购物车
commit(types.CHECKOUT_REQUEST)
// 购物 API 接受一个成功回调和一个失败回调
shop.buyProducts(
products,
// 成功操作
() => commit(types.CHECKOUT_SUCCESS),
// 失败操作
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}
}
注意我们进行了一系列异步操作,并且通过提交 mutation 来记录 action 产生的副作用(即状态变更)。
在组件中调用 Action
你可以在组件中使用 store.dispatch('increment')
进行 action 调用,或者使用 store.mapActions
辅助函数将组件中名为 increment
的 method
映射为 store.dispatch('increment')
:
import { createComponent } from '@mpxjs/core'
import store from '../store'
createComponent({
// ...
methods: {
...store.mapActions([
// 将 this.increment() 映射为 store.dispatch('increment')
'increment',
// mapActions 也支持载荷:将 this.incrementBy(amount) 映射为 store.dispatch('incrementBy', amount)
'incrementBy'
]),
// mapActions 同样支持传入对象形式的参数进行别名映射
...store.mapActions({
// 将 this.add() 映射为 store.dispatch('increment')
add: 'increment'
})
}
})
组合 Action
Action 通常是异步的,那么如何知道 action 什么时候结束呢?更重要的是,我们如何才能组合多个 action,以处理更加复杂的异步流程?
在 Mpx 中,action 永远返回一个 Promise,你可以在定义 action 手动返回一个 Promise,即使你没有这样做,框架也会对你 action 的返回值进行 Promise.resolve(returned)
包装,确保你可以使用 Promise 的方式处理多个 action 之间的异步组合:
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
现在你可以通过 then
方法获取 actionA
执行完毕的时机:
store.dispatch('actionA').then(() => {
// ...
})
在另外一个 action 中也可以通过这种方式进行一系列异步调用:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
如果我们利用 async / await,我们能够更方便地对一系列异步操作进行组合:
// 假设 getData() 和 getOtherData() 返回的是 Promise
actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}
Modules
Mpx 虽然支持了 modules,但并不推荐使用。在 Mpx 中,我们更推荐使用[多实例模式](#多实例 store)进行对应用状态进行模块划分
在 Mpx 中,modules 的设计与 vuex 中基本保持一致,在 createStore 中将子模块配置传入 modules 配置项中即可使用:
import {createStore} from '@mpxjs/core'
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = createStore({
modules: {
moduleA,
moduleB
}
})
store.state.moduleA // -> moduleA 的状态
store.state.moduleB // -> moduleB 的状态
export default store
Mpx 并未实现 vuex 中的命名空间,除 state 外的所有属性(getters / mutations / actions)将被平铺展开到根 store 的对应空间下。在多实例 store 中,我们的实现方式则与命名空间高度相似。
模块的局部状态
对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象。
const moduleA = {
state: { count: 0 },
mutations: {
increment (state) {
// 这里的 state 对象是模块的局部状态
state.count++
}
},
getters: {
doubleCount (state) {
return state.count * 2
}
}
}
对于模块内部的 action,局部状态通过 context.state
访问,根状态则通过 context.rootState
访问:
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}
对于模块内部的 getter,根状态能通过第三个参数访问:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
模块在组件中的引入方式
const store = createStore({
modules: {
a: {
state: {
name: 1
},
getters: {
getName: s => s.name
}
},
b: moduleB
}
})
createComponent({
// 我们支持了多种方式引入子模块中的 state
computed: {
// 通过函数引入
...store.mapState({
nameA: state => state.a.name
}),
// 通过路径字符串引入
...store.mapState({
nameA2: 'a.name'
}),
// 通过传入模块路径引入
...store.mapState('a', ['name']),
// 对于 getters / mutations / actions,由于我们没有实现 namespace,子模块当中定义的 getters / mutations / actions 都能在根空间下直接访问,正常调用映射方法即可
...store.mapGetters('getName')
}
})
多实例 store
在 Mpx 中,我们允许在一个应用下创建多个 store 实例,进行模块化分布式的数据管理,同时提供了 deps 声明模块依赖的机制,让用户自由组合多个 store 实例并基于这些已有的 store 创建新的继承 store。在实际业务使用中,我们发现多实例模式的灵活性远高于 module,更加适合跨团队合作当中的数据管理。
使用多实例 store 的方式非常简单,你只需要多次调用 createStore
api 创建出多个 store 示例,并分别将其注入到组件中即可使用,简单示例如下:
import { createComponent, createStore } from '@mpxjs/core'
const storeA = createStore({
state: {
countA: 0
},
mutations: {
incrementA (state) {
state.countA++
}
}
})
const storeB = createStore({
state: {
countB: 0
},
mutations: {
incrementB (state) {
state.countB++
}
}
})
createComponent({
computed: {
// ...
...storeA.mapState(['countA']),
...storeB.mapState(['countB'])
},
methods: {
// ...
...storeA.mapMutations(['incrementA']),
...storeB.mapMutations(['incrementB'])
}
})
可以看到 Mpx 中的 map 辅助方法都挂载在 store 实例上,正是为了支持多实例 store 的实现
合并继承多实例 store
在实际的跨团队业务当中,我们既希望不同团队间的数据管理尽量解耦,也希望一些共同的部分能够复用,这就要求我们的 store 实例可以以某种方式组合起来使用,我们提供了 deps 能来实现多实例 store 的合并与继承。
承接上面的示例,我们基于 storeA 和 storeB 创建一个新的 storeC,在 storeC 当中可以定义自身的独立状态,也能基于 storeA 和 storeB 进行状态衍生:
import { createComponent, createStore } from '@mpxjs/core'
const storeA = createStore({
state: {
countA: 0
},
mutations: {
incrementA (state) {
state.countA++
}
}
})
const storeB = createStore({
state: {
countB: 0
},
mutations: {
incrementB (state) {
state.countB++
}
}
})
const storeC = createStore({
state: {
countC: 0
},
getters: {
abc (state) {
// 此处 state.storeA 指向了原始的 storeA.state
return state.storeA.countA + state.storeB.countB + state.countC
}
},
mutations: {
incrementC (state) {
state.countC++
}
},
actions: {
incrementB ({ commit }) {
// storeC内部也可以通过命名空间路径的方式提交 storeB 的 mutation
commit('storeB.incrementB')
}
},
// 此处 deps 声明了 storeC 的依赖,依赖中的 state / getters / mutations / actions 都会以 deps 中的 key 为命名空间存放在 storeC 对应的域下
deps: {
storeA,
storeB
}
})
// 通过继承合并得到的 storeC,我们可以完整访问其依赖 storeA / storeB
createComponent({
computed: {
// ...
// 使用路径字符串或函数映射,可以看出和 modules 中 mapState 的方式非常类似
...storeC.mapState({
countA: 'storeA.countA',
countA2: state => state.storeA.countA
}),
// 传入命名空间参数映射 storeB 中的 countB
...storeC.mapState('storeB', ['countB']),
// 映射基于 storeA/B/C 衍生得到的 getters
...storeC.mapGetters(['abc'])
},
methods: {
// ...
// mutation不支持函数映射
// 下面代码以三种方式分别映射了increment、incrementB和incrementC
...storeC.mapMutations({
incrementA: 'storeA.incrementA'
}),
...storeC.mapMutations('storeB', ['incrementB']),
...storeC.mapMutations(['incrementC'])
}
})
简单来讲,作为 deps 的 store 会以注册在 deps 中的 key 值作为命名空间,将其原始的 state / getters / mutations / actions 存放在新生成 store 对应的域下,便于新 store 对其进行访问并衍生出新的数据或操作,如上述示例中,storeA.state 会存放在 storeC.state.storeA 中,对于 getters / mutations / actions 亦然。
在 Typescript 中使用 store
Mpx 自 2.2 版本开始支持 Typescript,为了更好地支持 store 中的类型推导,我们针对 Typescript 环境提供了变种的 store api createStoreWithThis
进行 store 创建,该 api 最主要的变化在于定义 getters,mutations 和 actions 时,自身的 state,getters 等属性不再通过参数传入,而是会挂载到函数的执行上下文 this
当中,通过 this.state 或 this.getters 的方式进行访问,简单的使用示例如下:
const store = createStoreWithThis({
state: {
aa: 1,
bb: 2
},
getters: {
cc() {
// 使用 this.state 访问 state
return this.state.aa + this.state.bb
}
},
actions: {
doSth3() {
// 使用 this.getters 访问 getter
console.log(this.getters.cc)
return false
}
}
})
详细的使用方式及推导规则请查看 [Typescript 支持](todo add link)章节。