Vuex
Vuex是一个专门为Vue.js应用所设计的集中式状态管理架构,它借鉴了Flux和Redux的设计思想,但简化了概念,采用了一种为能更好地发挥Vue.js数据相应机制而专门设计的实现。
Vuex是一个专为Vue.js应用开发的状态管理模式,采用集中式存储管理应用组件状态,并以响应规则保证状态以一种可预测的方式发生变化。
# 安装Vuex
$ npm i vuex --S
# 引入
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
状态管理模式
# 计数器
new Vue({
// state 驱动应用的数据源
data () {
return {
count:0
}
},
// view 以声明方式将数据源映射到视图
template:`<span>{{count}}</span>`,
//actions 响应在视图上的用户输入导致的中状态变化
methods:{
increment() {
this.count ++
}
}
})
状态state
的概念,简单来说可视为项目中使用的数据的集合。Vuex使组件本地状态和应用层级状态有了一定的差异。
- 组件本地状态:表示仅仅在组件内部使用的状态,类似于通过配置选项传入Vue组件内部。
- 应用层级状态:表示同时被多个组件共享的状态层级
单向数据流
假如有一个父组件同时包含两个子组件,父组件可以很容易的使用prop
属性向子组件传递数据。两个子组件如何和对象互相通信呢?子组件如何传递数据给父组件呢?项目规模很小时,可通过事件派发和监听来完成父组件和子组件的通信。随着项目规模增长,遇到的问题是:
- 保持对所有事件追踪变得越来越困难,到底哪个事件是哪个组件派发的,哪个组件该监听哪个事件呢?
- 项目逻辑分散在各个组件当中,很容易导致逻辑混乱,不利于项目维护。
- 父组件变得和子组件耦合度越来越严重,因为它需要明确的派发和监听子组件的某些事件。
当应用遇到多个组件共享状态时,单向数据流的间接性很容易被破坏:
- 多个视图依赖于同一个状态
传参的方法对于多层嵌套的组件将会非常繁琐,对于兄弟组件间的状态传递无能为力。 - 来自不同视图的行为需要变更同一状态
采用父子组件直接饮用或通过事件来变更和同步状态的多份拷贝
为什么不把组件的共享状态抽取出来以一个全局单例模式管理呢?在这种模式下,组件树构成一个巨大的视图,不管在树的哪个位置,任何组件都能获取状态或触发行为。
另外,通过定义和隔离状态管理中各种概念并强制遵守一定的规则,代码将会变得更加结构化且易于维护。
核心概念
- 单一状态树
使用一个对象包含全部应用层级状态,它作为唯一数据源存在,这意味着,每个应用将紧紧包含一个仓库store
的实例。单一状态树让我们能够直接地定位任意特定的状态片段,在调用过程中也能轻易地取得整个当前应用状态的快照。 - 获取状态
派生状态getters
用来从仓库store
中获取Vue组件数据 - 状态变更
状态变更mutators
的事件处理器可用来驱动状态的变化 - 异步操作
异步操作actions
可给组件使用的函数,以此用来驱动事件处理器mutations
。
Vuex规定,属于应用层级的状态只能通过状态变更mutation
中的方法来修改,而派发mutation
中的事件只能通过action
。
从左至右,从组件触发,组件中调用action
,在action
层可以和后台数据交互,比如获取初始化数据源,或者中间数据的过滤等。然后在action
中去派发mutation
。mutation
去触发状态的改变,从而触发视图的更新。
注意
- 数据流都是单向的
- 组件能够调用
action
-
action
用来派发mutation
- 只有
mutation
可以改变状态 -
store
是响应式的,无论state
什么时候更新,组件都将同步更新。
开始
每个Vuex应用的核心就是仓库(store),store基本就是一个容器,包含着应用中大部分的状态(state)。Vuex和单纯的全局对象由两点不同:
- Vuex的状态存储时响应式的,当Vue组件从store中读取状态时,若store中的状态发生变化,相应的组件也会得到高效更新。
- 不能直接改变store中的状态,改变store中的状态唯一途径就是显式地提交(commit)mutation。这样使得我们可以方便地跟踪每一个状态的变化。
仓库
Vuex应用状态state
都存放在store
中,Vue组件可从store
中获取状态,可把store
通俗的理解为一个全局变量的仓库。和单纯的全局变量的区别是当store
中的状态发生变化时,相应的Vue组件也会得到更新。
//仓库
const store = new Vuex.Store({
//状态
state:{
count:0
},
//状态变更
mutations:{
increment(state){
state.count++
}
}
})
//获取状态对象
store.state.count
//触发状态变更
store.commit('increment')
状态
单一状态树
Vuex使用单一状态树,用一个对象包含了全部应用层级状态,并作为一个“唯一数据源(SSOT)”而存在。这意味着,每个应用将仅仅包含一个状态实例。单一状态树让我们能够直接地定位任意特定的状态片段,在调试过程中也能轻易地去的整个当前应用状态的快照。
在Vue组件中获得Vuex状态
由于Vuex的状态存储的是响应式的,从store实例中读取状态最简单是方式是在计算属性中返回某个状态。
// 创建组件
const Counter = {
template:`<div>{{count}}</div>`,
computed:{
count () {
// 每当store.state.count变化时会重新求取计算属性并触发更新相关联的DOM
return store.state.count
}
}
}
这种模式导致组件依赖全局状态单例,在模块化的构建系统中,在每个需要使用state的组件中需要频繁地导入,并在测试组件时需模拟状态。
Vuex通过store选项,提供了一种机制将状态从根组件注入到每个子组件中。
const app = new Vue({
el:'#app',
store,//将store对象提供给store选项,把store实例注入到所有的子组件中。
componenets:{Counter},
template:`<div class="app"><counter></counter></div>`
})
通过在根实例中注册store选项,该store实例会注入到根组件下的所有子组件中,且子组件能通过this.$store访问到。
const Counter = {
template:`<div>{{count}}</div>`,
computed:{
count () {
return this.$store.state.count
}
}
}
当一个组件需要获取多个状态时,将这些状态都声明为计算属性会有些重复和冗余,为解决这个问题,使用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传入一个字符串数组。
// 映射this.count为store.state.count
computed:mapState(['count'])
mapState函数返回的是一个对象,如何将其与局部计算属性混合使用呢?通常需要使用一个工具函数将多个对象合并为一个,以便于将最终对象传给computed属性。自从有了对象展开运算符可极大地简化写法。
computed:{
localComputed () {
// 使用对象展开运算符对此对象混入到外部对象中
...mapState({
//...
})
}
}
使用Vuex并不意味着需要将所有状态放入Vuex,虽然将所有状态放入Vuex会使状态变化更显式和易调用,但也会使代码变得冗长且不直观。有些状态 严格属于单个组件,最好还是作为组件的局部状态。
getter
有时需从仓库中的状态中派生一些状态
computed:{
// 对列表进行过滤并计数
doneTodosCount() {
return this.$store.state.todos.filter(todo=>todo.done).length
}
}
如果有多个组件需要用到此属性,要么复制函数或抽取到一个共享函数然后再多出导入,但是无论哪种方式都不是很理想。
Vuex允许在仓库中定义getter(可认为是store的计算属性),就像计算属性一样,getter的返回值会根据它的依赖被缓存起来,只有当当它的依赖发生变化了才会被重新计算。
const store = new Vuex.Store({
state:{
todos:[
{id:1, text:'...', done:true},
{id:2, text:'...', done:false}
]
},
// getter类似仓库的计算属性一样,其返回值会根据其依赖被缓存起来,只有当以来之发生改变才会被重新计算。
getters:{
doneTodos:state=>{
return state.todos.filter(todo=>todo.done)
}
}
})
getter会暴露为store.getters对象,可以以属性的形式访问其值:
# getter会暴露为store.getters对象,可以以属性的形式访问其值:
store.getters.doneTodos
# getter也可以接受其他getter作为第二个参数
getters:{
doneTodosCount:(state, getters)=>{
return getters.doneTodos.length
}
}
store.getters.doneTodosCount
# 可以很容易在任何组件中使用它
computed:{
doneTodosCount(){
return this.$store.getters.doneTodosCount
}
}
注意:getter在通过属性访问时是作为Vue的响应式系统的一部分缓存其中的。
可以通过让getter返回一个函数来实现给getter传参,在对store中的数组进行查询时非常有用。
getters:{
//可以通过让getter返回一个函数来实现给getter传参
getTodoById:(state)=>(id)=>{
return state.todos.find(todo=>todo.id === id)
}
}
store.getters.getTodoById(2)
注意,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'
})
mutation
更改Vuex仓库中的状态的唯一方式是提交mutation。vuex中的mutation非常类似于事件:每个mutation都有一个字符串的事件类型(type)和一个回调函数(handler)。这个回调函数就是实际进行状态更改的地方,并且它会接收state作为第一个参数。
const store = new Vuex.Store({
//状态
state:{count:1},
//状态变更
mutations:{
//每个mutation都有一个字符串的事件类型和一个回调函数
increment (state) {
state.count++ //变更状态
}
}
})
不能直接调用一个mutation handler,这个选项更像是事件注册:“当触发一个类型为increment的mutation时,调用此函数”。要唤醒一个mutation handler,你需要以相应的type调用store.commit方法。
store.commit('increment')
提交载荷
可以像store.commit传入额外的参数,即mutation的载荷payload
mutations:{
increment (state, n){
state.count += n
}
}
// 向store.commit传入额外的参数及mutation的载荷
store.commit('increment', 10)
多数情况下,载荷是一个对象,可包含多个字段并记录mutation会更易读。
mutations:{
increment(state, payload) {
state.count += payload.amount
}
}
// 载荷为对象
store.commit('increment',{acount:10})
对象风格的提交方式
提交mutation的另一种方式是直接使用包含type属性的对象
# 使用包含type属性的对象提交mutation
store.commit({type:'increment', amount:10})
# 使用对象风格提交方式,整个对象都作为载荷传给mutation函数,因此handler保持不变。
mutations:{
increment(state, payload){
state.count += payload.amount
}
}
mutation需遵守vue的响应规则
既然vuex的store中的状态是响应式的,当变更状态时,监视状态的vue组件会也自动更新,这也意味着vuex中的mutation也需要与使用vue一样遵守一些事项:
- 最好提前在store中初始化好所有属性
- 当需要在对象上添加新属性时,应该
- 使用Vue.set(obj, 'prop', 123)
- 以新对象替换老对象
state.obj = {...state.obj, prop:123}
使用常量替代mutation事件类型
使用常量替代mutation事件类型在各种flux实现中最为常见,可使用linter之类的工具发挥作用,同时将常量放在单独的文件中可让代码合作者对整个app包含的mutation一目了然。
// mutation-types.js
export const 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){
}
}
})
mutation必须是同步函数
Axios
Axios是一个基于promise的HTTP库,可用于浏览器和Node.js中。
- 从浏览器中创建XMLHttpRequest
- 从Node.js中创建HTTP请求
- 支持Promise API
- 拦截请求和响应
- 转换请求数据和响应数据
- 取消请求
- 自动转换JSON数据
- 客户端支持防御XSRF
# 安装 axios
$ npm i axios -S
# 执行GET请求
axios.get('/user?id=1').then(function(response){
console.log(response);
}).catch(function(error){
console.log(error)
})
axios.get('/user', {params:{id:1}}).then(function(response){
console.log(response);
}).catch(function(error){
console.log(error)
})
# 执行POST请求
axios.post('/user',{username:'', password:''}).then(function(response){
console.log(response);
}).catch(function(error){
console.log(error)
})
# 执行多个并发请求
function getUsername(){
return axios.get('/user/1')
}
function getPermission(){
return axios.get('/user/1/permission')
}
axios.all([getUsername(), getPermission()]).then(axios.spread(function(username, permission){
}))
axios API
# 向axios传递相关配置来创建请求
axios(url[, config])
Vue中使用Axios
Axios并非Vue的插件无法直接引入后使用,只能在每个需要发送请求的组件中即时引入。为了解决这个问题,有两种思路:
- 引入Axios修改原型链
# main.js
# 引入
import axios from 'axios`
# 原型链绑定
Vue.prototype.$ajax = axios
# 使用
methods:{
submit(){
this.$ajax({
method:'post',
url:'url',
data:{username:'username', password:'password'}
})
}
}
- 结合Vuex封装action
Vuex的mutation类似于事件,可用于提交Vuex中的状态,action和mutation很类似,区别在于action包含异步操作,还可通过action来提交mutation。最主要的区别在于
- mutation 固有参数state用于接收Vuex中的state对象
- action 固有参数context是state的父级,包含着state、getters。