Vuex状态管理
Vuex是专门为Vue应用程序提供的状态管理模式,每个Vuex应用的核心是store
(仓库),即装载应用程序state
(状态)的容器,每个应用通常只拥有一个store
实例。
Vuex的state
是响应式的,即store
中的state
发生变化时,相应组件也会进行更新,修改store
当中state
的唯一途径是提交mutations
。
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}
})
store.commit("increment") // 通过store.state来获取状态对象
console.log(store.state.count) // 通过store.commit()改变状态
State
从store
当中获取state
的最简单办法是在计算属性中返回指定的state
,每当state
发生改变的时候都会重新执行计算属性,并且更新关联的DOM。
const Counter = {
template: `{{ count }}`,
computed: {
count () {
return store.state.count
}
}
}
Vuex提供store
选项,将state
从根组件注入到每个子组件中,从而避免频繁import store
。
// 父组件中注册store属性
const app = new Vue({
el: "#app",
store: store,
components: { Counter },
template: `
`
})
// 子组件,store会注入到子组件,子组件可通过this.$store进行访问
const Counter = {
template: `{{ count }}`,
computed: {
count () {
return this.$store.state.count
}
}
}
Vuex提供mapState()
辅助函数,避免使用多个state
的场景下,多次去声明计算属性。
// 在单独构建的版本中辅助函数为 Vuex.mapState
import { mapState } from "vuex"
export default {
computed: mapState({
count: state => state.count,
// 传递字符串参数"count"等同于`state => state.count`
countAlias: "count",
countPlusLocalState (state) {
return state.count + this.localCount
}
})
}
// 当计算属性名称与state子节点名称相同时,可以向mapState传递一个字符串数组
computed: mapState([
"count" // 映射this.count到store.state.count
])
mapState()
函数返回一个包含有state
相关计算属性的对象,这里可以通过ES6的对象展开运算符...
将该对象与Vue组件本身的computed
属性进行合并。
computed: {
localComputed () {},
...mapState({})
}
Vuex允许在store
中定义getters
(可视为store的计算属性),getters
的返回值会根据其依赖被缓存,只有当依赖值发生了改变才会被重新计算。该方法接收state
作为第1个参数,其它getters
作为第2个参数。可以直接在store
上调用getters
来获取指定的计算值。
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: "...", done: true },
{ id: 2, text: "...", done: false }
]
},
getters: {
doneTodos: (state, getters) => {
return state.todos.filter(todo => todo.done)
}
}
})
// 获取doneTodos = [{ id: 1, text: "...", done: true }]
store.getters.doneTodos
这样就可以方便的根据store
中现有的state
派生出新的state
,从而避免在多个组件中复用时造成代码冗余。
computed: {
doneTodosCount () {
// 现在可以方便的在Vue组件使用store中定义的doneTodos
return this.$store.getters.doneTodos
}
}
Vuex提供的mapGetters()
辅助函数将store
中的getters
映射到局部计算属性。
import { mapGetters } from "vuex"
export default {
computed: {
// 使用对象展开运算符将getters混入computed计算属性
...mapGetters([
"doneTodosCount",
// 映射store.getters.doneTodosCount到别名this.doneCount
doneCount: "doneTodosCount"
])
}
}
Mutations
修改store中的state的唯一方法是提交mutation([mjuː"teɪʃ(ə)n] n.变化),mutations类似于自定义事件,拥有一个字符串事件类型和一个回调函数(接收state作为参数,是对state进行修改的位置)。
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
// 触发类型为increment的mutation时被调用
increment (state) {
state.count++ // 变更状态
}
}
})
// 触发mutation
store.commit("increment")
可以通过store的commit()
方法触发指定的mutations,也可以通过store.commit()
向mutation传递参数。
// commit()
store.commit({
type: "increment",
amount: 10
})
// store
mutations: {
increment (state, payload) {
state.count += payload.amount
}
}
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: {
// 可以通过ES6的计算属性命名特性去使用常量作为函数名
[SOME_MUTATION] (state) {
// mutate state
}
}
})
mutation()
必须是同步函数,因为devtool无法追踪回调函数中对state
进行的异步修改。
Vue组件可以使用this.$store.commit("xxx")
提交mutation,或者使用mapMutations()
将Vue组件中的methods
映射为store.commit
调用(需要在根节点注入store
)。
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,且Action中可以包含异步操作。Action函数接受一个与store实例具有相同方法和属性的context
对象,因此可以通过调用context.commit
提交一个mutation,或者通过context.state
和context.getters
来获取state、getters。
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit("increment")
}
}
})
生产环境下,可以通过ES6的解构参数来简化代码。
actions: {
// 直接向action传递commit方法
increment ({ commit }) {
commit("increment")
}
}
Action通过store.dispatch()
方法进行分发,mutation当中只能进行同步操作,而action内部可以进行异步的操作。下面是一个购物车的例子,代码中分发了多个mutations,并进行了异步API操作。
actions: {
checkout ({ commit, state }, products) {
const savedCartItems = [...state.cart.added] // 把当前购物车的物品备份起来
commit(types.CHECKOUT_REQUEST) // 发出结账请求,然后清空购物车
// 购物Promise分别接收成功和失败的回调
shop.buyProducts(
products,
() => commit(types.CHECKOUT_SUCCESS), // 成功操作
() => commit(types.CHECKOUT_FAILURE, savedCartItems) // 失败操作
)
}
}
组件中可以使用this.$store.dispatch("xxx")
分发action,或者使用mapActions()
将组件的methods
映射为store.dispatch
(需要在根节点注入store
)。
import { mapActions } from "vuex"
export default {
methods: {
...mapActions([
"increment" // 映射this.increment()为this.$store.dispatch("increment")
]),
...mapActions({
add: "increment" // 映射this.add()为this.$store.dispatch("increment")
})
}
}
store.dispatch
可以处理action
回调函数当中返回的Promise
,并且store.dispatch
本身仍然返回一个Promise
。
actions: {
// 定义一个返回Promise对象的actionA
actionA ({ commit }) {
return new Promise((resolve, reject) => {
setTimeout(() => {
commit("someMutation") // 触发mutation
resolve()
}, 1000)
})
},
// 也可以在actionB中分发actionA
actionB ({ dispatch, commit }) {
return dispatch("actionA").then(() => {
commit("someOtherMutation") // 触发另外一个mutation
})
}
}
// 现在可以分发actionA
store.dispatch("actionA").then(() => {
... ... ...
})
可以体验通过ES7的异步处理特性async
/await
来组合action。
actions: {
async actionA ({ commit }) {
commit("gotData", await getData())
},
async actionB ({ dispatch, commit }) {
await dispatch("actionA") //等待actionA完成
commit("gotOtherData", await getOtherData())
}
}
Module
整个应用使用单一状态树的情况下,所有state都会集中到一个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的状态
module内部的mutations()
和getters()
接收的第1个参数是模块的局部状态对象。
const moduleA = {
state: { count: 0 },
mutations: {
increment (state) {
state.count++ // 这里的state是模块的局部状态
}
},
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")
}
}
}
}
模块内部的getters()
方法,可以通过其第3个参数接收到全局状态。
const moduleA = {
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}
}
严格模式
严格模式下,如果state变化不是由mutation()
函数引起,将会抛出错误。只需要在创建store
的时候传入strict: true
即可开启严格模式。
const store = new Vuex.Store({
strict: true
})
不要在生产环境下启用严格模式,因为它会深度检测不合法的state变化,从而造成不必要的性能损失,我们可以通过在构建工具中增加如下判断避免这种情况。
const store = new Vuex.Store({
strict: process.env.NODE_ENV !== "production"
})
严格模式下,在属于Vuex的state上使用v-model指令会抛出错误,此时需要手动绑定value并监听input、change事件,并在事件回调中手动提交action。另外一种实现方式是直接重写计算属性的get和set方法。
总结
- 应用层级的状态应该集中到单个store对象中。
- 提交
mutation
是更改状态的唯一方法,并且这个过程是同步的。 - 异步逻辑都应该封装到
action
里面。
Webpack Vue Loader
vue-loader是由Vue开源社区提供的Webpack加载器,用来将.vue
后缀的单文件组件转换为JavaScript模块,每个.vue
单文件组件可以包括以下部分:
- 一个
。
- 一个
。
- 多个
。
只能有1个
CSS作用域
向.vue
单文件组件的标签上添加
scoped
属性,可以让该标签中的样式只作用于当前组件。使用scoped时,样式选择器尽量使用class或者id,以提升页面渲染性能。
Hank
Hank
可以在一个组件中同时使用带
scoped
属性和不带该属性的,分别用来定义组件私有样式和全局样式。
CSS模块化
在单文件组件.vue
的标签上添加
module
属性即可打开CSS模块化特性。CSS Modules用于模块化组合CSS,vue-loader已经集成了CSS模块化特性。
CSS模块会向Vue组件中注入名为$style
计算属性,从而实现在组件的中使用动态的
class
属性进行绑定。
This should be red
动画
Vue在插入、更新、移除DOM的时候,提供了如下几种方式去展现进入(entering)和离开(leaving)的过渡效果。
- 在CSS过渡和动画中应用class。
- 钩子过渡函数中直接操作DOM。
- 使用CSS、JavaScript动画库,如Animate.css、Velocity.js。
transition组件
Vue提供了内置组件
来为HTML元素、Vue组件添加过渡动画效果,可以在条件展示(使用v-if
或v-show
)、动态组件、展示组件根节点的情况下进行渲染。
主要用来处理单个节点,或者同时渲染多个节点当中的一个。
自动切换的class类名
在组件或HTML进入(entering)和离开(leaving)的过渡效果当中,Vue将会自动切换并应用下图中的六种class类名。
可以使用
的name
属性来自动生成过渡class类名,例如下面例子中的name: "fade"
将自动拓展为.fade-enter
,.fade-enter-active
等,name
属性缺省的情况下默认类名为v
。
hello
自定义CSS类名
结合Animate.css
使用时,可以在
当中通过以下属性自定义class类名。
自定义JavaScript钩子
结合Velocity.js
使用时,通过v-on在属性中设置钩子函数。
显式设置过渡持续时间
可以使用
上的duration属性
设置一个以毫秒为单位的显式过渡持续时间。
Hank
Hank
组件首次渲染时的过渡
通过
上的appear属性
设置组件节点首次被渲染时的过渡动画。
HTML元素的过渡效果
Vue组件的key属性
key属性主要用在Vue虚拟DOM算法中去区分新旧VNodes,不显式使用key
的时候,Vue会使用性能最优的自动比较算法。显式的使用key
,则会基于key
的变化重新排列元素顺序,并移除不存在key
的元素。具有相同父元素的子元素必须有独特的key
,因为重复的key
会造成渲染错误。
- ...
元素的的交替过渡
可以通过Vue提供的v-if
和v-else
属性来实现多组件的交替过渡,最常见的过渡效果是一个列表以及描述列表为空时的消息。
Sorry, no items found.
Vue中具有相同名称的元素切换时,需要通过关键字key
作为标记进行区分,否则Vue出于效率的考虑只会替换相同标签内的内容,因此为
组件中的同名元素设置key
是一个最佳实践。
一些场景中,可以通过给相同HTML元素的key
属性设置不同的状态来代替冗长的v-if
和v-else
。
而对于使用了多个v-if
的多元素过渡,也可以通过动态的key
属性进行大幅度的简化。
Vue组件的过渡效果
多个Vue组件之间的过渡不需要使用key
属性,只需要使用动态组件即可。
实现的列表过渡效果在添加、移除某个HTML元素时,相临的其它HTML元素会瞬间移动至新位置,这个过程并非平滑的过渡。为解决这个问题,
提供v-move特性去覆盖移动过渡期间所使用的CSS类名。开启该特性,即可以通过name
属性手动设置(下面例子中的name="flip-list"
与.flip-list-move
),也可以直接使用move-class
属性。
{{ item }}