我眼中的Vuex

记录一下我所理解的vuex
或者说是官方文档的阅读笔记
所有代码均来自Vuex 是什么? | Vuex

what is Vuex

Vuex像是一个大盒子 把有关于数据的操作以及数据从组件里面抽离出来
把数据跟视图进行绑定
形成一个 action -> state -> view -> action的数据流
减少各个组件之间的耦合度

核心概念(名词

state

可以认为是data 也就是在vuex中储存的数据 or 状态

getter

对于state进行访问的时候可以增加个计算过程
就是增加了一个计算属性
可以返回一个函数
mapgetters辅助函数:
将getter直接混入到组件的计算属性中 减少代码量
官方demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { mapGetters } from 'vuex'

export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}

我的理解是遍历给的list 轮流给当前组件的computed进行绑定
给getter起个名字(有效增加可读性

1
2
3
4
mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})

Mutation

由于vuex的理念 在组件中是不能直接修改vuex中的store的 需要在vuex中提前设置好Mutation 通过这个方法来修改vuex中的数据
也就是增加了一层setter

1
2
3
4
5
6
7
8
9
10
11
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})

不能直接调用这个函数 🙅‍♂️
(我猜是因为作用域的关系无法传入state所以不让直接调用
需要用commit来进行调用

1
store.commit(‘increment’)

向mutation中传参的话需要来执行
参数在文档中被叫做payload

1
store.commit(‘increment’, 10)

对象风格的提交方式
直接提交包含type属性的对象

1
2
3
4
store.commit({
type: 'increment',
amount: 10
})

mutation需要注意的点

  1. 最好提前在你的 store 中初始化好所有所需属性。
  2. 当需要在对象上添加新属性时,你应该
  • 使用Vue.set(obj, ‘newProp’, 123), 或者
  • 以新对象替换老对象。例如
    1
    state.obj = { ...state.obj, newProp: 123 }

第二条是为什么呢?因为可读性么?

  1. Mutation 必须是同步函数
    每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。如果是异步的话,devtools 不知道什么时候回调函数实际上被调用。所以不要把devtools整懵逼:-D

  2. 可以使用常量来代替事件类型(也就是Mutation的名字 在需要多人协作的大型项目中,这会很有帮助。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // mutation-types.js
    export const SOME_MUTATION = 'SOME_MUTATION'

    // store.js
    import Vuex from 'vuex'
    import { SOME_MUTATION } from './mutation-types'

    const store = new Vuex.Store({
    state: { ... },
    mutations: {
    // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名
    [SOME_MUTATION] (state) {
    // mutate state
    }
    }
    })
  3. 在组件中提交 Mutation(跟setter一样 不过一个是绑定了一个返回值 一个是绑定了一个函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import { mapMutations } from 'vuex'

    export default {
    // ...
    methods: {
    ...mapMutations([
    'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`

    // `mapMutations` 也支持载荷:
    'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
    ]),
    ...mapMutations({
    add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
    })
    }
    }

Action

Action类似mutation

  • Action 提交的是 mutation,而不是直接变更状态。(mutation更像setter了
  • Action 可以包含任意异步操作。
    注册一个Action
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    ...
    mutations: {
    increment (state) {
    state.count++
    }
    },
    actions: {
    increment (context) {
    context.commit('increment')
    }
    }
    ...

Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用context.commit提交一个 mutation,或者通过context.state和context.getters来获取 state 和 getters。
利用参数解构)可以优化代码

1
2
3
4
5
actions: {
increment ({ commit }) {
commit('increment')
}
}

分发Action(使用Action

1
store.dispatch(‘increment’)

使用Action就可以执行异步操作了 (将mutation看做原子操作

1
2
3
4
5
6
7
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}

跟Mutation相似 他也支持载荷(payload)跟对象形式来分发(传参)

1
2
3
4
5
6
7
8
9
store.dispatch('incrementAsync', {
amount: 10
})

// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})

来看一个更加实际的购物车示例,涉及到调用异步 API分发多重 mutation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
)
}
}

在组件中分发 可以一样使用mapActions

1
2
3
4
5
6
7
8
9
10
11
methods: {
...mapActions([
'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`

// `mapActions` 也支持载荷:
'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
})
}

store.dispatch可以处理被触发的 action 的处理函数返回的 Promise,并且store.dispatch仍旧返回 Promise

1
2
3
4
5
6
7
8
9
10
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}

现在你可以

1
2
3
store.dispatch('actionA').then(() => {
// ...
})

如果我们利用 async / await ,我们可以如下组合 action:

1
2
3
4
5
6
7
8
9
10
11
// 假设 getData() 和 getOtherData() 返回的是 Promise

actions: {
async actionA ({ commit }) {
commit('gotData', await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch('actionA') // 等待 actionA 完成
commit('gotOtherData', await getOtherData())
}
}

Module

由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
也就是说 在vuex里面这个大盒子里面
还可以又多个小盒子组成
这样可以减少vuex之间不同的store的耦合度
每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}

const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}

const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})

模块的局部状态
对于模块内部的 mutation 和 getter,接收的第一个参数是模块的局部状态对象
即对于 mutation 与 getter来说 接收到的第一个参数 是该模块里的自身store
同样,对于模块内部的 action,局部状态通过context.state暴露出来,根节点状态则为context.rootState:
即对于 模块里的 action 模块的store 通过上下文中的store来判断 根模块的store则是context.rootState来访问

1
2
3
4
5
6
7
8
9
10
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}
}

命名空间

默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应
可以通过添加namespaced: true的方式使其成为带命名空间的模块。
会自动根据模块注册的路径调整命名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,

// 模块内容(module assets)
state: { ... }, // 模块内的状态已经是嵌套的了,使用 `namespaced` 属性不会对其产生影响
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 嵌套模块
modules: {
// 继承父模块的命名空间
myPage: {
state: { ... },
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// 进一步嵌套命名空间
posts: {
namespaced: true,
state: { ... },
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}
})

在带命名空间的模块中 注册全局
只需要添加一个root:true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
modules: {
foo: {
namespaced: true,

actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
...

带命名空间的绑定函数
当使用mapState ,mapGetters ,mapActions 和mapMutations 这些函数来绑定带命名空间的模块时,写起来可能比较繁琐:

1
2
3
4
5
6
7
8
9
10
11
12
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})
},
methods: {
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()
])
}

对于这种情况 可以把模块的空间名称字符串(也就是模块的路径传给这些函数)这样所有的绑定都会把该模块作为上下文

1
2
3
4
5
6
7
8
9
10
11
12
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}

通过使用createNamespacedHelpers 创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
export default {
computed: {
// 在 `some/nested/module` 中查找
...mapState({
a: state => state.a,
b: state => state.b
})
},
methods: {
// 在 `some/nested/module` 中查找
...mapActions([
'foo',
'bar'
])
}
}

模块动态注册
在 store 创建之后,可以使用store.registerModule方法追加模块到store上
追加的模块可以使用store.unregisterModule(moduleName)来进行卸载

1
2
3
4
5
6
7
8
// 注册模块 `myModule`
store.registerModule('myModule', {
// ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})

项目结构

需要注意的规则:

  1. 应用层级的状态应该集中到单个 store 对象中。
  2. 提交mutation是更改状态的唯一方法,并且这个过程是同步的。
  3. 异步逻辑都应该封装到action里面。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    ├── index.html
    ├── main.js
    ├── api
    │ └── ... # 抽取出API请求
    ├── components
    │ ├── App.vue
    │ └── ...
    └── store
    ├── index.js # 我们组装模块并导出 store 的地方
    ├── actions.js # 根级别的 action
    ├── mutations.js # 根级别的 mutation
    └── modules
    ├── cart.js # 购物车模块
    └── products.js # 产品模块

插件

Vuex 的 store 接受plugins选项,这个选项暴露出每次 mutation 的钩子。Vuex 插件就是一个函数,它接收 store 作为唯一参数:
等于说给store的mutation又加了个装饰器 每次mutation执行后会调用这个函数

1
2
3
4
5
6
7
8
9
10
11
const myPlugin = store => {
// 当 store 初始化后调用
store.subscribe((mutation, state) => {
// 每次 mutation 之后调用
// mutation 的格式为 { type, payload }
})
}
const store = new Vuex.Store({
// ...
plugins: [myPlugin]
})

在插件内提交 Mutation

在插件中不允许直接修改状态——类似于组件,只能通过提交 mutation 来触发变化。emo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function createWebSocketPlugin (socket) {
return store => {
socket.on('data', data => {
store.commit('receiveData', data)
})
store.subscribe(mutation => {
if (mutation.type === 'UPDATE_DATA') {
socket.emit('update', mutation.payload)
}
})
}
}
const plugin = createWebSocketPlugin(socket)

const store = new Vuex.Store({
state,
mutations,
plugins: [plugin]
})

严格模式

开启严格模式,仅需在创建 store 的时候传入strict: true:

1
2
3
4
const store = new Vuex.Store({
// ...
strict: true
})

在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。

表单处理

当在严格模式中使用 Vuex 时,在属于 Vuex 的 state 上使用v-model会比较棘手:
<input v-model=“obj.message”>
用“Vuex 的思维”去解决这个问题的方法是:给中绑定 value,然后侦听input或者change事件,在事件回调中调用 action:

1
2
3
4
5
6
7
8
9
10
11
12
<input :value="message" @input="updateMessage">
// ...
computed: {
...mapState({
message: state => state.obj.message
})
},
methods: {
updateMessage (e) {
this.$store.commit('updateMessage', e.target.value)
}
}

1
2
3
4
5
mutations: {
updateMessage (state, message) {
state.obj.message = message
}
}

双向绑定的计算属性

必须承认,这样做比简单地使用“v-model+ 局部状态”要啰嗦得多,并且也损失了一些v-model中很有用的特性。另一个方法是使用带有 setter 的双向绑定计算属性:

1
2
3
4
5
6
7
8
9
10
11
<input v-model="message">
// ...
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}

测试

测试 | Vuex
热重载
https://vuex.vuejs.org/zh/guide/hot-reload.html

总结

Vuex定义了一个全局对象:store
其中所有数据都被封装了起来
如果需要访问 必须要调用getter方法
如果需要修改必须要使用mutation(像是setter)
增加了一些内置Action来方便修改一系列数据 以及重用于不同的store
由于把所有的store都放在一个地方会非常臃肿
Vuex又引入了module来增加命名空间
为了让其他mutation在不需要做任何代码变动的前提下增加额外功能
或者说抽离 mutation的职能 又增加了插件
也就是在每次mutation调用后调用 plugins
增加mutation(setter)的功能
通过这些操作 来减小各个组件之间的耦合度
v