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
}

购物车运行效果:

posted @ 2017-09-28 16:38  rogerwu  阅读(470)  评论(0编辑  收藏  举报