Vuex 学习笔记
Vuex 是什么?
Vuex 是一个专为 Vue.js应用程序开发的状态管理模式。由于SPA应用的模块化,每个组件都有它各自的数据(state)、视图(view)和方法(actions),当项目内容越来越多时,每个组件中的状态就变得很难管理。Vuex 就是采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
1、单个组件中的状态
看一下官网提供的计数示例:
<template> <div> <button class="btn btn-success" @click="increment">increment</button> view: {{count}} </div> </template> <script> export default { // state data () { return { count: 0 } }, // actions methods: { increment () { this.count++ } } } </script>
运行结果:
从效果图中可以直观的看到,每点击一次按钮触发添加事件(actions),数据count(state)就会发生改变,然后映射到视图界面(view)中。
下图可以表示 “ 单项数据流 ” 理念的极简示意:
这个状态管理应用包含以下几个部分:
• state:驱动应用的数据源
• view:以声明方式将 state 映射到视图
• actions:响应在 view 上的用户输入导致的状态变化
2、多个组件中的状态
当我们的应用遇到 多个组件共享状态 时,单向数据流的简洁性很容易被破坏:
• 多个视图依赖于同一状态
• 来自不同视图的行为需要变更同一状态
同样是计数器,我们现在更换一种场景,两个相同的组件A和B,共享一个数据count,并且都有一个方法可以操作这个count(是不是跟上面提到的多组件共享状态描述的一样呢)
// 组件A <template> <div> {{ $store.state.count }} <button @click="increment">组件A</button> </div> </template> <script> export default { methods: { increment () { this.$store.commit('increment') } } } </script> //组件B <template> <div> {{ $store.state.count }} <button @click="increment">组件B</button> </div> </template> <script> export default { methods: { increment () { this.$store.commit('increment') } } } </script>
运行效果:
从图中可以看到,“组件A” 和 “组件B” 两个按钮 会同时改变两个 count 的数据,因为数据源 count 和 方法increment 都是全局的。如下图所示,我们把 全局数据源 state,改变数据源的方法 mutations 和 异步操作方法 actions 提取出来放到 store 中,实现全局数据状态单独管理的功能
安装
1、使用 npm 安装并保存到 package.json 中
npm install vuex --save
package.json
"dependencies": { ..., ..., ..., "vuex": "^2.4.1" },
2、配置
// 如果在模块化构建系统中,请确保在开头调用了 Vue.use(Vuex) import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) //创建Store实例 const store = new Vuex.Store({ // 存储状态值 state: { ... }, // 状态值的改变方法,操作状态值 // 提交mutations是更改Vuex状态的唯一方法 mutations: { ... }, // 在store中定义getters(可以认为是store的计算属性)。Getters接收state作为其第一个函数 getters: { ... }, actions: { ... } }) // 要改变状态值只能通过提交mutations来完成 /* eslint-disable no-new */ const app = new Vue({ router, i18n, // 将 store 实例注入到根组件下的所有子组件中,子组件通过 this.$store 来访问store
store,
...App
})
app.$mount('#app')
看一下官网提供的例子:
<template> <div> <p>{{ count }}</p> <p> <button @click="increment">+</button> <button @click="decrement">-</button> </p> </div> </template> <script> export default { computed: { count () { // 通过 store.state 来获取状态对象 return this.$store.state.count } }, methods: { increment () { // 通过 store.commit 方法触发状态变更 this.$store.commit('increment') }, decrement () { this.$store.commit('decrement') } } } </script>
// 创建 Store 实例 const store = new Vuex.Store({ // 存储状态值 state: { count: 0 }, // 状态值的改变方法,操作状态值 // 提交 mutations 是更改Vuex状态的唯一方法 mutations: { increment: state => state.count++, decrement: state => state.count-- } })
运行效果:
核心概念
1、State
state 就是全局的状态(数据源),从前面的例子中看到我们可以按如下方式获取 Vuex 的state 状态
// html 中 {{ $store.state.count }} // js 中 this.$store.state.count
2、Getter
getter 可以认为是 store 的计算属性,跟计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会重新计算
如下官网提供的案例:
computed: { doneTodosCount () { return this.$store.state.todos.filter(todo => todo.done).length } }
如果有多个组件需要用到此属性,我们要么复制这个函数,或者抽取到一个共享函数然后在多处导入它,然而这两种方法都不是很理想,最佳方式当然是使用 getter 了
我们尝试使用下getter
(1)、定义 getter
const store = new Vuex.Store({ state: { count: 0 }, getters: { formatMoney: state => { return '¥'+state.count.toFixed(2)+'元' } }, mutations: { increment: state => state.count++ } })
(2)、在组件中引用 getter
export default { methods: { increment () { this.$store.commit('increment') // 这里为了更清楚的看到计算后的值 let aaa = document.getElementById('aaa') let p = document.createElement('p') p.innerHTML = this.$store.getters.formatMoney aaa.appendChild(p) } }, computed: { formatMoney() { return this.$store.getters.formatMoney } } }
效果:
3、Mutation
更改 Vuex 的 store 中的状态的唯一方法就是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的 事件类型(type)和一个 回调函数(handler),这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
const store = new Vuex.Store({ state: { count: 1 }, mutations: { increment (state) { // 变更状态 state.count++ } } })
要唤醒一个 mutation handler,你需要以相应的 type 调用 store.commit 方法
store.commit('increment')
(1)、提交载荷(Payload)
载荷(payload)就是说 可以向 store.commit 传入额外的参数
// ... 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 })
4、Action
Vuex 中一条重要的原则就是 mutation 必须是同步函数, action 类似于 mutation,不同之处在于:
• Action 提交的是 mutation,而不是直接变更状态
• Action 可以包含任意异步操作
const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } }, actions: { increment (context) { context.commit('increment') }, // 异步 incrementAsync (context) { // 延时1秒 setTimeout(() => { context.commit('increment') }, 1000) } } })
Action 函数接受一个与 store 实例具有相同方法和属性的context对象,因此,可以有以下调用方法
• context.commit 提交一个 mutation
• context.state 获取 state
• context.getters 获取 getters
不同于 mutation 使用 commit 方法,action 使用 dispatch 方法
store.dispatch('increment')
Actions 同样支持 载荷方式 和 对象方式 进行分发:
// 以载荷形式分发 store.dispatch('incrementAsync', { amount: 10 }) // 以对象形式分发 store.dispatch({ type: 'incrementAsync', amount: 10 })
5、Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象,当应用变得非常复杂时,store 对象就有可能变得非常臃肿。
为了解决以上问题,Vuex 允许我们将 store 分割成 模块(module),每个模块拥有自己的 state、mutation、getter、action,甚至是嵌套子模块 --- 从上至下进行同样方式的分割
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 的状态
关于项目结构,我们可以看看官网提供的示例:
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我们组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块
官网同时也提供了一个 购物车 示例:
app.js 文件如下:
import 'babel-polyfill' import Vue from 'vue' import App from './components/App.vue' import store from './store' import { currency } from './currency' Vue.filter('currency', currency) new Vue({ el: '#app', store, render: h => h(App) })
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' import createLogger from '../../../src/plugins/logger' Vue.use(Vuex) const debug = process.env.NODE_ENV !== 'production' export default new Vuex.Store({ actions, getters, modules: { cart, products }, strict: debug, plugins: debug ? [createLogger()] : [] })
getters.js 文件如下:
export const cartProducts = state => { return state.cart.added.map(({ id, quantity }) => { const product = state.products.all.find(p => p.id === id) return { title: product.title, price: product.price, quantity } }) }
actions.js 文件如下:
import * as types from './mutation-types' export const addToCart = ({ commit }, product) => { if (product.inventory > 0) { commit(types.ADD_TO_CART, { id: product.id }) } }
mutation-type.js 文件如下:
export const ADD_TO_CART = 'ADD_TO_CART' export const CHECKOUT_REQUEST = 'CHECKOUT_REQUEST' export const CHECKOUT_SUCCESS = 'CHECKOUT_SUCCESS' export const CHECKOUT_FAILURE = 'CHECKOUT_FAILURE' export const RECEIVE_PRODUCTS = 'RECEIVE_PRODUCTS'
cart.js 文件如下:
import shop from '../../api/shop' import * as types from '../mutation-types' // initial state // shape: [{ id, quantity }] const state = { added: [], checkoutStatus: null } // getters const getters = { checkoutStatus: state => state.checkoutStatus } // actions const actions = { checkout ({ commit, state }, products) { const savedCartItems = [...state.added] commit(types.CHECKOUT_REQUEST) shop.buyProducts( products, () => commit(types.CHECKOUT_SUCCESS), () => commit(types.CHECKOUT_FAILURE, { savedCartItems }) ) } } // mutations const mutations = { [types.ADD_TO_CART] (state, { id }) { state.lastCheckout = null const record = state.added.find(p => p.id === id) if (!record) { state.added.push({ id, quantity: 1 }) } else { record.quantity++ } }, [types.CHECKOUT_REQUEST] (state) { // clear cart state.added = [] state.checkoutStatus = null }, [types.CHECKOUT_SUCCESS] (state) { state.checkoutStatus = 'successful' }, [types.CHECKOUT_FAILURE] (state, { savedCartItems }) { // rollback to the cart saved before sending the request state.added = savedCartItems state.checkoutStatus = 'failed' } } export default { state, getters, actions, mutations }
products.js 文件如下:
import shop from '../../api/shop' import * as types from '../mutation-types' // initial state const state = { all: [] } // getters const getters = { allProducts: state => state.all } // actions const actions = { getAllProducts ({ commit }) { shop.getProducts(products => { commit(types.RECEIVE_PRODUCTS, { products }) }) } } // mutations const mutations = { [types.RECEIVE_PRODUCTS] (state, { products }) { state.all = products }, [types.ADD_TO_CART] (state, { id }) { state.all.find(p => p.id === id).inventory-- } } export default { state, getters, actions, mutations }
购物车运行效果: