Vuex深度解析
我希望有個如你一般的人。
如山間清爽的風,如古城溫暖的光。
從清晨到夜晚,由山野到書房。
只要最后是你,就好。
一、組件通信方式
1. 組件內的狀態管理流程
組件化開發優點: 更快的開發效率、更好的可維護性
此處數據的流向是單向的,State狀態就是我們所說的數據,數據綁定到視圖展示給用戶,當用戶和視圖交互,通過Actions更改數據之后,更改后的數據重新綁定到視圖。
狀態管理包含以下幾部分:
- state(狀態): 驅動應用的數據源
- view(視圖): 以聲明方式將 state 映射到視圖
- actions(行為): 響應在 view 上的用戶輸入導致的狀態變化
2. 組件間傳值方式
在 Vue 中為不同的組件關系提供了不同的通信規則,組件間傳值可分為:父傳子、子傳父、不相關組件之間
3. 父向子傳值
父組件:
<template><div><h1>Props Down Parent</h1><child title="My journey with Vue"></child></div> </template><script> import child from './01-Child' export default {components: {child} } </script>子組件:
<template><div><h1>Props Down Child</h1><h2>{{ title }}</h2></div> </template><script> export default {// props: ['title'],props: {title: String} } </script>4. 子向父傳值
父組件:
<template><div><h1 :style="{ fontSize: hFontSize + 'em'}">Event Up Parent</h1><child :fontSize="hFontSize" v-on:enlargeText="enlargeText"></child></div> </template><script> import child from './02-Child' export default {components: {child},data () {return {hFontSize: 1}},methods: {enlargeText (size) {this.hFontSize += size}} } </script>子組件:
<template><div><h1 :style="{ fontSize: fontSize + 'em' }">Props Down Child</h1><button @click="handler">文字增大</button></div> </template><script> export default {props: {fontSize: Number},methods: {handler () {this.$emit('enlargeText', 0.1)}} } </script>5. 不相關組件傳值(Event Bus)
組件 A:
<template><div><h1>Event Bus Sibling01</h1><div class="number" @click="sub">-</div><input type="text" style="width: 30px; text-align: center" :value="value"><div class="number" @click="add">+</div></div> </template> ? <script> import bus from './eventbus' ? export default {props: {// 文本框默認顯示的商品個數num: Number},// 因為props的值不建議直接修改,將props數據存儲到value中created () {this.value = this.num},data () {return {value: -1}},methods: {sub () {if (this.value > 1) {this.value--bus.$emit('numchange', this.value)}},add () {this.value++bus.$emit('numchange', this.value)}} } </script>組件 B:
<template><div><h1>Event Bus Sibling02</h1><div>{{ msg }}</div></div> </template> ? <script> import bus from './eventbus' export default {data () {return {msg: ''}},created () {bus.$on('numchange', (value) => {this.msg = `您選擇了${value}件商品`})} } </script>6. 其他組件傳值方法(ref)
其他組件傳值方式: $root、$parent、$children、$ref …
ref 的作用:
- 把它作用到普通 HTML 標簽上,則獲取到的是 DOM 對象
- 如果你把它作用到組件標簽上,則獲取到的是組件實例對象
$refs 只會在組件渲染完成之后生效,并且它們不是響應式的。
這僅作為一個用于直接操作子組件的一種捷徑,盡量避免在模板或計算屬性中訪問 $refs。
濫用會導致數據管理的混亂。
子組件:
<template><div><h1>ref Child</h1><input ref="input" type="text" v-model="value"> // 在HTML標簽上使用</div> </template> ? <script> export default {data () {return {value: ''}},methods: {// 用來從父級組件聚焦輸入框focus () {this.$refs.input.focus()}} } </script>父組件:
<template><div><h1>ref Parent</h1><child ref="c"></child> // 在組件上使用ref</div> </template><script> import child from './04-Child' export default {components: {child},mounted () {this.$refs.c.focus()this.$refs.c.value = 'hello input'} } </script>7. 簡易的狀態管理方案
⑴. 狀態管理遇到的問題:
- 多個視圖依賴同一狀態
- 來自不同視圖的行為需要變更同一狀態
因此,需要把組件的的共享狀態抽取出來(將來使用時保證其為響應式的),不管樹在哪個位置,任何組件都能獲取狀態或者觸發行為。
⑵. 實現一個簡易的狀態集中管理:
1. 創建:
首先創建一個共享的倉庫 store 對象,這是集中式的狀態管理,所有狀態都在 store 中進行管理,且它為全局唯一的對象,任意的組件都可以導入 store 模塊使用其中的狀態,更改狀態也是在該模塊中實現的:
// store.jsexport default {debug: true,state: {user: {name: 'xiaomao',age: 18,sex: '男'}},setUserNameAction (name) {if (this.debug) {console.log('setUserNameAction triggered:', name)}this.state.user.name = name} }2. 儲存:
把共享的倉庫 store 對象,存儲到需要共享狀態的組件 data 中
// componentA.vue<template><div><h1>componentA</h1>user name: {{ sharedState.user.name }}<button @click="change">Change Info</button></div> </template><script> import store from './store' export default {methods: {// 點擊按鈕的時候通過 action 修改狀態change () {store.setUserNameAction('componentA')}},data () {return {// 當前組件特有的自己的狀態,存儲到privateStateprivateState: {},// 把store中的state(共享的狀態)存儲到sharedStatesharedState: store.state}} } </script>3. 修改:
componentA 和 componentB 兩個組件共享了 store 中的狀態,并且和用戶交互的時候還會更改狀態中的 name 屬性
// componentB.vue<template><div><h1>componentB</h1>user name: {{ sharedState.user.name }}<button @click="change">Change Info</button></div> </template><script> import store from './store' export default {methods: {change () {store.setUserNameAction('componentB')}},data () {return {privateState: {},sharedState: store.state}} } </script>采用集中式的狀態管理,使用了全局唯一的對象 store 來存儲狀態,并且有一個共同點約定:
組件不允許直接變更屬于 store 對象的 State,而應執行 Action 來分發(dispatch)事件通知 store 去改變,這樣最終的樣子跟 Vuex 的結構就類似了。
這樣好處是,能夠記錄所有 store 中發生的 State 變更,同時實現能做到記錄變更、保存狀態快照、歷史回滾 / 時光旅行的調試工具。
二、Vuex 核心概念
1. 概述
官方文檔:Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件 的狀態,并以相應的規則保證狀態以一種可預測的方式發生變化。Vuex 也集成到 Vue 的官方調 試工具 devtools extension,提供了諸如零配置的 time-travel 調試、狀態快照導入導出等高級調 試功能。
- Vuex 是專門為 Vue.js 設計的狀態管理庫
- Vuex 采用集中式的方式存儲需要共享的數據
- 從使用角度,Vuex 就是一個 JavaScript 庫
- Vuex 的作用是進行狀態管理,解決復雜組件通信,數據共享
- Vuex 集成到了 devtools 中,提供了 time-travel 時光旅行和歷史回滾的功能
什么情況下使用 Vuex ?
- 非必要不使用 Vuex: Vuex 可以幫助我們管理共享狀態,并附帶了更多的概念和框架。這需要對短期和長期效益進行權衡。
- 大型的單頁應用程序: 多個視圖依賴同一狀態(如購物車)
- 注意: Vuex 不要濫用,不符合以上需求的業務不要使用,反而會讓你的應用變得更麻煩。
2. 工作流程
- Store: 倉庫,是使用Vuex應用程序的核心,每一個應用僅有一個 Store。Store 是一個容器,包含應用中的大部分狀態,不能直接改變 Store 中的狀態,要通過提交 Mutation 的方式
- State: 狀態保存至 Store 中,因為 Store 是唯一的,因此狀態也是唯一的,稱為單一狀態樹。但是如果所有的狀態都保存在 State 中,程序則難以維護,可以通過后續的模塊來解決該問題。注意,這里的狀態時響應式的
- Getter: 像是 Vue 中的計算屬性,對 state 中的數據進行加工,方便從一個屬性派生出其他的值。它內部可以對計算的結果進行緩存,只有當依賴的狀態發生改變時才會重新計算
- Mutation: 狀態的變化必須通過提交 Mutation 來完成 (同步)
- Action: 和 Mutation 類似,不同的是 Action 可以進行異步操作,內部改變狀態的時候都需要提交 Mutation
- Module: 模塊,由于使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象上來,當應用變得非常復雜時,Store對象就有可能變得非常臃腫。為了解決這個問題,Vuex允許我們將 Store 分割成模塊每個模塊擁有自己的 State 、Mutation、Action、Getter甚至是嵌套的子模塊
三、Vuex 基本使用
1. 基本結構
// store/index.jsimport Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {},// 注意單詞復數!!mutations和actions都得加s!!!mutations: {},actions: {},modules: {} })創建 Vue 實例的時候傳入 store 選項,這個 store 選項會被注入到 Vue 實例中,在組件中使用到的 this.$store 就是在這個位置注入的。
// main.jsimport store from './store'new Vue({router,store,render: h => h(App) }).$mount('#app')2. State
Vuex 是單一狀態樹,并且是響應式的,用一個對象就包含了全部的應用層級狀態
⑴. 定義狀態
// store/index.jsimport Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {count: 0,msg: 'Hello Vuex'},mutations: {},actions: {},getters: {},modules: {} })⑵. 常規使用
<template><div><h1>test</h1>count: {{$store.state.count}}msg: {{$store.state.msg}}</div> </template>⑶. 使用mapState轉化為計算屬性來使用
<template><div><h1>test</h1>count: {{count}}msg: {{msg}}</div> </template> <script>import { mapState } from 'vuex'export default {computed: {// 以數組的形式來接收參數// ...mapState(['count', 'msg'])// 這里也可以使用對象的形式來接收數據并進行重命名,防止和組件中原有的數據沖突...mapState({num:'count', message: 'msg'})// mapState 會返回一個對象,其包含兩個計算屬性對應的方法// count : state => state.count // msg : state => state.msg// 計算屬性使用時,內部本來就是接收屬性名對應的帶有返回值的方法的鍵值對的形式}} </script>3. Getter
Getter 就是 store 中的計算屬性,使用 mapGetter 簡化視圖中的使用
⑴. 定義Getter
// store/index.jsimport Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {},mutations: {},actions: {},getters: {// 接收state數據,將處理結果返回reverseMsg (state) {return state.msg.split('').reverse().join('')}},modules: {} })⑵. 使用 mapGetter 簡化視圖中的使用
<template><div><h1>test</h1><!-- 直接使用:{{$store.getters.reverseMsg}} -->reverseMsg的值是{{reverse}}</div> </template> <script>import { mapGetter } from 'vuex'export default {computed: {// 同樣也有兩種接收方式,一種是數組,一種是對象// ...mapGetter(['reverseMsg']),// 重命名,可以在模板中使用 reverse來使用數據...mapGetter({reverse: 'reverseMsg'})}} </script>4. Mutation
更改 Vuex 的 store 中的狀態的唯一方法是提交 mutation
⑴. 定義
// store/index.jsimport Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {count: 0},mutations: {increate (state, payload) { // 參數含義:payload表示傳遞的參數state.count += payload},actions: {},getters: {},modules: {} })⑵. 使用
<template><div><!--如果不使用mapMutation的話需要借助commit來觸發mutation--><!-- <button @click="$store.commit('increate', 2)">Mutation</button> --><button @click="increateMut(3)">Mutation</button></div> </template> <script>import { mapMutations } from 'vuex'export default {// mutation本質上是方法,所以可以映射到方法中methods: {// 以數組方式傳參// ...mapMutations(['increate']),返回的是個映射的方法,但是不再是計算屬性而是對應的函數// 以對象形式傳參來解決重名的問題...mapMutations({increateMut: 'increate'})}} </script>5. Action
Action 類似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接變更狀態
- Action 可以包含任意異步操作
⑴. 定義
// store/index.jsimport Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {count: 0},mutations: {increate (state, payload) { // 參數含義:payload表示傳遞的參數state.count += payload},actions: {increateAsync (context, payload) {setTimeout(() => {context.commit('increate', payload)}, 2000)}},getters: {},modules: {} })⑵. 使用
<template><div><!--如果不使用mapAction的話需要借助dispatch來觸發action--><!-- <button @click="$store.dispatch('increateAsync', 5)">Action</button> --><button @click="increateA(5)">Action</button></div> </template> <script>import { mapAction } from 'vuex'export default {methods: {// 以數組方式傳參// ...mapActions(['increateAsync']),返回的是個映射的方法,但是不再是計算屬性而是對應的函數// 以對象形式傳參來解決重名的問題...mapMutations({increateA: 'increateAsync'})}} </script>6. Module
- 由于使用單一狀態樹,應用的所有狀態會集中到一個比較大的對象。當應用變得非常復雜時,store 對象就有可能變得相當臃腫。
- 為了解決以上問題,Vuex 允許我們將 store 分割成模塊(module)。每個模塊擁有自己的 state、mutation、action、getter、甚至是嵌套子模塊。
- 將單一狀態樹劃分成多個模塊方便管理狀態
⑴. 模塊文件定義
1. 模塊 一
// store/moudules/cart.jsconst state = {} const getters = {} const mutations = {} const actions = {}export default {namespaced: true, // 開啟命名空間,防止多個模塊文件之間的命名沖突state,getters,mutations,actions }2. 模塊 二
// store/moudules/products.jsconst state = {products: [{ id: 1, title: 'iPhone 11', price: 8000 },{ id: 2, title: 'iPhone 12', price: 10000 }] } const getters = {} const mutations = {setProducts (state, payload) {state.products = payload} } const actions = {}export default {namespaced: true, // 開啟命名空間,防止多個模塊文件之間的命名沖突state,getters,mutations,actions }3. 模塊入口文件
// store/index.jsimport Vue from 'vue' import Vuex from 'vuex' import cart from './moudules/cart.js' import products form './moudules/products.js'Vue.use(Vuex)export default new Vuex.Store({state: {products: [{ id: 1, title: 'iPhone 11', price: 8000 },{ id: 2, title: 'iPhone 12', price: 10000 }]},mutations: {},actions: {},getters: {},modules: {products, // 模塊化后的文件cart} })⑵. 使用
<template><div><!-- 將模塊的狀態在入口文件的state中進行分塊管理,通過$store.state.模塊文件的名稱.模塊中的數據名即可訪問 --><!-- products為:{{$store.state.products.products}}<button @click="$store.commit('setProducts', [])">Mutation</button>-->products為:{{products}} <button @click="setProducts([])">Mutation</button></div> </template> <script>import { mapAction } from 'vuex'export default {methods: {...mapMutations('products', ['setProducts']) // 第一個參數是模塊名,第二個參數是mutation名},computed: {...mapState('products',['products']) // 第一個參數是模塊名,第二個參數是模塊中state中的成員}} </script>7. Vuex 嚴格模式
- 開啟嚴格模式后,在組件中直接修改 state 狀態,會拋出錯誤
- 組件中獲取到 $store.state.msg 對它進行修改,從語法層面這是沒有問題的,但這破壞了Vuex 的約定。如果在組件中直接修改 state,devtools 會無法跟蹤到這次狀態的修改
- 所有的狀態變更必須通過提交Mutation
開啟嚴格模式:
// store/index.jsexport default new Vuex.Store({strict: true... })開啟嚴格模式后直接在組件中修改state時會報錯(但數據仍然改變成功),如果不開啟就不會報錯
注意:
- 不要在生產環境下開啟嚴格模式因為嚴格模式會深度檢查狀態樹,來檢查不合規的狀態改變,會影響性能。
- 在開發環境中啟用嚴格模式,在生產中關閉。
所以Vuex實例時進行如下配置進行改良:
// store/index.jsexport default new Vuex.Store({strict: process.env.NODE_ENV !== 'production' // 在開發環境中啟用嚴格模式,在生產中關閉... })四、購物車案例
GitHub 項目地址
1. 目的
- 數據共享: 購物車內的數據為公共數據,能夠被所有組件調用
- 組件: 商品列表、購物車列表、購物車(鼠標懸停彈窗)
- 數據:
- 數據的存儲 -> 使用Vuex模塊化,存儲在Vuex的state中
- 數據的來源 -> 使用action發送異步請求,請求回數據后再提交mutation
- 數據的流向 -> 對應的組件從對應的vuex模塊中去拿數據
- 數據的操作 -> 都在vuex中的mutation中進行,并且通過map
2. 項目基礎結構
3. 路由配置
// src/router/index.jsimport Vue from 'vue' import VueRouter from 'vue-router' import Products from '../views/products.vue'Vue.use(VueRouter)const routes = [{path: '/',name: 'products',component: Products},{path: '/cart',name: 'cart',// route level code-splitting// this generates a separate chunk (about.[hash].js) for this route// which is lazy-loaded when the route is visited.component: () => import(/* webpackChunkName: "about" */ '../views/cart.vue')} ]const router = new VueRouter({routes })export default router4. 后端接口
// server.jsconst 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}/`) })5. Vuex
⑴. 入口文件
// src/store/index.jsimport Vue from 'vue' import Vuex from 'vuex' import products from './modules/products' import cart from './modules/cart'Vue.use(Vuex)// 插件需要在store前進行定義 // 使用vuex插件,讓這個插件在mutation執行結束后再執行 const myPlugin = store => {// subscribe方法監聽mutation的執行store.subscribe((mutation, state) => {// 這里的mutation格式為:{ type:'命名空間模塊名/mutation名', paylod:{參數} }// 判斷當前執行的mutation是否是cart模塊下的if (mutation.type.startsWith('cart/')) {// !!! 當cart模塊下的mutation中觸發時將數據存到本地(防止頁面刷新,導致 vuex 數據重置)window.localStorage.setItem('cart-products', JSON.stringify(state.cart.cartProducts))}}) }export default new Vuex.Store({state: {},mutations: {},actions: {},modules: {products,cart},plugins: [myPlugin] })⑵. 商品列表
// src/store/modules/products.jsimport axios from 'axios' // 記錄所有的商品數據 const state = {products: [] } const getters = {}// 在mutation中定義方法,修改商品數據 const mutations = {setProducts (state, payload) {state.products = payload} }// 在action中添加方法,異步請求商品數據 const actions = {async getProducts ({ commit }) { // 解構出context中的commit方法,否則后續得使用context.commit書寫比較麻煩const { data } = await axios({method: 'GET',url: 'http://127.0.0.1:3000/products'})// 服務端返回的數據結果結構:有id,title,price這三個屬性// const _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 }// ]commit('setProducts', data) // 當數據請求成功后修提交mutation修改state中的數據} }export default {namespaced: true,state,getters,mutations,actions }⑶. 購物車
// src/store/modules/cart.jsconst state = {// cartProducts :[] 這種形式的數組時寫在vuex中的,怕刷新// 改成從本地中拿去數據cartProducts: JSON.parse(window.localStorage.getItem('cart-products')) || [] }// 類似于計算屬性,這里將購物車中商品的屬性進行了一些操作再進行輸出,在組件中使用mapGetter進行映射后可直接使用,并且當數組發生變化時會重新進行計算getter中的操作 const getters = {// 統計商品總數,放到購物車右上角徽章圓點顯示那塊totalCount (state) {return state.cartProducts.reduce((sum, prod) => sum + prod.count, 0)},// 統計商品總價格,將購物車中的商品價格全部加一起(商品數量是在加入購物車處進行的處理,// 所以這里不是采用數量*單價的形式進行計算的,而是遍歷所有商品進行累加計算,這樣處理的原因是在添加購物車時已經將同樣的商品進行了求總價計算拿到totalPrice了)totalPrice (state) {return state.cartProducts.reduce((sum, prod) => sum + prod.totalPrice, 0)},checkedCount (state) {return state.cartProducts.reduce((sum, prod) => {if (prod.isChecked) {sum += prod.count}return sum}, 0)},checkedPrice (state) {return state.cartProducts.reduce((sum, prod) => { // reduce語法,reduce中的回調函數第一個參數sum是求和變量,會不斷累加。// 而prod是cartProducts數組的每一項,會被不斷遍歷加到sum上,sum會記錄上次的返回結果不斷進行累加if (prod.isChecked) {sum += prod.totalPrice}return sum}, 0) // 這個0是sum的初始值,如果不指定則會去數組的第一元素,第二個參數會從數組的第二項開始} }// 添加購物車,向購物車cartProducts數組中添加商品 const mutations = {// 將商品加入購物車的方法addToCart (state, product) {// 分為兩種情況// 1. cartProducts 中還沒有該商品,把該商品添加到數組,并增加 count,isChecked,totalPrice// 2. cartProducts 有該商品,讓商品的數量加1,選中,并重新計算小計const prod = state.cartProducts.find(item => item.id === product.id) // 使用數組的find方法來找當前購物車里面是否存在這個商品// 如果存在返回該商品,如果不存在返回undefinedif (prod) {prod.count++prod.isChecked = trueprod.totalPrice = prod.count * prod.price} else { // 購物車一開始肯定是空的,里面沒有商品,所以每件商品都會先經過else里面的操作進行處理,也就是在原有的三個屬性基礎上進行拓展,新增了count、isChecked、totalPrice屬性state.cartProducts.push({...product, count: 1,isChecked: true,totalPrice: product.price})}},// 刪除購物車中的商品的方法deleteFromCart (state, prodId) {// findIndex 是找到當前項在數組中的索引位置const index = state.cartProducts.findIndex(item => item.id === prodId)index !== -1 && state.cartProducts.splice(index, 1)},// 更新(改變)所有商品的選中狀態,調用時傳入參數checked,將checked(具體是啥調用時候出入)參數賦值給每一個商品的isCheckedupdateAllProductChecked (state, checked) {state.cartProducts.forEach(prod => {prod.isChecked = checked})},// 更新(改變)某一商品的選中狀態,調用時需要傳入一個對象作為參數(這個對象成員一般用插槽scope.row來獲取當前操作的項目),這里將商品對象中的屬性進行了解構updateProductChecked (state, {checked,prodId}) {const prod = state.cartProducts.find(prod => prod.id === prodId)prod && (prod.isChecked = checked)},// 通過文本框更新購物車中某個商品的數量以及商品的總價小計updateProduct (state, {prodId,count}) {const prod = state.cartProducts.find(prod => prod.id === prodId) // 找到對應的商品if (prod) {prod.count = count // 這個count是文本框中的值傳遞過來的prod.totalPrice = count * prod.price}} } const actions = {}export default {namespaced: true,state,getters,mutations,actions }6. 商品列表
// src/views/products.vue<template><div><el-breadcrumb separator="/"><el-breadcrumb-item><a href="#/">首頁</a></el-breadcrumb-item><el-breadcrumb-item><a href="#/">商品列表</a></el-breadcrumb-item></el-breadcrumb><el-table :data="products" style="width: 100%"><el-table-column prop="title" label="商品"></el-table-column><el-table-column prop="price" label="價格"></el-table-column><el-table-column prop="address" label="操作"><!-- <template slot-scope="scope"> --><template v-slot="scope"><el-button @click="addToCart(scope.row)">加入購物車</el-button></template></el-table-column></el-table></div> </template><script> import { mapState, mapActions, mapMutations } from 'vuex' export default {name: 'ProductList',computed: {...mapState('products', ['products']) // 從對應的vuex模塊中拿數據},methods: {...mapActions('products', ['getProducts']), // 異步獲取商品數據,觸發mutation...mapMutations('cart', ['addToCart'])},created () {this.getProducts()} } </script>7. 購物車列表
<template><div><el-breadcrumb separator="/"><el-breadcrumb-item :to="{ path: '/' }">首頁</el-breadcrumb-item><el-breadcrumb-item>購物車</el-breadcrumb-item></el-breadcrumb><el-table :data="cartProducts" style="width: 100%" ><el-table-column width="55"><template v-slot:header><!-- checkedAll是個計算屬性,并對表單控件checkbox的值進行了雙向數據綁定,由于只在當前組件中使用,所以不再定義到vuex中 --><el-checkbox v-model="checkedAll" size="mini"></el-checkbox></template><!--@change="updateProductChecked" 默認參數:更新后的值@change="updateProductChecked(productId, $event)" 123, 原來那個默認參數當你傳遞了自定義參數,也就是下面的這個對象的時候,如果還想得到原來那個默認參數(change觸發時會默認傳遞改變后的checkbox的value值),就手動傳遞一個 $event,這個$event就是true或者false用來表示點擊checkbox狀態改變后的value值--><template v-slot="scope"><el-checkboxsize="mini":value="scope.row.isChecked"@change="updateProductChecked({prodId: scope.row.id,checked: $event})"></el-checkbox></template></el-table-column><el-table-column prop="title" label="商品"></el-table-column><el-table-column prop="price" label="單價"></el-table-column><el-table-column prop="count" label="數量"><template v-slot="scope"><el-input-number :value="scope.row.count" @change="updateProduct({prodId: scope.row.id,count: $event })" size="mini"></el-input-number><!-- $event就是文本框中的值 --></template></el-table-column><el-table-column prop="totalPrice" label="小計"></el-table-column><el-table-column label="操作"><template><el-button size="mini">刪除</el-button></template></el-table-column></el-table><div><p>已選 <span>{{ checkedCount }}</span> 件商品,總價:<span>{{ checkedPrice }}</span></p><el-button type="danger">結算</el-button></div></div> </template><script> import { mapState, mapMutations, mapGetters } from 'vuex' export default {name: 'Cart',computed: {...mapState('cart', ['cartProducts']),...mapGetters('cart', ['checkedCount', 'checkedPrice']),// 計算屬性中可以定義get和setcheckedAll: {get () {return this.cartProducts.every(prod => prod.isChecked) // 有一個沒選中就是false,全選中了才是true},set (value) {this.updateAllProductChecked(value) // 將全選box設置為value,value是用戶調用時傳入的}}},methods: {...mapMutations('cart', ['updateAllProductChecked', // 改變所有商品的checkebox狀態屬性'updateProductChecked', // 改變某個商品的checkebox狀態屬性'updateProduct'])} } </script>8. 購物車彈窗
// src/components/pop-carts.vue<template><el-popover width="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>五、模擬 Vuex 實現
替換 vuex 為 myvuex:
// src/store/index.jsimport Vuex from '../myvuex'模擬 Vuex:
// src/myvuex/index.jslet _Vue = nullclass Store {constructor (options) {const {state = {},getters = {},mutations = {},actions = {}} = optionsthis.state = _Vue.observable(state)this.getters = Object.create(null)Object.keys(getters).forEach(key => {Object.defineProperty(this.getters, key, {get: () => getters[key](state)})})this._mutaions = mutationsthis._actions = actions}commit (type, payload) {this._mutaions[type](this.state, payload)}dispatch (type, payload) {this._actions[type](this, payload)} }function install (Vue) {_Vue = Vue_Vue.mixin({beforeCreate () {if (this.$options.store) {_Vue.prototype.$store = this.$options.store}}}) }export default {Store,install }下一篇:服務端渲染概念
總結
- 上一篇: JAVA SE (14)
- 下一篇: Vuex 4源码学习笔记 - 通过Vue