【1】vuex 是什么
github 站点: https://github.com/vuejs/vuex,在线文档: https://vuex.vuejs.org/zh-cn/
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。每一个 Vuex 应用的核心就是 store(仓库)。“store”
基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。
Vuex 和单纯的全局对象有以下两点不同:
-
Vuex 的状态存储是响应式的。
当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。 -
你不能直接改变 store 中的状态。
改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
简单来说: 对vue 应用中多个组件的共享状态进行集中式的管理(读/写)
VueX是适用于在Vue项目开发时使用的状态管理工具。
通过定义和隔离状态管理中的各种概念并通过强制规则维持视图和状态间的独立性,我们的代码将会变得更结构化且易维护。
这就是 Vuex 背后的基本思想,借鉴了 Flux (opens new window)、Redux (opens new window)和 The Elm Architecture (opens new window)。与其他模式不同的是,Vuex 是专门为 Vue.js 设计的状态管理库,以利用 Vue.js 的细粒度数据响应机制来进行高效的状态更新。
状态管理模式&&状态自管理应用
如下一个简单计数器:
new Vue({
// state
data () {
return {
count: 0
}
},
// view
template: `
<div>{{ count }}</div>
`,
// actions
methods: {
increment () {
this.count++
}
}
})
这个状态自管理应用包含以下几个部分:
-
state: 驱动应用的数据源
-
view: 以声明方式将state 映射到视图
-
actions: 响应在view 上的用户输入导致的状态变化(包含n 个更新状态的方法)
以下是一个表示“单向数据流”理念的简单示意:
多组件共享状态的问题
多个视图依赖于同一状态,来自不同视图的行为需要变更同一状态。
以前的解决办法:
- 将数据以及操作数据的行为都定义在父组件
- 将数据以及操作数据的行为传递给需要的各个子组件(有可能需要多级传递)
vuex 就是用来解决这个问题的,vuex 是一个专门为vue.js应用程序开发的状态管理模式。
【2】VUEX核心概念和API
vuex中,有默认的五种基本的对象:state、getters、mutations、actions、modules。
① state
存储状态(变量),它应该是唯一的。
/*
状态对象模块
*/
import storageUtils from '../utils/storageUtils'
export default {
todos: storageUtils.readTodos()
}
② getters
对数据获取之前的再次编译,可以理解为state的计算属性。我们在组件中使用 $sotre.getters.fun()
。
- 包含多个计算属性(get)的对象
- 谁来读取: 组件中:
$store.getters.xxx
/*
包含n个基于state的getter计算属性方法的对象模块
*/
export default {
// 总数量
totalSize (state) {
return state.todos.length
}
}
③ mutations
修改状态,并且是同步的,在组件中使用$store.commit('',params)
。这个和我们组件中的自定义事件类似。
- 包含多个直接更新state 的方法(回调函数)的对象
- 谁来触发:
action 中的commit('mutation 名称')
- 只能包含同步的代码, 不能写异步代码
const mutations = {
yyy (state, {data1}) {
// 更新state 的某个属性
}
}
简单来说mutations是操作state数据的方法的集合,比如对该数据的修改、增加、删除等等。
④ actions
异步操作,在组件中使用是$store.dispath('')
。
- 包含多个事件回调函数的对象
- 通过执行
commit()来触发mutation 的调用, 间接更新state
- 谁来触发: 组件中
$store.dispatch('action 名称', data1) // 'zzz'
- 可以包含异步代码(定时器, ajax)
const actions = {
zzz ({commit, state}, data1) {
commit('yyy', {data1})
}
}
⑤ modules
store的子模块,为了开发大型项目,方便状态管理而使用的。其包含多个module, 一个module 是一个store 的配置对象并与一个组件(包含有共享数据)对应。
使用流程如下图
组件分发(dispatch)动作到Actions,Actions提交行为到Mutations,Mutations对State进行改变然后映射到组件上。
【3】VUEX简单实例计数器
① 安装vuex
npm方式
npm install --save vuex
Yarn
yarn add vuex
② main.js引入
/*
入口js
*/
import Vue from 'vue'
import Counter from './Counter.vue'
import store from './store'
new Vue({
el: '#app',
components: {
Counter
},
template: '<Counter/>',
store // 注册上vuex的store: 所有组件对象都多一个属性$store
})
③ Counter组件
<template>
<div>
<p>click {{count}} times, count is {{evenOrOdd}}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="incrementIfOdd">increment if odd</button>
<button @click="incrementAsync">increment async</button>
</div>
</template>
<script>
export default {
mounted () {
console.log(this.$store)
},
computed: {
count () {
return this.$store.state.count
},
evenOrOdd () {
return this.$store.getters.evenOrOdd
}
},
methods: {
increment () {
this.$store.dispatch('increment')
},
decrement () {
this.$store.dispatch('decrement')
},
incrementIfOdd () {
this.$store.dispatch('incrementIfOdd')
},
incrementAsync () {
this.$store.dispatch('incrementAsync')
}
}
}
</script>
<style>
</style>
computed和methods可以优化为如下:
import {mapState, mapGetters, mapActions} from 'vuex'
computed: {
...mapState(['count']),
...mapGetters(['evenOrOdd'])
},
methods: {
...mapActions(['increment', 'decrement', 'incrementIfOdd', 'incrementAsync'])
}
④ store.js
/*
vuex最核心的管理对象store
*/
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
/*
相当于data对象的状态对象
*/
const state = {
count: 0 // 指定初始化数据
}
/*
包含了n个直接更新状态的方法的对象
*/
const mutations = {
INCREMENT (state) {
state.count++
},
DECREMENT (state) {
state.count--
}
}
/*
包含了n个间接更新状态的方法的对象 actions-->mutations
*/
const actions = {
increment ({commit}) {
// 提交一个mutation请求
commit('INCREMENT')
},
decrement ({commit}) {
// 提交一个mutation请求
commit('DECREMENT')
},
incrementIfOdd ({commit, state}) {
if(state.count%2===1) {
// 提交一个mutation请求
commit('INCREMENT')
}
},
incrementAsync ({commit}) {
setTimeout(() => {
// 提交一个mutation请求
commit('INCREMENT')
}, 1000)
},
}
/*
包含多个getter计算属性的对象
*/
const getters = {
evenOrOdd (state) { // 当读取属性值时自动调用并返回属性值
return state.count%2===0 ? '偶数' : '奇数'
}
}
**向外暴露store 对象**
export default new Vuex.Store({
state,
mutations,
actions,
getters
})
【4】state与mapState
① state
单一状态树
Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT (opens new window))”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
存储在 Vuex 中的数据和 Vue 实例中的 data 遵循相同的规则,例如状态对象必须是纯粹 (plain) 的
在 Vue 组件中获得 Vuex 状态
那么我们如何在 Vue 组件中展示状态呢?由于 Vuex 的状态存储是响应式的,从 store 实例中读取状态最简单的方法就是在计算属性 (opens new window)中返回某个状态:
// 创建一个 Counter 组件
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}
每当 store.state.count
变化的时候, 都会重新求取计算属性,并且触发更新相关联的 DOM。
然而,这种模式导致组件依赖全局状态单例。在模块化的构建系统中,在每个需要使用 state 的组件中需要频繁地导入,并且在测试组件时需要模拟状态。
Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个子组件中(需调用 Vue.use(Vuex)):
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`
})
通过在根实例中注册 store 选项,该 store 实例会注入到根组件下的所有子组件中,且子组件能通过 this.$store
访问到。让我们更新下 Counter 的实现:
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
② mapState 辅助函数
当一个组件需要获取多个状态的时候,将这些状态都声明为计算属性会有些重复和冗余。为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键:
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭头函数可使代码更简练
count: state => state.count,
// 传字符串参数 'count' 等同于 `state => state.count`
countAlias: 'count',
// 为了能够使用 `this` 获取局部状态,必须使用常规函数
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组。
computed: mapState([
// 映射 this.count 为 store.state.count
'count'
])
③ 对象展开运算符
mapState 函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。但是自从有了对象展开运算符 (opens new window),我们可以极大地简化写法:
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
④ 组件仍然保有局部状态
使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。
【5】Getters和mapGetters
① getters
有时候我们需要从 store 中的 state 中派生出一些状态,例如对列表进行过滤并计数:
computed: {
doneTodosCount () {
return this.$store.state.todos.filter(todo => todo.done).length
}
}
如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它——无论哪种方式都不是很理想。
Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
Getter 接受 state 作为其第一个参数:
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
② 通过属性访问
Getter 会暴露为 store.getters
对象,你可以以属性的形式访问这些值:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getter 也可以接受其他 getter 作为第二个参数:
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1
我们可以很容易地在任何组件中使用它:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
注意,getter 在通过属性访问时是作为 Vue 的响应式系统的一部分缓存其中的。
③ 通过方法访问
你也可以通过让 getter 返回一个函数,来实现给 getter 传参。在你对 store 里的数组进行查询时非常有用。
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
注意,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果。
④ mapGetters 辅助函数
mapGetters 辅助函数仅仅是将 store 中的 getter 映射到局部计算属性:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getter 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
如果你想将一个 getter 属性另取一个名字,使用对象形式:
...mapGetters({
// 把 `this.doneCount` 映射为 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'
})
⑤ 使用计算属性给data里属性赋值
computed中的属性和data中的属性最终都会加载到app这个实例下,如果data中的实例属性被创建完成的时候,computed中的实例属性还没被创建,很明显,在data中获取到computed中的属性必定是undefined。实际上,初始化data在初始化computed之前。所以我们可以直接在computed中对data中的属性赋值,因为此时data已经初始化。
computed: {
// 注释下面方法
// ...mapGetters(['user'])
// 不使用辅助函数采用下面方式给data中的basic.id进行赋值
user () {
this.basic.id=this.$store.getters.user.id;
return this.$store.getters.user
}
},
⑥ 计算属性不触发
如下所示视图从store中获取user赋予loginUser,使用的时候发现是undefined,这说明计算属性没有触发。
computed: {
// 以此为data中数据赋值
user(){
this.loginUser=this.$store.getters.user;
console.log(" 计算属性触发 this.loginUser : ", this.loginUser);
return this.$store.getters.user
}
},
这里我们可以打印日志或者在created显式调用一下计算属性:
created() {
this.user;
},
// 或者在使用的地方前打印日志:
console.log(this.user);
【6】mutations和mapMutations
① mutations
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型 (type) 和 一个 回调函数 (handler)
。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
你不能直接调用一个 mutation handler。这个选项更像是事件注册:“当触发一个类型为 increment 的 mutation 时,调用此函数。”要唤醒一个 mutation handler,你需要以相应的 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 的另一种方式是直接使用包含 type 属性的对象:
store.commit({
type: 'increment',
amount: 10
})
当使用对象风格的提交方式,整个对象都作为载荷传给 mutation 函数,因此 handler 保持不变:
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
④ Mutation 需遵守 Vue 的响应规则
既然 Vuex 的 store 中的状态是响应式的,那么当我们变更状态时,监视状态的 Vue 组件也会自动更新。这也意味着 Vuex 中的 mutation 也需要与使用 Vue 一样遵守一些注意事项:
1.最好提前在你的 store 中初始化好所有所需属性。
2.当需要在对象上添加新属性时,你应该
-
使用 Vue.set(obj, ‘newProp’, 123), 或者
-
以新对象替换老对象。例如,利用对象展开运算符 我们可以这样写:
state.obj = { ...state.obj, newProp: 123 }
使用常量替代 Mutation 事件类型
使用常量替代 mutation 事件类型在各种 Flux 实现中是很常见的模式。这样可以使 linter 之类的工具发挥作用,同时把这些常量放在单独的文件中可以让你的代码和作者对整个 app 包含的 mutation 一目了然:
// 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
}
}
})
用不用常量取决于你——在需要多人协作的大型项目中,这会很有帮助。但如果你不喜欢,你完全可以不这样做。
Mutation 必须是同步函数
一条重要的原则就是要记住 mutation 必须是同步函数。为什么?请参考下面的例子:
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
现在想象,我们正在 debug 一个 app 并且观察 devtool 中的 mutation 日志。每一条 mutation 被记录,devtools 都需要捕捉到前一状态和后一状态的快照。然而,在上面的例子中 mutation 中的异步函数中的回调让这不可能完成:因为当 mutation 触发的时候,回调函数还没有被调用,devtools 不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
⑤ 在组件中提交 Mutation
你可以在组件中使用 this.$store.commit('xxx')
提交 mutation,或者使用 mapMutations
辅助函数将组件中的 methods
映射为 store.commit
调用(需要在根节点注入 store)。
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')`
})
}
}