Vuex 状态管理
组件内装填管理流程
状态管理
通过状态集中管理和分发,解决多个组件共享状态的问题;
- state:驱动应用的数据源;
- view:以声明方式将 state 映射到视图;
- actions:响应在 view 上的用户输入导致的状态变化;
组件间通信方式
- 父组件给子组件传值(props)
- 子组件给父组件传值($emit)
- 不相关组件之间传值(eventbus)
- 其他常见方式(parent/refs)
父组件给子组件传值
- 子组件中通过 props 接收数据;
- 父组件中给子组件通过响应属性传值;
child.vue
<template>
<div>
<h2>child: {{ title }}</h2>
</div>
</template>
<script>
export default {
props: {
title: String,
},
};
</script>
parent.vue
<template>
<div>
<h1>parent: props down</h1>
<child title="01-props-down"></child>
</div>
</template>
<script>
import child from "./01-child.vue";
export default {
components: {
child,
},
};
</script>
子组件给父组件传值
传值核心:自定义事件的方式
通过子组件触发事件的方式,触发事件的时候携带参数,父组件注册子组件内部触发的事件,并接收其传递的数据,完成子向父的传值;
child.vue
<template>
<div>
<h1 :style="{ fontSize: fontSize + 'em' }">child: Event up</h1>
<button @click="handler">文字增大</button>
</div>
</template>
<script>
export default {
props: {
fontSize: Number,
},
methods: {
handler() {
this.$emit("enlargeText", 0.1);
},
},
};
</script>
parent.vue
<template>
<div>
<h1 :style="{ fontSize: hFontSize + 'em' }">parent: Event up</h1>
这里的文字不需要变化
<child :fontSize="hFontSize" @enlargeText="handleEnlargeText"></child>
<child :fontSize="hFontSize" @enlargeText="handleEnlargeText"></child>
<child :fontSize="hFontSize" @enlargeText="hFontSize += $event"></child>
</div>
</template>
<script>
import child from "./02-child";
export default {
components: {
child,
},
data() {
return {
hFontSize: 1,
};
},
methods: {
handleEnlargeText(size) {
this.hFontSize += size;
},
},
};
</script>
不相关组件之间传值
传值核心:自定义事件的方式
因为组件不相关(不存在父子关系),因此无法通过子组件触发事件进行传值,需要使用 eventbus(公共 vue 实例,该实例的作用是作为事件总线或者事件中心)
eventbus.js
import Vue from "vue";
export default new Vue();
sibling-01.js
<template>
<div>
<h1>Sibling01: Event Bus</h1>
<div class="number" @click="sub">-</div>
<input type="text" style="width: 30px; text-align: enter" :value="value" />
<div class="number" @click="add">+</div>
</div>
</template>
<script>
import bus from "./eventbus";
export default {
props: {
num: Number,
},
created() {
this.value = this.num;
},
data() {
return {
value: -1,
};
},
methods: {
sub() {
if (this.value > 1) {
this.value--;
bus.$emit("numchange", this.value);
}
},
add() {
this.value++;
bus.$emit("numchange", this.value);
},
},
};
</script>
sibling-02.js
<template>
<div>
<h1>Sibling02: Event Bus</h1>
<div>{{ msg }}</div>
</div>
</template>
<script>
import bus from "./eventbus";
export default {
data() {
return {
msg: "",
};
},
created() {
bus.$on("numchange", (value) => {
this.msg = `你选择了${value}商品`;
});
},
};
</script>
通过 $refs 获取子组件
ref 两个作用
- 在普通 HTML 标签上使用 ref,获取到的是 DOM;
- 在组件标签上使用 ref,获取到的是组件实例;
child.vue
<template>
<div>
<h1>child: ref</h1>
<input ref="input" type="text" v-model="value" />
</div>
</template>
<script>
export default {
data() {
return {
value: "",
};
},
methods: {
focus() {
this.$refs.input.focus();
},
},
};
</script>
parent.vue
<template>
<div>
<h1>parent: ref</h1>
<child ref="c"></child>
</div>
</template>
<script>
import child from "./04-child";
export default {
components: {
child,
},
data() {
return {
value: "",
};
},
mounted() {
this.$refs.c.focus();
this.$refs.c.value = "hello input";
},
};
</script>
简易的状态管理方案
通过以上方式可能会遇到的问题
- 多个视图依赖同一状态;
- 来自不同视图的行为需要变更同一状态;
Vuex 概念
什么是 Vuex
- Vuex 是专门为 Vue.js 设计的状态管理库;
- Vuex 采用集中式的方式存储需要共享的状态;
- Vuex 的作用是进行状态管理,解决复杂组件通信,数据共享的问题;
- Vuex 集成到了 devtools 中,提供 time-travel 时光旅行、历史回滚功能;
什么时候使用 Vuex
- 非必要的情况不使用 Vuex;
- 大型的单页应用程序:
- 多视图依赖于同一状态;
- 来自不同视图的行为需要变更同一状态;
Vuex 核心概念
- Store:仓库,它是使用 Vuex 应用程序的核心,每个应用仅有一个 store。它是一个容器,包含着应用的大部分状态;
- State:状态,保存在 store 中,称为单一状态树,是响应式的;
- Getter:Vuex 中的计算属性,方便你从一个属性派生出其他值。内部可以对计算结果进行缓存,只有当依赖的状态发生改变以后才会重新计算;
- Mutation:状态的变化,必须通过提交 Mutation 来完成,Mutation 必须是同步执行的;
- Action:和 Mutation 类似,不同的是 Action 中可以进行异步操作,最后要改变状态还是通过提交 Mutation 进行;
- Module:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象中,当应用变的非常复杂时,Store 对象会变的非常臃肿,而是用 Module 可以将 Store 分割成模块,每个模块可以有自己的 State、Getter、Mutation、Action、子 Module 从而解决臃肿问题;
Vuex 基本结构
State
store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
count: 0,
msg: "Vuex study",
},
getters: {},
mutations: {},
actions: {},
modules: {},
});
06-index.vue
<template>
<div>
<h1>Vuex - State</h1>
<h2>1. $store.state</h2>
count: {{ $store.state.count }}
<br />
msg: {{ $store.state.msg }}
<h2>2. mapState 数组方式</h2>
count: {{ count }}
<br />
msg: {{ msg }}
<h2>3. mapState 对象方式</h2>
count: {{ num }}
<br />
msg: {{ message }}
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
computed: {
// 1. $store.state.count
// count: state => state.count
// 2. ...mapState(["count", "msg"]),
// 3.
...mapState({
num: "count",
message: "msg",
}),
},
};
</script>
Getter
store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
count: 0,
msg: "Vuex study",
},
getters: {
reverseMsg(state) {
return state.msg.split("").reverse().join("");
},
},
mutations: {},
actions: {},
modules: {},
});
07-index.vue
<template>
<div>
<h1>Vuex - Getters</h1>
<h2>1. $store.getters</h2>
reverseMsg: {{ $store.getters.reverseMsg }}
<h2>2. mapgetters 数组方式</h2>
reverseMsg: {{ reverseMsg }}
<h2>3. mapgetters 对象方式</h2>
reverseMsg: {{ mpgMsg }}
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
computed: {
// ...mapGetters(["reverseMsg"]),
...mapGetters({
mpgMsg: "reverseMsg",
}),
},
};
</script>
Mutation
store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
count: 0,
msg: "Vuex study",
},
getters: {
reverseMsg(state) {
return state.msg.split("").reverse().join("");
},
},
mutations: {
increate(state, payload) {
state.count += payload;
},
},
actions: {},
modules: {},
});
08-index.vue
<template>
<div>
<h1>Vuex - Mutainots</h1>
<h2>count: {{ count }}</h2>
<h2>1. $store.commit('name', payload)</h2>
<button @click="$store.commit('increate', 2)">Mutatinos</button>
<h2>2. mapMutations 数组方式</h2>
<button @click="increate(3)">Mutatinos</button>
<h2>3. mapMutations 对象方式</h2>
<button @click="mpmIncreate(5)">Mutatinos</button>
</div>
</template>
<script>
import { mapMutations, mapState } from "vuex";
export default {
computed: {
...mapState(["count"]),
},
methods: {
...mapMutations(["increate"]),
...mapMutations({
mpmIncreate: "increate",
}),
},
};
</script>
Action
store.js
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
count: 0,
msg: "Vuex study",
},
getters: {
reverseMsg(state) {
return state.msg.split("").reverse().join("");
},
},
mutations: {
increate(state, payload) {
state.count += payload;
},
},
actions: {
increateAsync(context, payload) {
setTimeout(() => {
context.commit("increate", payload);
}, 2000);
},
},
modules: {},
});
09-index.vue
<template>
<div>
<h1>Vuex - Actions</h1>
<h2>count: {{ count }}</h2>
<h2>1. $store.dispatch('name', payload)</h2>
<button @click="$store.dispatch('increateAsync', 2)">Mutatinos</button>
<h2>2. mapActions 数组方式</h2>
<button @click="increateAsync(3)">Mutatinos</button>
<h2>3. mapActions 对象方式</h2>
<button @click="mpaIncreateAsync(5)">Mutatinos</button>
</div>
</template>
<script>
import { mapActions, mapState } from "vuex";
export default {
computed: {
...mapState(["count"]),
},
methods: {
...mapActions(["increateAsync"]),
...mapActions({
mpaIncreateAsync: "increateAsync",
}),
},
};
</script>
Module
products.js
export default {
namespaced: true,
state: {
phone: {
version: "1.0",
price: "20000.00",
num: 1000,
},
},
getters: {
getPhoneNum(state) {
return state.phone.num;
},
},
mutations: {
decreasePhoneNum(state, payload) {
state.phone.num -= payload;
},
},
actions: {
delayDecreasePhoneNum(context, payload) {
setTimeout(() => {
context.commit("decreasePhoneNum", payload);
}, 1000);
},
},
};
store.js
import Vue from "vue";
import Vuex from "vuex";
import products from "./modules/products";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
count: 0,
msg: "Vuex study",
},
getters: {
reverseMsg(state) {
return state.msg.split("").reverse().join("");
},
},
mutations: {
increate(state, payload) {
state.count += payload;
},
},
actions: {
increateAsync(context, payload) {
setTimeout(() => {
context.commit("increate", payload);
}, 2000);
},
},
modules: {
products,
cart,
},
});
10-index.vue
<template>
<div>
<h1>Vuex - Modules</h1>
<h3>modules state: {{ phone }}</h3>
<h3>modules getters: {{ getPhoneNum }}</h3>
<button @click="decreasePhoneNum(2)">modules mutations</button>
<button @click="delayDecreasePhoneNum(2)">modules actions</button>
</div>
</template>
<script>
import { mapActions, mapGetters, mapMutations, mapState } from "vuex";
export default {
computed: {
...mapState("products", ["phone"]),
...mapGetters("products", ["getPhoneNum"]),
},
methods: {
...mapMutations("products", ["decreasePhoneNum"]),
...mapActions("products", ["delayDecreasePhoneNum"]),
},
};
</script>
Vuex 严格模式
store.js
import Vue from "vue";
import Vuex from "vuex";
import products from "./modules/products";
import cart from "./modules/cart";
Vue.use(Vuex);
export default new Vuex.Store({
strict: process.env.NODE_ENV !== "production",
state: {
count: 0,
msg: "Vuex study",
},
getters: {
reverseMsg(state) {
return state.msg.split("").reverse().join("");
},
},
mutations: {
increate(state, payload) {
state.count += payload;
},
},
actions: {
increateAsync(context, payload) {
setTimeout(() => {
context.commit("increate", payload);
}, 2000);
},
},
modules: {
products,
cart,
},
});
【注】:不要在生产模式下开启严格模式,严格模式会深度检查状态树,来检查不合规的状态改变,从而影响性能,因此我们可以在开发环境中使用严格模式,在生产环境中关闭严格模式
Vuex 的插件
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 (opens new window)或者是 Browserify 的 envify (opens new window)使 process.env.NODE_ENV !== 'production' 为 false。
内置 Logger 插件
如果正在使用 vue-devtools (opens new window),你可能不需要此插件。
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 !== "aBlocklistedMutation"
},
actionFilter (action, state) {
// 和 `filter` 一样,但是是针对 action 的
// `action` 的格式是 `{ type, payload }`
return action.type !== "aBlocklistedAction"
},
transformer (state) {
// 在开始记录之前转换状态
// 例如,只返回指定的子树
return state.subTree
},
mutationTransformer (mutation) {
// mutation 按照 { type, payload } 格式记录
// 我们可以按任意方式格式化
return mutation.type
},
actionTransformer (action) {
// 和 `mutationTransformer` 一样,但是是针对 action 的
return action.type
},
logActions: true, // 记录 action 日志
logMutations: true, // 记录 mutation 日志
logger: console, // 自定义 console 实现,默认为 `console`
})
日志插件还可以直接通过 <script> 标签引入,它会提供全局方法 createVuexLogger。
要注意,logger 插件会生成状态快照,所以仅在开发环境使用。
模拟简单的 Vuex
- install 方法;
- Store 类:
- state;
- getters;
- mutations;
- actions;
- commit 方法;
- dispatch 方法;
myvuex.js
let _Vue = null;
class Store {
constructor(options) {
const { state = {}, getters = {}, mutations = {}, actions = {} } = options;
this.state = _Vue.observable(state);
this.getters = Object.create(null);
Object.keys(getters).forEach((key) => {
Object.defineProperty(this.getters, key, {
get: () => getters[key](state),
});
});
this._mutations = mutations;
this._actions = actions;
}
commit(type, payload) {
this._mutations[type](this.state, payload);
}
dispatch(type, payload) {
this._actions[type](this, payload);
}
}
function install(Vue) {
_Vue = Vue;
_Vue.mixin({
beforeCreate() {
if (this.$options.store) {
_Vue.prototype.$store = this.$options.store;
}
},
});
}
export default {
Store,
install,
};