Vuex学习
徐徐 抱歉选手

Vuex is a state management pattern + library for Vue.js application.

vuex提供一个中心化的store全体供所有的components使用。

It serves as a centralized store for all the components in an application, with rules ensuring that the state can only be mutated in a predictable fashion.

为什么要用vuex/想法与redux类似

对于一个self-contained app来说,state/view/actions的很容易维持(one-way data flow),但是当许多组件components需要共享一个state的时候这个三角关系就很容易被打破:有很多view依赖于同一块state,有很多view发出的actions可以变更同一块state。

当然可以通过passing props来解决父子组件间的这个问题,但是当components的层级关系越来越深,这个变更就很不好操作;但是兄弟组件sibling components如果要共享同一块数据,passing props的方法并不能解决问题。

因此vuex的出现就是extract the shared state out of the components, and manage it in a global singleton。数据/state/data应当成为单独管理的一棵树,view/UI/components应当也是单独的一棵树。

vuex-data-flow

vuex store

任何store有两个约定:

  • vuex store是响应式reactive的,当有vue组件向他拿state来用时,如果store state有所变动,vue组件中这些引用都会自动更新
  • vue组件不能直接变更store state,唯一的mutate store state的方法就是by explictly committing mutations(为了保证留下track-able record).

创建store

store本质上就是一个object,使用createStore()方法,向里面传入一个包含state field/mutation object/actions obejct等其他field的object。

1
2
3
4
5
6
const store = createStore({
state: {},
mutations: {},
actions: {},
modules: {}
})

使用store

在main.js中createApp()调用完成之后以.user(store) plugin的形式注册vuex store实例。

这个操作inject store into all child components from the root component through Vue’s plugin system,因此在组件内部可以通过this.$store访问global store的所有field。

state field

在createStore的state field中储存的data,本质上和vue实例中的data property function是一样的作用。

从store中取出最直接/不经加工的数据在ui/components中使用有以下几个方法:

使用state computed field+store.state

在组件内部的computed field通过引用store.state.访问store中state field的里某个对象,并返回该对象。

1
2
3
4
5
computed: {
count () {
return this.$store.state.count
}
}

该方法只能在一个app中的所有组件都依赖于gloabal state的情况下适用。

使用state computed field+mapState

当有多个store state需要在computed field中被使用的时候,可以使用mapState helper which generates computed getter functions for us.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
computed: mapState({// 感觉mapState像是提供了一个前往store的通道
// 有点useSelector的意思了,但是是在component中适用的useSlector
count: state => state.count,

// passing the string value 'count' is same as `state => state.count`
countAlias: 'count',

// to access local state with `this`, a normal function must be used
countPlusLocalState (state) {
return state.count + this.localCount
},

// map this.count to store.state.count
// 这种方法只在mapped computed property is the same as a state sub tree name时使用
'count'
})

如果要和组件自己的local state融合,还需要用到spread syntax。

getters field

前面提到state和view有一对多的关系,如果有多个component要用到这个state,总不能在每个view的computed field里面都走一遍mapState或者this.$store吧?因此就有了getters函数,这个函数就像react里面定义在slice文件里面的Select函数,作为参数传递给component中的useSelector hook。

定义getters

一般函数定义与参数传递 property-style access

1
2
3
4
5
6
7
8
9
10
11
12
13
const store = createStore({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos (state) {
return state.todos.filter(todo => todo.done)
}
}
})

在定义getters函数的时候,不仅仅可以用当前store的state作为第一个参数;还可以利用当前store的getters作为第二个参数,通过点访问法访问同一个getters field中的其他getters。

1
2
3
4
5
6
getters: {
// ...
doneTodosCount (state, getters) {
return getters.doneTodos.length
}
}

箭头函数定义 method-style access

以上都是依据一般函数来定义getters的,当然也可以用arrow function来定义getters,这样可以实现在不同的地方按需求传递参数。

1
2
3
4
5
6
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}

从store中取出经过加工/选择的数据在ui/components中使用有以下几个方法:

使用getters

computed field + store.getters

在component/ui/view中利用getters提取数据可以通过引用store.getters,并点访问对应的getter函数。

computed field + mapGetters

需要使用spread syntax与原组件的自有computed融合。

1
2
3
4
5
6
7
8
9
10
computed: {
// mix the getters into computed with object spread operator
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
// map `this.doneCount` to `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
])
}

mutations field

The only way to actually change state in a Vuex store is by committing a mutation.

mutation field中不能包含异步操作,因为在使用devtool debug需要跟踪before&after snapshots of the state,一个mutation被commit了但是异步操作的callback function还没有被调用,这样子state就完全是不可预测的了。

不带payload的mutation定义与调用

vuex mutation和events十分相似,每一个mutation都需要一个string type和一个handler。

在mutations field内部的每一个函数都是一个mutation handler。mutation handler内部是实现actual state modifications的地方,它接收state作为第一个参数。那么string type是什么?就是这个函数的名字。

1
2
3
4
5
6
mutations: {
increment (state) {
// mutate state
state.count++
}
}

但是mutation handler不能被直接调用,需要走一遍事件注册的流程。我们希望有以下流程:”When a mutation with type increment is triggered, call this handler.” 我们通过调用store.commit函数并传入handler function的string type来实现这个流程。

1
store.commit('increment')

如果需要在component中commit mutations,需要使用this.$store.commit(),或者mapMutations helper。

带payload的mutation定义与调用

mutation handler function除了接受state作为第一个参数,也可以把当前mutation的payload作为第二个参数传入,只需要在commit函数中同步传入第二个参数即可。如果payload是多个数据,直接用对象表示payload,并用点访问法访问。

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

object-style commit with type field

既然都用到对象了,为什么不把mutation type一起放到payload的对象中去呢?这就是obejct-style commit,也就是commit a mutation is by directly using an object that has a type property。

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

为mutation type定义constants

在flux中,一般会把当前store所有mutation都放到同一个mutation-type.js文件中export const,然后再当前store中import {},使用computed property name作为function name。

1
2
// mutation-types.js
export const SOME_MUTATION = 'SOME_MUTATION'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// store.js
import { createStore } from 'vuex'
import { SOME_MUTATION } from './mutation-types'

const store = createStore({
state: { ... },
mutations: {
// we can use the ES2015 computed property name feature
// to use a constant as the function name
[SOME_MUTATION] (state) {
// mutate state
}
}
})

actions field

actions和mutations其实类似,区别在于

  • action一般不直接变更state,因为有mutation field存在,因此action可以通过commit mutation或dispatch action来实现state field的变更。
  • action可以包含任何异步逻辑。

定义action handler与destructing语法

通常action handlers会接受一个context obejct,该context obejct把同样的store instance都暴露给(但context object本身不是the store instance itself,感觉更向copy)当前action handlers,因此可以在action handler function内部访问到当前store的state/getters等field。

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

因为conetxt本来就是copy当前store instance的object,但是我们又不需要用到这个object里面所有的内容,因此可以用ES6的数组结构获取需要的内容。

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

使用action handler

使用方法与mutations类似,通过store.dispatch()方法,不带payload、带payload、objecy-style dispatch都可以。

在组件内部通过this.$store.dispatch()或者mapActions helper实现调用。在传递给mapActions() 参数的时候可以是一个对象,也可以是一个数组,内部的action type都是string类型。

模块 modules

把全体store分成不同的模块,每个模块管理自己的state/mutations/actions/getters field。这有点类似redux中的每个slice。

当前模块state与rootState

1
2
3
4
5
6
7
8
9
10
11
12
const moduleA = {
state: () => ({ ... }),
mutations: { ... },
actions: { ... },
getters: { ... }
}
const store = createStore({
modules: {
a: moduleA
}
})
store.state.a // -> `moduleA`'s state

在每一个模块内部,muations filed和getters field接受的第一个参数state就变成了当前模块的local state(也就是只属于这个slice的state)。

1
2
3
4
5
6
7
8
mutations: {
increment (state) {
// `state` is the local module state
}
},
getters: {
doubleCount (state) {}
}

action field接受的第一个参数context,这个context.state暴露的是当前local store实例,而全体global store的实例要通过context.rootState来实现。

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

在getters field中,rootState也被作为第三个参数使用

1
2
3
4
5
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}

namespacing 命名空间

默认所有的actions和mutations都是在global namespace下被注册的。但是当modules变多了,很难保证两个modules中的函数命名没有冲突,因此就需要把当前模块的所有方法都放在固定的命名空间。通过在创建module的时候添加namespaced: true来实现命名空间化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const store = createStore({
modules: {
account: {
namespaced: true,
// module assets
state: () => ({ ... }), // module state is already nested and not affected by namespace option
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
}

// nested modules
modules: {
// inherits the namespace from parent module
myPage: {
state: () => ({ ... }),
getters: {
profile () { ... } // -> getters['account/profile']
}
},

在子模块内部访问global assets

If you want to use global state and getters, rootState and rootGetters are passed as the 3rd and 4th arguments to getter functions, and also exposed as properties on the context object passed to action functions.

在getter field中,使用getters点访问当前模块的其他getters,使用rootGetters点访问root store中定义的getter或方括号访问其他模块中定义的getter。actions field同理。

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
modules: {
foo: {
namespaced: true,//该模块已经被namespaced了

getters: {
// `getters` is localized to this module's getters
// you can use rootGetters via 4th argument of getters
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
rootGetters['bar/someOtherGetter'] // -> 'bar/someOtherGetter'
},
someOtherGetter: state => { ... }
},

actions: {
// dispatch and commit are also localized for this module
// they will accept `root` option for the root dispatch/commit
someAction ({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'
rootGetters['bar/someGetter'] // -> 'bar/someGetter'

dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'

commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
},
someOtherAction (ctx, payload) { ... }
}
}
}

在子模块内部添加global assets

If you want to register global actions in namespaced modules, you can mark it with root: true and place the action definition to function handler.

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

actions: {
someAction: {
root: true,
// 在foo模块下定义的action handler是全局的
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}

在component内部访问使用模块

每次都得把模块说明白,如果有多层嵌套就得有很多斜杠和模块名。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
methods: {
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()
])
}
// 下面这种写法还好些
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}

可以通过createNamespacedHelpershelper将当前component绑定到特定的的模块去。

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

const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')

export default {
methods: {
// look up in `some/nested/module`
...mapActions([
'foo',
'bar'
])
}
}
  • 本文标题:Vuex学习
  • 本文作者:徐徐
  • 创建时间:2020-12-24 08:18:06
  • 本文链接:https://machacroissant.github.io/2020/12/24/vuex-summary/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
 评论