前言:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式,但并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发实际需要进行权衡和确定。
运用vueX的场景:
多个视图依赖于同一状态,此时传参的方法对于多层嵌套的组件将会非常繁琐,并且对于兄弟组件间的状态传递无能为力。
来自不同视图的行为需要变更同一状态。此时采用父子组件直接引用或者通过事件来变更和同步状态的多份拷贝,通常会导致无法维护的代码
兼容:
Vuex 依赖 Promise。如果你支持的浏览器并没有实现 Promise (如 IE),那么你可以使用一个 polyfill 的库(如 es6-promis)
1.你可以通过 CDN 将其引入,window.Promise
会自动可用:
<script src="https://cdn.jsdelivr.net/npm/es6-promise@4/dist/es6-promise.auto.js"></script>
2.包管理器安装:
npm install es6-promise --save //yarn add es6-promise
然后,将下列代码添加到你使用 Vuex 之前的一个地方:
import 'es6-promise/auto'
一、安装
npm install vuex --save
二、新建仓库
在src目录下新建一个名为store的文件夹,然后在该文件夹下面创建一个store.js文件
三、在main.js中引入
import store from './store/store'
//每当 store.state.count 变化的时候, 都会重新求取计算属性,并且触
//发更新相关联的 DOM。然而,这种模式会导致组件依赖全局状态单例。所以
//Vuex 通过 store 选项,提供了一种机制将状态从根组件“注入”到每一个
//子组件中(需调用 Vue.use(Vuex)):
Vue.use(Vuex)
...........
new Vue({
el: '#app',
router,
store,//记得需要挂载才可以用,把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的
子组件,且子组件能通过 this.$store
components: { App },
template: '<App/>'
})
四、使用
vuex中有默认的五个核心概念:
1、state:存储状态(变量),vuex中的数据源,我们需要保存的数据就保存在这里,可以在页面通过 this.$store.state来获取我们定义的数据,要记得将每个状态进行初始化。
2、getters:对数据获取之前的再次编译,可以理解为state的计算属性。在组件中通过 this.$sotre.getters.xxx使用,Getter相当于vue中的computed计算属性, getter 的返回值会根据它的依赖被缓存起来, 且只有当它的依赖值发生了改变才会被重新计算,getter 在通过方法访问时,每次都会去进行调用,而不会缓存结果,通过属性访问Getter 会暴露为 store.getters 对象,很容易地在任何组件中使用它 你可以以属性的形式访问这些值:this.$store.getters.xxx;Getters 可以用于监听、 state中的值的变化,返回计算后的结果,即getters中“+”的操作在进入时即会执行。
3、mutations:,在组件中通过this.$store.commit(‘xxx’,params)使用,它是更改 Vuex 的 store 中的状态的唯一方法,会执行状态的同步更新,且一旦状态发生改变监视状态的 Vue 组件也会自动更新,所以我们需要给每个状态进行初始化。方法中第一个参数默认为state,接下来的为自定义参数(即官方文档中说的载荷(payload),多数情况下,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读)。
4、actions:Action 可以包含任意异步操作,但它提交的是 mutation,而且是异步提交,在组件中通过this.$store.dispath(‘xxx’)触发,不能直接变更数据,Action 函数接收一个与 store 实例具有相同方法和属性的 context 对象,所以我们可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters。但是context 对象并不是 store 实例本身(这个和Modules相关 )。
5、modules:store的子模块,为了开发大型项目,方便状态管理而使用的,即将store分割为模块,使store对象不会太臃肿。
store.js
import Vue from 'vue'
import Vuex from 'vuex'
import {
log
} from 'util';
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0,
age: 0
},
getters: {
getterAge(state) {
console.log(state);
return state.age;
}
},
mutations: {
addCount(state, obj) {
return state.count += obj.num;
},
subCount(state, obj) {
return state.count -= obj.num;
}
},
actions: {
addCountAsy(context) {
setTimeout(() => {
context.commit('addCount', {
num: 2
})
}, 1000)
},
subCountAsy(context) {
setTimeout(() => {
context.commit('subCount', {
num: 2
})
}, 1000)
}
}
})
index.vue
<template>
<div>
<h1></h1>
<div class="addBtn" @click="handleAdd()">count++</div>
<div class="addBtn" @click="handleSub()">count--</div>
<div class="addBtn" @click="handleAddAsy()">异步count++(1S后执行)</div>
<div class="addBtn" @click="handleSubAsy()">异步count--(1S后执行)</div>
<h2>computed中通过state获取到的count:{{count}}</h2>
<h2>computed中通过getters获取到计算后的count:{{getCount}}</h2>
</div>
</template>
<script>
export default {
name: "HelloWorld",
computed: {
count() {
return this.$store.state.count;
},
// 辅助函数mapState的用法,可以使我们更加方便的运用vuex,效果都是相同的
/* 等价于:
...mapState([
'count'
]), */
getCount() {
return this.$store.getters.getterCount;
}
/* 等价于:
...mapGetters([
'getterCount'
]) */
},
//监听count的变化
watch: {
count: {
function(val, oldVal) {console.log(val,oldVal)},
deep: true
}
},
data() {
return {
// msg: "Welcome to Your Vue.js App"
};
},
methods: {
// 可以通过点击事件进行同步的增加或减少count的值,且每次加减的值为传入载荷(可以为参数及对象)num的值2,载荷应该是一个对象,这样可以包含多个字段并且记录的 mutation 会更易读
handleAdd() {
this.$store.commit("addCount", {
num: 2
});
console.log(
"state中的count为:" + this.$store.state.count,
"getters中的count为:" + this.$store.getters.getterCount
);
},
handleSub() {
this.$store.commit("subCount", {
num: 2
});
console.log(
"state中的count为:" + this.$store.state.count,
"getters中的count为:" + this.$store.getters.getterCount
);
},
// …mapMutations函数
// 这里这样写并用大括号是为了将函数重新命名,即重新命名addCount和subCount',
// 关于使用辅助函数后载荷的传参,直接在调用的地方进行传参便可
// <div class="addBtn" @click="handleAdd({num:2})">count++</div>
// <div class="addBtn" @click="handleSub({num:2})">count--</div>
/* 等价于:
...mapMutations({
handlerAdd: 'addCount',
handlerSub: 'subCount'
}), */
//分发action,这里看起来好像比直接提交mutation麻烦,但是它却解决了异步的操作问题
// …mapActions函数 ,和…mapMutations用法相似。
/* 等价于
...mapActions({
handlerAddasy: 'addCountasy',
handlerSubasy: 'subCountasy'
}) */
handleAddAsy() {
this.$store.dispatch("addCountAsy");
},
handleSubAsy() {
this.$store.dispatch("subCountAsy");
}
},
mounted() {
console.log(this.$qs);
console.log(this.$axios);
console.log(this);
}
};
</script>
<style>
.addBtn {
width: 200px;
height: 50px;
line-height: 50px;
text-align: center;
font-size: 16px;
background-color: skyblue;
margin: 50px auto;
cursor: pointer;
}
</style>
actions getters mutations也可以分别放在单独的文件夹下面进行定义,然后在store.js引入即可,main.js中的引入和上面的是一样的。
项目结构
store.js
import Vue from 'vue'
import Vuex from 'vuex'
import * as actions from './actions'
import * as getters from './getters'
import * as mutations from './mutations'
Vue.use(Vuex)
export const store = new Vuex.Store({
state:{
count: 0,
age: 0
},
getters,
mutations,
actions
})
拓展:
对象展开运算符
let a = [1,2,3];
let b = [0, ...a, 4]; // [0,1,2,3,4]
let obj = { a: 1, b: 2 };
let obj2 = { ...obj, c: 3 }; // { a:1, b:2, c:3 }
let a = [1,2,3];
let [b, ...c] = a; b; // 1 c; // [2,3]
let object = { a: '01', b: '02' };
let newObject = { c: '03', ...object };
console.log(newObject); //{c: "03", a: "01", b: "02"}
Module(允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中。)
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
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` 对象是模块的局部状态
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
}
}
}
#命名空间
默认情况下,模块内部的 action、mutation 和 getter 是注册在全局命名空间的——这样使得多个模块能够对同一 mutation 或 action 作出响应。
如果希望你的模块具有更高的封装度和复用性,你可以通过添加 namespaced: true
的方式使其成为带命名空间的模块。当模块被注册后,它的所有 getter、action 及 mutation 都会自动根据模块注册的路径调整命名。例如:
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']
}
}
}
}
}
})
启用了命名空间的 getter 和 action 会收到局部化的 getter
,dispatch
和 commit
。换言之,你在使用模块内容(module assets)时不需要在同一模块内额外添加空间名前缀。更改 namespaced
属性后不需要修改模块内的代码。
#在带命名空间的模块内访问全局内容(Global Assets)
如果你希望使用全局 state 和 getter,rootState
和 rootGetters
会作为第三和第四参数传入 getter,也会通过 context
对象的属性传入 action。
若需要在全局命名空间内分发 action 或提交 mutation,将 { root: true }
作为第三参数传给 dispatch
或 commit
即可。
modules: {
foo: {
namespaced: true,
getters: {
// 在这个模块的 getter 中,`getters` 被局部化了
// 你可以使用 getter 的第四个参数来调用 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
someOtherGetter: state => { ... }
},
actions: {
// 在这个模块中, dispatch 和 commit 也被局部化了
// 他们可以接受 `root` 属性以访问根 dispatch 或 commit
someAction ({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'
dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
},
someOtherAction (ctx, payload) { ... }
}
}
}
#在带命名空间的模块注册全局 action
若需要在带命名空间的模块注册全局 action,你可添加 root: true
,并将这个 action 的定义放在函数 handler
中。例如:
{
actions: {
someOtherAction ({dispatch}) {
dispatch('someAction')
}
},
modules: {
foo: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) { ... } // -> 'someAction'
}
}
}
}
}
#带命名空间的绑定函数
当使用 mapState
, mapGetters
, mapActions
和 mapMutations
这些函数来绑定带命名空间的模块时,写起来可能比较繁琐:
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']()
])
}
对于这种情况,你可以将模块的空间名称字符串作为第一个参数传递给上述函数,这样所有绑定都会自动将该模块作为上下文。于是上面的例子可以简化为:
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
创建基于某个命名空间辅助函数。它返回一个对象,对象里有新的绑定在给定命名空间值上的组件绑定辅助函数:
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'
])
}
}
#给插件开发者的注意事项
如果你开发的插件(Plugin)提供了模块并允许用户将其添加到 Vuex store,可能需要考虑模块的空间名称问题。对于这种情况,你可以通过插件的参数对象来允许用户指定空间名称:
// 通过插件的参数对象得到空间名称
// 然后返回 Vuex 插件函数
export function createPlugin (options = {}) {
return function (store) {
// 把空间名字添加到插件模块的类型(type)中去
const namespace = options.namespace || ''
store.dispatch(namespace + 'pluginAction')
}
}
#模块动态注册
在 store 创建之后,你可以使用 store.registerModule
方法注册模块:
// 注册模块 `myModule`
store.registerModule('myModule', {
// ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...
})
之后就可以通过 store.state.myModule
和 store.state.nested.myModule
访问模块的状态。
模块动态注册功能使得其他 Vue 插件可以通过在 store 中附加新模块的方式来使用 Vuex 管理状态。例如,vuex-router-sync
插件就是通过动态注册模块将 vue-router 和 vuex 结合在一起,实现应用的路由状态管理。
你也可以使用 store.unregisterModule(moduleName)
来动态卸载模块。注意,你不能使用此方法卸载静态模块(即创建 store 时声明的模块)。
#保留 state
在注册一个新 module 时,你很有可能想保留过去的 state,例如从一个服务端渲染的应用保留 state。你可以通过 preserveState
选项将其归档:store.registerModule('a', module, { preserveState: true })
。
当你设置 preserveState: true
时,该模块会被注册,action、mutation 和 getter 会被添加到 store 中,但是 state 不会。这里假设 store 的 state 已经包含了这个 module 的 state 并且你不希望将其覆写。
#模块重用
有时我们可能需要创建一个模块的多个实例,例如:
- 创建多个 store,他们公用同一个模块 (例如当
runInNewContext
选项是false
或'once'
时,为了在服务端渲染中避免有状态的单例) - 在一个 store 中多次注册同一个模块
如果我们使用一个纯对象来声明模块的状态,那么这个状态对象会通过引用被共享,导致状态对象被修改时 store 或模块间数据互相污染的问题。
实际上这和 Vue 组件内的 data
是同样的问题。因此解决办法也是相同的——使用一个函数来声明模块状态(仅 2.3.0+ 支持):
const MyReusableModule = {
state () {
return {
foo: 'bar'
}
},
// mutation, action 和 getter 等等...
}
项目结构
Vuex 并不限制你的代码结构。但是,它规定了一些需要遵守的规则:
-
应用层级的状态应该集中到单个 store 对象中。
-
提交 mutation 是更改状态的唯一方法,并且这个过程是同步的。
-
异步逻辑都应该封装到 action 里面。
只要你遵守以上规则,如何组织代码随你便。如果你的 store 文件太大,只需将 action、mutation 和 getter 分割到单独的文件。
对于大型应用,我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:
├── 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 之后调用
// mutation 的格式为 { type, payload }
})
}
然后像这样使用:
const store = new Vuex.Store({
// ...
plugins: [myPlugin]
})
#在插件内提交 Mutation
在插件中不允许直接修改状态——类似于组件,只能通过提交 mutation 来触发变化。
通过提交 mutation,插件可以用来同步数据源到 store。例如,同步 websocket 数据源到 store(下面是个大概例子,实际上 createPlugin
方法可以有更多选项来完成复杂任务):
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
})
}
生成状态快照的插件应该只在开发阶段使用,使用 webpack 或 Browserify,让构建工具帮我们处理:
const store = new Vuex.Store({
// ...
plugins: process.env.NODE_ENV !== 'production'
? [myPluginWithSnapshot]
: []
})
上面插件会默认启用。在发布阶段,你需要使用 webpack 的 DefinePlugin 或者是 Browserify 的 envify 使 process.env.NODE_ENV !== 'production'
为 false
。
#内置 Logger 插件
如果正在使用 vue-devtools,你可能不需要此插件。
Vuex 自带一个日志插件用于一般的调试:
import createLogger from 'vuex/dist/logger'
const store = new Vuex.Store({
plugins: [createLogger()]
})
createLogger
函数有几个配置项:
const logger = createLogger({
collapsed: false, // 自动展开记录的 mutation
filter (mutation, stateBefore, stateAfter) {
// 若 mutation 需要被记录,就让它返回 true 即可
// 顺便,`mutation` 是个 { type, payload } 对象
return mutation.type !== "aBlacklistedMutation"
},
transformer (state) {
// 在开始记录之前转换状态
// 例如,只返回指定的子树
return state.subTree
},
mutationTransformer (mutation) {
// mutation 按照 { type, payload } 格式记录
// 我们可以按任意方式格式化
return mutation.type
},
logger: console, // 自定义 console 实现,默认为 `console`
})
日志插件还可以直接通过 <script>
标签引入,它会提供全局方法 createVuexLogger
。
要注意,logger 插件会生成状态快照,所以仅在开发环境使用。
严格模式
开启严格模式,仅需在创建 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.message
。在严格模式中,由于这个修改不是在 mutation 函数中执行的, 这里会抛出一个错误。
用“Vuex 的思维”去解决这个问题的方法是:给 <input>
中绑定 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)
}
}
下面是 mutation 函数:
// ...
mutations: {
updateMessage (state, message) {
state.obj.message = message
}
}
#双向绑定的计算属性
必须承认,这样做比简单地使用“v-model
+ 局部状态”要啰嗦得多,并且也损失了一些 v-model
中很有用的特性。另一个方法是使用带有 setter 的双向绑定计算属性:
<input v-model="message">
// ...
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}