Vuex
Vuex是什么
Vuex是一个专为Vue.js应用程序开发的状态管理模式。
什么是状态管理模式
new Vue({
// state
data () {
return {
count: 0
}
},
// view
template: `
<div>{{ count }}</div>
`,
// actions
methods: {
increment () {
this.count++
}
}
})
这个状态自管理应用包含以下几个部分:
state,驱动应用的数据源;
view,以声明方式将state映射到视图;
actions,响应在view上的用户输入导致的状态变化。
但是,当我们的应用遇到多个组件共享状态时,单向数据流的简洁性很容易被破坏:
多个视图依赖于同一状态。
来自不同视图的行为需要变更同一状态。
对于问题一,传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。对于问题二,我们经常会采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝。以上的这些模式非常脆弱,通常会导致无法维护的代码。
因此,我们为什么不把组件的共享状态抽取出来,以一个全局单例模式管理呢?在这种模式下,我们的组件树构成了一个巨大的“视图”,不管在树的哪个位置,任何组件都能获取状态或者触发行为!
开始
每一个Vuex应用得核心就是store(仓库)。
模块化开发中调用方式和vue-router应该是差不多的。
store保存着应用中大部分的状态,但是它和全局变量有两个不同:
1.Vuex的状态存储是响应式的。store里的状态发生变化,则vue组件内部的状态也会发生变化。
2.不能直接改变store中的状态。唯一改变store中的状态途径就是显式地提交(commit)mutations。这样使得我们可以方便得跟踪每个状态的变化。
最简单的store
安装了Vuex后,让我们来创建一个store。创建过程直截了当,仅需要提供一个初始对象和一些mutations。
// 如果在模块化构建系统中,请确保在开头调用了Vue.use(Vuex)
const store = new Vuex.store({
state: {
count: 0
},
mutations: {
increment(state) {
state.count++
}
}
})
store.commit(‘increment’)
console.log(store.state.count),
不直接改变store.state.count,是因为想要更明确地追踪到状态的变化。
由于store中的状态是响应式的,在组件中调用store中的状态简单到仅需要在计算属性中返回即可。触发变化也只需要在methods中提交mutations。
计数实例:
<div id="app">
<p>{{ count }}</p>
<p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</p>
</div>
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment: state => state.count++,
decrement: state => state.count--
}
})
const app = new Vue({
el: '#app',
computed: {
count () {
return store.state.count
}
},
methods: {
increment () {
store.commit('increment')
},
decrement () {
store.commit('decrement')
}
}
})
核心概念
State
单一状态树
Vuewx使用单一状态树,用一个对象就包含了全部的应用层级状态。至此它便作为一个【唯一数据源SSOT】而存在。
这也就意味着,每个应用将仅仅包含一个store实例。
单一状态树让我们能够直接定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
在Vue组件中获得状态
从store实例中读取状态最简单的方法就是计算属性中返回某个状态:
// 创建一个 Counter 组件
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}
}
这样,每当store.state.count变化的时候,都会重新求取计算属性,并且触发更新相关联的DOM。
在模块化的构建系统中,在每个需要使用state的组建中需要频繁的导入,并且在测试组件时需要模拟状态。
Vuex通过store选项,提供了一种机制将状态从根组件注入到每个子组件中(需调用Vue.vue(Vuex)):
const app = new Vue({
el: '#app',
// 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`
})
子组件可以通过this.$store来访问这个store实例。
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}
}
mapState辅助函数
当一个组件需要获取多个状态时,一一将这些状态通过计算属性导出会有些重复,vuex提供了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
}
})
}
对象展开运算符
mapState函数返回的是个对象,如何将其当做局部计算属性混合使用呢?
...mapState({
// 这里存放从$store调用过来的state
}),
local() {
// 这里用的是组件的局部计算属性
}
computed: {
localComputed () { /* ... */ },
// 使用对象展开运算符将此对象混入到外部对象中
...mapState({
// ...
})
}
组件仍然保有局部状态
使用Vuex并不意味这你需要将所有的状态放入Vuex。虽然讲所有的状态放到Vuex会使状态变化更显示和易调试,但也会使代码变得不直观。
如果有些状态严格属于某个组件,最好还是作为组件的局部状态。
模块化入口main.js
import Vue from 'vue'
import store from './store'
new Vue({
store,
})
其中./store是一个文件夹,index.js入口文件
import Vue from 'vue'
import Vuex from 'vuex'
import * as actions from './actions'
import * as getters from './getters'
import cart from './modules/cart'
import products from './modules/products'
Vue.use(Vuex)
export default new Vuex.Store({
actions,
getters,
modules: {
cart,
products
},
strict: debug,
plugns:
})
单个组件调用$store时不需要import或use
Getters
有时候我们需要从store中的state中派生出一些状态,例如对列表进行过滤并计数:
computed: {
doneTodosCount() {
return this.$store.todos.filter(todo => todo.done).length
}
}
如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它。都不是很理想。
Vuex允许我们在store中定义【getters】(可以认为是store的计算属性)。
Getters接受state作为其第一个参数:
const store = new Vuex.Store({
state: {
todos: [
{id: 1, text: '...', done: true}
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}
})
Getters会暴露为store.getters对象:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getters也可以接受其他getters作为第二个参数:
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
store.getters.doneTodosCount // -> 1
在单独组件中可如此调用:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}
}
mapGetters辅助函数
mapGetters副主函数仅仅是将store中的getters映射到局部计算属性:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用对象展开运算符将 getters 混入 computed 对象中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}
}
如果你想将一个 getter 属性另取一个名字,使用对象形式:
mapGetters({
// 映射 this.doneCount 为 store.getters.doneTodosCount
doneCount: 'doneTodosCount'
})
Mutations(同步)
更改Vuex的store中的唯一方法是提交mutations。Vuex中的mutations非常类似于事件:每个mutation都有一个字符串的事件类型(type)和一个回调函数(handler)。
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
触发得用store.commit('increment')
提交载荷
提交载荷这个我感觉很重要,举个例子。
组件A和B,A和B不是父子组件,但是A和B存在数据交流,比如A传送给B。
这时候我们因为用了Vuex,就可以通过A组件,将要传送的数据通过提交载荷提交到store里,在B组件里直接调用。
// ...
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
})
对象风格的提交方式
可以直接commit一个对象,如下:
store.commit({
type: 'increment',
amount: 10
})
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
Mutations需遵循Vue的响应规则
既然Vuex的store中的状态时响应式的,那么当我们变更状态时,监视状态的Vue组件也会自动更新。
这也意味着Vuex中的mutations需要遵守一些事项:
1.最好提前在store中初始化好所有所需属性
2.当需要在对象上添加属性时,应该使用
Vue.set(obj, 'newProp', 123)
或者
state.obj = { ...state.obj, newProp: 123 }
以新对象替换老对象
使用常量替代Mutations 事件类型
// 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
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment' // 映射 this.increment() 为 this.$store.commit('increment')
]),
...mapMutations({
add: 'increment' // 映射 this.add() 为 this.$store.commit('increment')
})
}
}
Actions(可异步操作)
Action类似于mutation,不同在于:
1.Action提交的是mutation,而不是直接变更状态。
2.Action可以包含任意异步操作。
看一个实例:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}
})
Action函数接受一个与store实例具有相同方法和属性的context对象,但是这个context对象不是store实例本身。
这个context对象可以通过调用commit方法提交一个mutation,或者通过state和getters属性获取state和getters。
分发Action
Action通过store.dispatch方法触发:
store.dispatch(' increment')
可以在action的内部执行异步操作:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
action同样支持载荷和对象方式
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式发布
store.dispatch({
type: 'incrementAsync',
amount: 10
})
在组件中分发Action
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment' // 映射 this.increment() 为 this.$store.dispatch('increment')
]),
...mapActions({
add: 'increment' // 映射 this.add() 为 this.$store.dispatch('increment')
})
}
}
组合Actions
Actions是异步的,而且它内部支持Promise函数。
actions: {
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit('someMutation')
resolve()
}, 1000)
})
}
}
现在你可以:
store.dispatch('actionA').then(() => {
// ...
})
在另外一个 action 中也可以:
actions: {
// ...
actionB ({ dispatch, commit }) {
return dispatch('actionA').then(() => {
commit('someOtherMutation')
})
}
}
最后,如果我们利用async/await这个JS即将到来的新特性,我们可以这样组合action:
actions: {
async actionA({ commit }) {
commit('gotData', await getData())
},
async actionB( { dispatch, commit} ) {
await dispatch('actionA')
commit('gotOtherData', await getOtherData())
}
}
一个store.dispatch在不同模块中可以出发多个action函数。在这种情况下,只有当所有出发函数完成后,返回的Promise才会执行。
Modules
使用单一状态树,导致应用得所有状态集中到一个很大的对象。但是,当应用变得很大时,store对象会变得臃肿不堪。
Vuex允许我们将store分割成模块。每个模块有自己的state, mutation等。
const moduleA = {
state: { ... },
mutations: { ... },
actions: { ... },
getters: { ... }
}
const moduleB = {
state: { ... },
mutations: { ... },
actions: { ... }
}
const store = new Vuex.Store({
modules: {
a: moduleA,
b: moduleB
}
})
store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
模块的局部状态
对于模块内部的mutation和getter,接受的第一个参数时模块的局部状态。
const moduleA = {
state: { count:0 },
mutations: {
increment(state) {
state.count++
}
}
}
同样,对于模块内部的 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
}
}
}
项目结构
需要遵守的规则:
1.应用层级的状态应该集中到单个store对象中。
2.提交mutation是更改状态的唯一方法,并且这个过程是同步的。
3.异步逻辑都应该封装到action里。
├── 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作为唯一参数:
const myPlugin = store => {
// 当store初始化后调用
store.subscribe((mutation, state) => {
// 每次mutation之后调用
// muation的格式为 { type, payload }
})
}
// 然后像这样使用:
const store = new Vuex.Store({
// ...
plugins: [myPlugin]
})
在插件内提交Mutation
在插件中也不允许直接更改状态,只能通过提交mutation来触发变化。
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]
})
生成State快照
有时候插件需要获得状态的快照,比较改变的前后状态,想要实现这项功能,只需要对状态对象进行深拷贝。
const myPluginWithSnapshot = store => {
let prevState = _.cloneDeep(store.state)
store.subscribe((mutation, state) => {
let nextState = _.cloneDeep(state)
// 比较 prevState 和 nextState...
// 保存状态,用于下一次 mutation
prevState = nextState
})
}
严格模式
开启严格模式,仅需要在创建store的时候传入strict:true:
const store = new Vuex.Store({
//...
strict: true
})
在严格模式下,无论何时发生了状态变更且不是由mutation函数引起的,都会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
开发环境与发布环境
不要再发布环境下启用严格模式。
const store = new Vuex.Store({
// ...
strict: process.env.NODE_ENV !== 'production'
})
表单处理
当在严格模式中使用Vuex时,在属于Vuex的state上使用v-model会比较棘手:
<input v-model="obj.message">
假设这里的obj是在计算属性中返回的一个属于Vuex store的对象,在用户输入时,v-model会试图直接修改obj这个对象,但是因为不是通过提交mutation函数执行的,这里会抛出一个错误。
用Vuex的思维去解决这个问题的方法是:给中绑定value,然后侦听input或者change事件,在事件回调中调用action:
<input :value="message" @input="updateMessage">
// ...
computed: {
...mapState({
message: state => state.obj.message
})
},
methods: {
updateMessage (e) {
this.$store.commit('updateMessage', e.target.value)
}
}
双向绑定的计算属性
另一个方法:
<input v-model="message">
// ...
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}