Vuex-状态管理(24)
Vuex狀態管理
課程目標
- 組件通信方式回顧
- Vuex核心概念和基本使用
- 購物車案例
- 模擬實現Vuex
組件內的狀態管理流程
Vue最核心的兩個功能:數據驅動和組件化
組件化開發給我們帶來了:
- 更快的開發效率
- 更好的可維護性
每個組件都有自己的狀態、視圖和行為等組成部分
new Vue({ // statedata () {return {count: 0}}, // viewtemplate: ` <div>{{ count }}</div> `, // actionsmethods: {increment () {this.count++}} })狀態管理包含一下幾部分:
- state,驅動應用的數據源
- view,以聲明方式將state映射到視圖
- actions,響應在view上的用戶輸入導致的狀態變化
組件間通信方式回顧
大多數場景下的組件都并不是獨立存在的,而是相互協作共同構成了一個復雜的業務功能。在 Vue 中為不同的組件關系提供了不同的通信規則。
父傳子:Props Down
- 子組件中通過props接受數據
- 父組件中給子組件通過相應屬性傳值
Props
Prop 的大小寫 (camelCase vs kebab-case)
HTML 中的 attribute 名是大小寫不敏感的,所以瀏覽器會把所有大寫字符解釋為小寫字符。這意味著當你使用 DOM 中的模板時,camelCase (駝峰命名法) 的 prop 名需要使用其等價的 kebab-case (短橫線分隔命名) 命名:
Vue.component('blog-post', { // 在 JavaScript 中是 camelCase 的 props: ['postTitle'], template: '<h3>{{ postTitle }}</h3>' }) <!-- 在 HTML 中是 kebab-case 的 --> <blog-post post-title="hello!"></blog-post>重申一次,如果你使用字符串模板,那么這個限制就不存在了。
Prop 類型
到這里,我們只看到了以字符串數組形式列出的 prop:
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']但是,通常你希望每個 prop 都有指定的值類型。這時,你可以以對象形式列出 prop,這些 property 的名稱和值分別是 prop 各自的名稱和類型:
props: {title: String,likes: Number,isPublished: Boolean,commentIds: Array,author: Object,callback: Function,contactsPromise: Promise // or any other constructor }這不僅為你的組件提供了文檔,還會在它們遇到錯誤的類型時從瀏覽器的 JavaScript 控制臺提示用戶。你會在這個頁面接下來的部分看到類型檢查和其它 prop 驗證。
子傳父:Event Up
非父子組件:Event Bus
我們可以使用一個非常簡單的Event Bus來解決這個問題:
eventbus.js
export default new Vue()然后在需要通信的兩端:
使用$on訂閱:
// 沒有參數 bus.$on('自定義事件名稱', () => {// 執行操作 })// 有參數 bus.$on('自定義事件名稱', data => {// 執行操作 })使用$emit發布:
// 沒有自定義傳參 bus.$emit('自定義事件名稱')// 有自定義傳參 bus.$emit('自定義事件名稱', 參數數據)父直接訪問子組件:通過ref獲取子組件
ref有兩個作用:
- 如果把它作用到普通HTML標簽上,則獲取的是DOM
- 如果把它作用到組件標簽上,則獲取到的是組件實例
創建base-input
<template><div><h1>ref Child</h1> <input ref="input" type="text" v-model="value"></div> </template> <script>export default {data() {return {value: ''}}, methods: {focus() {this.$refs.input.focus()}} }</script>在使用子組件的時候,添加ref屬性:
<base-input ref='usernameInput'></base-input>然后在父組件等渲染完畢后使用$refs訪問:
mounted() {this.$refs.usernameInput.focus() }$refs只會在組件渲染完成之后生效,并且它們不是響應式的。這僅作為一個用于直接操作子組件的“逃生艙”——你應該避免在模板或計算屬性中訪問$refs
簡易的狀態管理方案
如果多個組件之間要共享狀態(數據),使用上面的方式雖然可以實現,但是比較麻煩,而且多個組件之間互相傳值很難跟蹤數據的變化,如果出現問題很難定位問題。
當遇到多個組件需要共享狀態的時候,典型的場景:購物車。我們如果使用上述的方案都不合適,會遇到以下問題:
- 多個視圖依賴同一狀態
- 來自不同視圖的行為需要變更同一狀態
對于問題一,傳參的方法對于多層嵌套的組件將會非常繁瑣,并且對于兄弟組件間的狀態傳遞無能為力。
對于問題二,我們經常會采用父子組件直接引用或者通過事件來變更和同步狀態的多份拷貝。以上的這些模式非常脆弱,通常會導致無法維護的代碼。
因此,我們為什么不把組件的共享狀態抽取出來,以一個全局單例模式管理呢?在這種模式下,我們的組件樹構成了一個巨大的“視圖”,不管在樹的哪個位置,任何組件都能獲取狀態或者觸發行為!
通過定義和隔離狀態管理中的各種概念并通過強制規則維持視圖和狀態間的獨立性,我們的代碼將會變得更結構化且易維護。
我們可以把多個組件的狀態,或者整個程序的狀態放到一個集中的位置存儲,并且可以檢測到數據的更改。你可能已經想到了 Vuex。
這里我們先以一種簡單的方式來實現
- 首先創建一個共享的倉庫 store 對象
- 把共享的倉庫 store 對象,存儲到需要共享狀態的組件的 data 中
接著我們繼續延伸約定,組件不允許直接變更屬于 store 對象的 state,而應執行 action 來分發(dispatch) 事件通知 store 去改變,這樣最終的樣子跟 Vuex 的結構就類似了。這樣約定的好處是,我們能夠記錄所有 store 中發生的 state 變更,同時實現能做到記錄變更、保存狀態快照、歷史回滾/時光旅行的先進的調試工具。
Vuex回顧
什么是Vuex
Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件的狀態,并以相應的規則保證狀態以一種可預測的方式發生變化。Vuex 也集成到 Vue 的官方調試工具 devtools extension,提供了諸如零配置的 time-travel 調試、狀態快照導入導出等高級調試功能。
- Vuex 是專門為 Vue.js 設計的狀態管理庫
- 它采用集中式的方式存儲需要共享的數據
- 從使用角度,它就是一個 JavaScript 庫
- 它的作用是進行狀態管理,解決復雜組件通信,數據共享
什么情況下使用Vuex
官方文檔:
Vuex 可以幫助我們管理共享狀態,并附帶了更多的概念和框架。這需要對短期和長期效益進行權衡。
如果您不打算開發大型單頁應用,使用 Vuex 可能是繁瑣冗余的。確實是如此——如果您的應用夠簡單,您最好不要使用 Vuex。一個簡單的 store 模式就足夠您所需了。但是,如果您需要構建一個中大型單頁應用,您很可能會考慮如何更好地在組件外部管理狀態,Vuex 將會成為自然而然的選擇。引用 Redux 的作者 Dan Abramov 的話說就是:Flux 架構就像眼鏡:您自會知道什么時候需要它。
當你的應用中具有以下需求場景的時候:
- 多個視圖依賴于同一狀態
- 來自不同視圖的行為需要變更同一狀態
建議符合這種場景的業務使用 Vuex 來進行數據管理,例如非常典型的場景:購物車。
注意:Vuex 不要濫用,不符合以上需求的業務不要使用,反而會讓你的應用變得更麻煩。
核心概念回顧
- Store:倉庫,store是使用Vuex應用程序的核心,每一個應用僅有一個store。store是一個容器,包含應用中的大部分狀態,當然我們不能直接改變store中的應用狀態,我們需要通過提交mutation的方式改變狀態。
- State:就是狀態,保存在store中。因為store是唯一的,所以state狀態也是惟一的,稱為單一狀態樹。但是所有的狀態都保存在state中的話,會讓程序難以維護,可以通過后續的模塊解決該問題。
- 注意:這里的state狀態是響應式的
- Getter:getter就像是Vuex中的計算屬性,方便從一個屬性派生出其他的值,它內部可以對計算的結果進行緩存,只有當內部依賴的state狀態發生改變時才會重新計算。
- Mutation:state狀態的變化必須要通過提交mutation來完成
- Action:action和mutation類似,不同的是action可以進行異步的操作,內部改變狀態的時候都需要提交mutation
- Module:由于使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象上來,當應用變得十分復雜時,,store對象就有可能編的相當臃腫。為了解決以上問題,Vuex允許我們將store分隔成模塊,每個模塊擁有自己的state、mutation、action、getter甚至是嵌套的子模塊
示例演示
使用vue create vuex-demo創建包含router和vuex的空項目
基本結構
src/store/index.js
import Vue from 'vue' // 1.導入Vuex import Vuex from 'vuex'// 2.注冊Vuex Vue.use(Vuex)export default new Vuex.Store({state: {},mutations: {},actions: {},modules: {} })src/main.js
import Vue from 'vue' import store from './store'Vue.config.productionTip = falsenew Vue({store, // 3.注入$store到Vue實例render: h => h(App) }).$mount('#app')State
Vuex使用單一狀態樹,用一個對象就包含了全部的應用層級狀態。
使用mapState簡化State在視圖中的使用,mapState返回計算屬性
mapState有兩種使用方式:
-
接收數組參數
// 該方式是Vuex提供的,所以使用前需要先導入 import { mapState } from 'vuex' // mapState返回名稱為count和msg的計算屬性 // 在模板中直接使用count和msg computed: {...mspState(['count', 'msg']) }使用數組參數
<h1>Vuex - Demo</h1> <!-- count: {{ $store.state.count }}<br>--> <!-- msg: {{ $store.state.msg }}-->count: {{ count }}<br> msg: {{ msg }} -
接受對象參數
如果當前視圖中已經有了count和msg,如果使用上述方式的話會有命名沖突,解決的方式:
import {mapState} from 'vuex'export default {computed: {// count: state => state.count// ...mapState(['count', 'msg'])...mapState({num: 'count', message: 'msg'}) // 當store中存在count和msg時,使用對象參數重命名count和msg} }使用對象參數
<h1>Vuex - Demo</h1> count: {{ num }}<br> msg: {{ message }}
Getter
Getter就是store中的計算屬性,使用mapGetter簡化視圖中的使用
App.vue
import {mapGetters, mapState} from 'vuex'export default {computed: {// count: state => state.count// ...mapState(['count', 'msg'])...mapState({num: 'count', message: 'msg'}),...mapGetters(['reverseMsg'])} }使用
<h2>Getter</h2> <!-- reverseMsg: {{ $store.getters.reverseMsg }}--> reverseMsg: {{ reverseMsg }}src/store/index.js
import Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {count: 1,msg: 'Hello Vuex'},getters: {reverseMsg(state) {return state.msg.split('').reverse().join('')}},mutations: {},actions: {},modules: {} })Mutation
更改Vuex的store中的狀態的唯一方法是提交mutation。Vuex中的mutation非常類似于事件:每個mutation都有一個字符串的事件類型(type)和一個回調函數(handler)。這個回調函數就是我們實際進行狀態更改的地方,并且它會接受state作為第一個參數。
使用Mutation改變狀態的好處是:集中的一個位置對狀態修改,不管在什么地方修改,都可以追蹤到狀態的修改。可以實現高級的time-travel調試功能
App.vue
import {mapGetters, mapMutations, mapState} from 'vuex'export default {computed: {// count: state => state.count// ...mapState(['count', 'msg'])...mapState({num: 'count', message: 'msg'}),...mapGetters(['reverseMsg']),},methods: {...mapMutations(['increate'])} }使用
<h2>Mutation</h2> <!-- <button @click="$store.commit('increate', 2)">Mutation</button>--> <button @click="increate(3)">Mutation</button>src/store/index.js
import Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {count: 1,msg: 'Hello Vuex'},getters: {reverseMsg(state) {return state.msg.split('').reverse().join('')}},mutations: {increate(state, payload) {state.count += payload}},actions: {},modules: {} })Action
Action類似于mutation,不同在于:
- Action提交的是mutation,而不是直接變更狀態
- Action可以包含任意異步操作
App.vue
import {mapActions, mapGetters, mapMutations, mapState} from 'vuex'export default {computed: {// count: state => state.count// ...mapState(['count', 'msg'])...mapState({num: 'count', message: 'msg'}),...mapGetters(['reverseMsg']),},methods: {...mapMutations(['increate']),...mapActions(['increateAsync'])} }使用
<h2>Action</h2> <!-- <button @click="$store.dispatch('increateAsync', 5)">Action</button>--> <button @click="increateAsync(5,1)">Action</button>src/store/index.js
import Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {count: 1,msg: 'Hello Vuex'},getters: {reverseMsg(state) {return state.msg.split('').reverse().join('')}},mutations: {increate(state, payload) {state.count += payload}},actions: {increateAsync(context, payload) {console.log(payload)setTimeout(() => {context.commit('increate', payload)}, 2000)}},modules: {} })Module
由于使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象。當應用變得非常復雜時,store 對象就有可能變得相當臃腫。
為了解決以上問題,Vuex 允許我們將 store 分割成模塊(module)。每個模塊擁有自己的 state、mutation、action、getter、甚至是嵌套子模塊。在案例中體會 Module 的使用。
目錄結構:
嚴格模式
之前在介紹核心概念時說過,所有的狀態變更必須通過提交mutation,但是這僅僅是一個約定。如果你想的話,可以在組建中隨時獲取$store.state.msg,對它進行修改。從語法層面來說,這是沒有問題的,但是這樣操作破壞了Vuex的約定。如果在組件中直接修改state,那在dev-tools中無法追蹤狀態的變更。開啟嚴格模式之后,如果在組件中直接修改state狀態,會拋出錯誤。演示如下:
store/index.js中添加strict: true
在App.vue中添加如下代碼,點擊按鈕直接修改store中state.msg的值
打開瀏覽器進行測試,發現$store.state.msg的值確實被修改了,但是console中會拋出異常。
需要注意的是:不要再生產模式下開啟嚴格模式,嚴格模式會深度檢查狀態樹,來檢查不合規的狀態改變,會影響性能。可以再開發環境中啟用嚴格模式,在生產模式下關閉嚴格模式。調整后的代碼:
- 當npm run serve時,process.env.NODE_ENV為development開發環境;
- 當npm run build時,process.env.NODE_ENV是production。這樣就可以根據環境來動態的設置嚴格模式。
購物車案例
接下來我們通過一個購物車案例來演示 Vuex 在項目中的使用方式,首先把購物車的項目模板下載下來。
模板地址
案例演示
server.js,在訪問數據時,必須先使用node server.js啟動server接口
const express = require('express') const cors = require('cors') const app = express()app.use(cors())const hostname = '127.0.0.1' const port = 3000const _products = [{ id: 1, title: 'iPad Pro', price: 500.01 },{ id: 2, title: 'H&M T-Shirt White', price: 10.99 },{ id: 3, title: 'Charli XCX - Sucker CD', price: 19.99 } ]app.use(express.json())app.get('/products', (req, res) => {res.status(200).json(_products) })app.post('/checkout', (req, res) => {res.status(200).json({success: Math.random() > 0.5}) })app.listen(port, hostname, () => {console.log(`Server is running at http://${hostname}:${port}/`) })功能列表
- 商品列表組件
- 商品列表中彈出框組件(購物車彈出框)
- 購物車列表組件
商品列表
商品列表功能
-
Vuex中創建兩個模塊,分別用來記錄商品列表和購物車的狀態,stroe的結構:
store--modulescart.jsproducts.jsindex.js -
products模塊,store/modules/products.js
- store/index.js中注冊products.js模塊
- views/products.vue中實現商品列表的功能
添加購物車
- cart 模塊實現添加購物車功能,store/modules/cart.js
- store/index.js 中注冊 cart 模塊
- view/products.vue 中實現添加購物車功能
- 測試,通過 vue-devtools 觀察數據的變化
商品列表-彈出購物車窗口
購物車列表
- components/pop-cart.vue中展示購物車列表
刪除
- cart 模塊實現從購物車刪除的功能,store/modules/cart.js
- components/pop-cart.vue 中實現刪除功能
小計
- cart 模塊實現統計總數和總價,store/modules/cart.js
- components/pop-cart.vue 中顯示徽章和小計
購物車
購物車列表
<template><el-popoverwidth="350"trigger="hover"><el-table :data="cartProducts" size="mini"><el-table-column property="title" width="130" label="商品"></el-table-column><el-table-column property="price" label="價格"></el-table-column><el-table-column property="count" width="50" label="數量"></el-table-column><el-table-column label="操作"><template v-slot="scope"><el-button @click="deleteFromCart(scope.row.id)" size="mini">刪除</el-button></template></el-table-column></el-table><div><p>共 {{ totalCount }} 件商品 共計¥{{ totalPrice }}</p><el-button size="mini" type="danger" @click="$router.push({ name: 'cart' })">去購物車</el-button></div><el-badge :value="totalCount" class="item" slot="reference"><el-button type="primary">我的購物車</el-button></el-badge></el-popover> </template><script> import { mapState, mapGetters, mapMutations } from 'vuex' export default {name: 'PopCart',computed: {...mapState('cart', ['cartProducts']),...mapGetters('cart', ['totalCount', 'totalPrice'])},methods: {...mapMutations('cart', ['deleteFromCart'])} } </script><style></style>全選功能
- cart 模塊實現更新商品的選中狀態,store/modules/cart.js
- views/cart.vue,實現全選功能
- 使用事件拋出一個值
數組文本框
- cart 模塊實現更新商品數量,store/modules/cart.js
- views/cart.vue,實現數字文本框功能
刪除
小計
- cart 模塊實現統計選中商品價格和數量,store/modules/cart.js
- views/cart.vue,實現小計
本地存儲
Vuex插件
- Vuex的插件就是一個函數
- 這個函數接受一個store的參數
- 這個函數內可以注冊一個函數,讓它可以在mutaions之后再執行
- 就像在axios中的過濾器,在所有請求之后統一完成一件事
- mutation的結構
- 如果想在cart模塊中的mutation之行結束之后再來調用調用,product模塊中不需要,可以使用mutation
- 注冊插件
-
最終實現
import Vue from 'vue' import Vuex from 'vuex' import products from './modules/products' import cart from './modules/cart'Vue.use(Vuex)const myPlugin = store => {// 當store初始化后調用// subscribe的作用是用來訂閱store中的mutation,會在每個mutation完成之后調用// 參數:mutation、state// 如果想在cart模塊中的mutation之行結束之后再來調用調用,product模塊中不需要,可以使用mutationstore.subscribe((mutation, state) => {// 每次調用mutation之后調用// mutation的格式為 { type, payload }if (mutation.type.startsWith('cart/')) {//記錄到localStoragewindow.localStorage.setItem('cart-products', JSON.stringify(state.cart.cartProducts))}}) }export default new Vuex.Store({state: {},mutations: {},actions: {},modules: {cart,products},plugins: [myPlugin] })
Vuex模擬實現
回顧基礎示例,自己模擬實現一個Vuex實現同樣的功能
import Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex) export default new Vuex.Store({state: {count: 0,msg: 'Hello World'},getters: {reverseMsg(state) {return state.msg.split('').reverse().join('')}},mutations: {increate(state, payload) {state.count += payload.num}},actions: {increate(context, payload) {setTimeout(() => {context.commit('increate', {num: 5})}, 2000)}} })實現思路
- 實現install方法
- Vuex是Vue的一個插件,所以和模擬VueRouter類似,縣實現Vue插件約定的install方法
- 實現Store類
- 實現構造函數,接受options對象參數
- state的響應式處理
- getter的實現
- commit、dispatch方法
install方法
let _Vue = null function install (Vue) {_Vue = Vue_Vue.mixin({beforeCreate () {if (this.$options.store) {Vue.prototype.$store = this.$options.store}}}) }Store類
let _Vue = nullclass Store {constructor(options) {const {state = {},getters = {},mutations = {},actions = {}} = optionsthis.state = _Vue.observable(state)// 此處不直接 this.getters = getters,是因為下面的代碼中要方法 getters 中的 key// 如果這么寫的話,會導致 this.getters 和 getters 指向同一個對象// 當訪問 getters 的 key 的時候,實際上就是訪問 this.getters 的 key 會觸發 key 屬性的getter// 會產生死遞歸this.getters = Object.create(null)Object.keys(getters).forEach(key => {Object.defineProperty(this.getters, key, {get: () => getters[key](state)})})this._mutations = mutationsthis._actions = actions}commit(type, payload) {this._mutations[type](this.state, payload)}dispatch(type, payload) {this._actions[type](this, payload)}}// install方法可以接受兩個參數,一個是Vue構造函數,另外一個是額外的選項,這里只需要Vue構造函數 function install(Vue) {_Vue = Vue_Vue.mixin({beforeCreate() {// 首先判斷當前Vue實例的$options中是否有store,如果是組件實例的話沒有store選項,就不需要做這件事if (this.$options.store) {// 這里注冊插件的時候會混入beforeCreate,當創建根實例的時候就會把$store注入到Vue實例上_Vue.prototype.$store = this.$options.store}}}) }export default {Store,install }使用自己實現的Vuex
src/store/index.js 中修改導入 Vuex 的路徑,測試
import Vuex from '../myvuex' // 注冊插件 Vue.use(Vuex)總結
以上是生活随笔為你收集整理的Vuex-状态管理(24)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 路由器里的DHCP是什么?如何开启路由器
- 下一篇: vue学习九--v-for的四种用法,以