Vuex深度解析
我希望有個(gè)如你一般的人。
如山間清爽的風(fēng),如古城溫暖的光。
從清晨到夜晚,由山野到書房。
只要最后是你,就好。
一、組件通信方式
1. 組件內(nèi)的狀態(tài)管理流程
組件化開發(fā)優(yōu)點(diǎn): 更快的開發(fā)效率、更好的可維護(hù)性
此處數(shù)據(jù)的流向是單向的,State狀態(tài)就是我們所說的數(shù)據(jù),數(shù)據(jù)綁定到視圖展示給用戶,當(dāng)用戶和視圖交互,通過Actions更改數(shù)據(jù)之后,更改后的數(shù)據(jù)重新綁定到視圖。
狀態(tài)管理包含以下幾部分:
- state(狀態(tài)): 驅(qū)動(dòng)應(yīng)用的數(shù)據(jù)源
- view(視圖): 以聲明方式將 state 映射到視圖
- actions(行為): 響應(yīng)在 view 上的用戶輸入導(dǎo)致的狀態(tài)變化
2. 組件間傳值方式
在 Vue 中為不同的組件關(guān)系提供了不同的通信規(guī)則,組件間傳值可分為:父傳子、子傳父、不相關(guān)組件之間
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. 不相關(guān)組件傳值(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: {// 文本框默認(rèn)顯示的商品個(gè)數(shù)num: Number},// 因?yàn)閜rops的值不建議直接修改,將props數(shù)據(jù)存儲(chǔ)到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 標(biāo)簽上,則獲取到的是 DOM 對象
- 如果你把它作用到組件標(biāo)簽上,則獲取到的是組件實(shí)例對象
$refs 只會(huì)在組件渲染完成之后生效,并且它們不是響應(yīng)式的。
這僅作為一個(gè)用于直接操作子組件的一種捷徑,盡量避免在模板或計(jì)算屬性中訪問 $refs。
濫用會(huì)導(dǎo)致數(shù)據(jù)管理的混亂。
子組件:
<template><div><h1>ref Child</h1><input ref="input" type="text" v-model="value"> // 在HTML標(biāo)簽上使用</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. 簡易的狀態(tài)管理方案
⑴. 狀態(tài)管理遇到的問題:
- 多個(gè)視圖依賴同一狀態(tài)
- 來自不同視圖的行為需要變更同一狀態(tài)
因此,需要把組件的的共享狀態(tài)抽取出來(將來使用時(shí)保證其為響應(yīng)式的),不管樹在哪個(gè)位置,任何組件都能獲取狀態(tài)或者觸發(fā)行為。
⑵. 實(shí)現(xiàn)一個(gè)簡易的狀態(tài)集中管理:
1. 創(chuàng)建:
首先創(chuàng)建一個(gè)共享的倉庫 store 對象,這是集中式的狀態(tài)管理,所有狀態(tài)都在 store 中進(jìn)行管理,且它為全局唯一的對象,任意的組件都可以導(dǎo)入 store 模塊使用其中的狀態(tài),更改狀態(tài)也是在該模塊中實(shí)現(xiàn)的:
// 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. 儲(chǔ)存:
把共享的倉庫 store 對象,存儲(chǔ)到需要共享狀態(tài)的組件 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: {// 點(diǎn)擊按鈕的時(shí)候通過 action 修改狀態(tài)change () {store.setUserNameAction('componentA')}},data () {return {// 當(dāng)前組件特有的自己的狀態(tài),存儲(chǔ)到privateStateprivateState: {},// 把store中的state(共享的狀態(tài))存儲(chǔ)到sharedStatesharedState: store.state}} } </script>3. 修改:
componentA 和 componentB 兩個(gè)組件共享了 store 中的狀態(tài),并且和用戶交互的時(shí)候還會(huì)更改狀態(tài)中的 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>采用集中式的狀態(tài)管理,使用了全局唯一的對象 store 來存儲(chǔ)狀態(tài),并且有一個(gè)共同點(diǎn)約定:
組件不允許直接變更屬于 store 對象的 State,而應(yīng)執(zhí)行 Action 來分發(fā)(dispatch)事件通知 store 去改變,這樣最終的樣子跟 Vuex 的結(jié)構(gòu)就類似了。
這樣好處是,能夠記錄所有 store 中發(fā)生的 State 變更,同時(shí)實(shí)現(xiàn)能做到記錄變更、保存狀態(tài)快照、歷史回滾 / 時(shí)光旅行的調(diào)試工具。
二、Vuex 核心概念
1. 概述
官方文檔:Vuex 是一個(gè)專為 Vue.js 應(yīng)用程序開發(fā)的狀態(tài)管理模式。它采用集中式存儲(chǔ)管理應(yīng)用的所有組件 的狀態(tài),并以相應(yīng)的規(guī)則保證狀態(tài)以一種可預(yù)測的方式發(fā)生變化。Vuex 也集成到 Vue 的官方調(diào) 試工具 devtools extension,提供了諸如零配置的 time-travel 調(diào)試、狀態(tài)快照導(dǎo)入導(dǎo)出等高級調(diào) 試功能。
- Vuex 是專門為 Vue.js 設(shè)計(jì)的狀態(tài)管理庫
- Vuex 采用集中式的方式存儲(chǔ)需要共享的數(shù)據(jù)
- 從使用角度,Vuex 就是一個(gè) JavaScript 庫
- Vuex 的作用是進(jìn)行狀態(tài)管理,解決復(fù)雜組件通信,數(shù)據(jù)共享
- Vuex 集成到了 devtools 中,提供了 time-travel 時(shí)光旅行和歷史回滾的功能
什么情況下使用 Vuex ?
- 非必要不使用 Vuex: Vuex 可以幫助我們管理共享狀態(tài),并附帶了更多的概念和框架。這需要對短期和長期效益進(jìn)行權(quán)衡。
- 大型的單頁應(yīng)用程序: 多個(gè)視圖依賴同一狀態(tài)(如購物車)
- 注意: Vuex 不要濫用,不符合以上需求的業(yè)務(wù)不要使用,反而會(huì)讓你的應(yīng)用變得更麻煩。
2. 工作流程
- Store: 倉庫,是使用Vuex應(yīng)用程序的核心,每一個(gè)應(yīng)用僅有一個(gè) Store。Store 是一個(gè)容器,包含應(yīng)用中的大部分狀態(tài),不能直接改變 Store 中的狀態(tài),要通過提交 Mutation 的方式
- State: 狀態(tài)保存至 Store 中,因?yàn)?Store 是唯一的,因此狀態(tài)也是唯一的,稱為單一狀態(tài)樹。但是如果所有的狀態(tài)都保存在 State 中,程序則難以維護(hù),可以通過后續(xù)的模塊來解決該問題。注意,這里的狀態(tài)時(shí)響應(yīng)式的
- Getter: 像是 Vue 中的計(jì)算屬性,對 state 中的數(shù)據(jù)進(jìn)行加工,方便從一個(gè)屬性派生出其他的值。它內(nèi)部可以對計(jì)算的結(jié)果進(jìn)行緩存,只有當(dāng)依賴的狀態(tài)發(fā)生改變時(shí)才會(huì)重新計(jì)算
- Mutation: 狀態(tài)的變化必須通過提交 Mutation 來完成 (同步)
- Action: 和 Mutation 類似,不同的是 Action 可以進(jìn)行異步操作,內(nèi)部改變狀態(tài)的時(shí)候都需要提交 Mutation
- Module: 模塊,由于使用單一狀態(tài)樹,應(yīng)用的所有狀態(tài)會(huì)集中到一個(gè)比較大的對象上來,當(dāng)應(yīng)用變得非常復(fù)雜時(shí),Store對象就有可能變得非常臃腫。為了解決這個(gè)問題,Vuex允許我們將 Store 分割成模塊每個(gè)模塊擁有自己的 State 、Mutation、Action、Getter甚至是嵌套的子模塊
三、Vuex 基本使用
1. 基本結(jié)構(gòu)
// store/index.jsimport Vue from 'vue' import Vuex from 'vuex'Vue.use(Vuex)export default new Vuex.Store({state: {},// 注意單詞復(fù)數(shù)!!mutations和actions都得加s!!!mutations: {},actions: {},modules: {} })創(chuàng)建 Vue 實(shí)例的時(shí)候傳入 store 選項(xiàng),這個(gè) store 選項(xiàng)會(huì)被注入到 Vue 實(shí)例中,在組件中使用到的 this.$store 就是在這個(gè)位置注入的。
// main.jsimport store from './store'new Vue({router,store,render: h => h(App) }).$mount('#app')2. State
Vuex 是單一狀態(tài)樹,并且是響應(yīng)式的,用一個(gè)對象就包含了全部的應(yīng)用層級狀態(tài)
⑴. 定義狀態(tài)
// 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: {} })⑵. 常規(guī)使用
<template><div><h1>test</h1>count: {{$store.state.count}}msg: {{$store.state.msg}}</div> </template>⑶. 使用mapState轉(zhuǎn)化為計(jì)算屬性來使用
<template><div><h1>test</h1>count: {{count}}msg: {{msg}}</div> </template> <script>import { mapState } from 'vuex'export default {computed: {// 以數(shù)組的形式來接收參數(shù)// ...mapState(['count', 'msg'])// 這里也可以使用對象的形式來接收數(shù)據(jù)并進(jìn)行重命名,防止和組件中原有的數(shù)據(jù)沖突...mapState({num:'count', message: 'msg'})// mapState 會(huì)返回一個(gè)對象,其包含兩個(gè)計(jì)算屬性對應(yīng)的方法// count : state => state.count // msg : state => state.msg// 計(jì)算屬性使用時(shí),內(nèi)部本來就是接收屬性名對應(yīng)的帶有返回值的方法的鍵值對的形式}} </script>3. Getter
Getter 就是 store 中的計(jì)算屬性,使用 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數(shù)據(jù),將處理結(jié)果返回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: {// 同樣也有兩種接收方式,一種是數(shù)組,一種是對象// ...mapGetter(['reverseMsg']),// 重命名,可以在模板中使用 reverse來使用數(shù)據(jù)...mapGetter({reverse: 'reverseMsg'})}} </script>4. Mutation
更改 Vuex 的 store 中的狀態(tài)的唯一方法是提交 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) { // 參數(shù)含義:payload表示傳遞的參數(shù)state.count += payload},actions: {},getters: {},modules: {} })⑵. 使用
<template><div><!--如果不使用mapMutation的話需要借助commit來觸發(fā)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本質(zhì)上是方法,所以可以映射到方法中methods: {// 以數(shù)組方式傳參// ...mapMutations(['increate']),返回的是個(gè)映射的方法,但是不再是計(jì)算屬性而是對應(yīng)的函數(shù)// 以對象形式傳參來解決重名的問題...mapMutations({increateMut: 'increate'})}} </script>5. Action
Action 類似于 mutation,不同在于:
- Action 提交的是 mutation,而不是直接變更狀態(tài)
- 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) { // 參數(shù)含義:payload表示傳遞的參數(shù)state.count += payload},actions: {increateAsync (context, payload) {setTimeout(() => {context.commit('increate', payload)}, 2000)}},getters: {},modules: {} })⑵. 使用
<template><div><!--如果不使用mapAction的話需要借助dispatch來觸發(fā)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: {// 以數(shù)組方式傳參// ...mapActions(['increateAsync']),返回的是個(gè)映射的方法,但是不再是計(jì)算屬性而是對應(yīng)的函數(shù)// 以對象形式傳參來解決重名的問題...mapMutations({increateA: 'increateAsync'})}} </script>6. Module
- 由于使用單一狀態(tài)樹,應(yīng)用的所有狀態(tài)會(huì)集中到一個(gè)比較大的對象。當(dāng)應(yīng)用變得非常復(fù)雜時(shí),store 對象就有可能變得相當(dāng)臃腫。
- 為了解決以上問題,Vuex 允許我們將 store 分割成模塊(module)。每個(gè)模塊擁有自己的 state、mutation、action、getter、甚至是嵌套子模塊。
- 將單一狀態(tài)樹劃分成多個(gè)模塊方便管理狀態(tài)
⑴. 模塊文件定義
1. 模塊 一
// store/moudules/cart.jsconst state = {} const getters = {} const mutations = {} const actions = {}export default {namespaced: true, // 開啟命名空間,防止多個(gè)模塊文件之間的命名沖突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, // 開啟命名空間,防止多個(gè)模塊文件之間的命名沖突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><!-- 將模塊的狀態(tài)在入口文件的state中進(jìn)行分塊管理,通過$store.state.模塊文件的名稱.模塊中的數(shù)據(jù)名即可訪問 --><!-- 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']) // 第一個(gè)參數(shù)是模塊名,第二個(gè)參數(shù)是mutation名},computed: {...mapState('products',['products']) // 第一個(gè)參數(shù)是模塊名,第二個(gè)參數(shù)是模塊中state中的成員}} </script>7. Vuex 嚴(yán)格模式
- 開啟嚴(yán)格模式后,在組件中直接修改 state 狀態(tài),會(huì)拋出錯(cuò)誤
- 組件中獲取到 $store.state.msg 對它進(jìn)行修改,從語法層面這是沒有問題的,但這破壞了Vuex 的約定。如果在組件中直接修改 state,devtools 會(huì)無法跟蹤到這次狀態(tài)的修改
- 所有的狀態(tài)變更必須通過提交Mutation
開啟嚴(yán)格模式:
// store/index.jsexport default new Vuex.Store({strict: true... })開啟嚴(yán)格模式后直接在組件中修改state時(shí)會(huì)報(bào)錯(cuò)(但數(shù)據(jù)仍然改變成功),如果不開啟就不會(huì)報(bào)錯(cuò)
注意:
- 不要在生產(chǎn)環(huán)境下開啟嚴(yán)格模式因?yàn)閲?yán)格模式會(huì)深度檢查狀態(tài)樹,來檢查不合規(guī)的狀態(tài)改變,會(huì)影響性能。
- 在開發(fā)環(huán)境中啟用嚴(yán)格模式,在生產(chǎn)中關(guān)閉。
所以Vuex實(shí)例時(shí)進(jìn)行如下配置進(jìn)行改良:
// store/index.jsexport default new Vuex.Store({strict: process.env.NODE_ENV !== 'production' // 在開發(fā)環(huán)境中啟用嚴(yán)格模式,在生產(chǎn)中關(guān)閉... })四、購物車案例
GitHub 項(xiàng)目地址
1. 目的
- 數(shù)據(jù)共享: 購物車內(nèi)的數(shù)據(jù)為公共數(shù)據(jù),能夠被所有組件調(diào)用
- 組件: 商品列表、購物車列表、購物車(鼠標(biāo)懸停彈窗)
- 數(shù)據(jù):
- 數(shù)據(jù)的存儲(chǔ) -> 使用Vuex模塊化,存儲(chǔ)在Vuex的state中
- 數(shù)據(jù)的來源 -> 使用action發(fā)送異步請求,請求回?cái)?shù)據(jù)后再提交mutation
- 數(shù)據(jù)的流向 -> 對應(yīng)的組件從對應(yīng)的vuex模塊中去拿數(shù)據(jù)
- 數(shù)據(jù)的操作 -> 都在vuex中的mutation中進(jìn)行,并且通過map
2. 項(xiàng)目基礎(chǔ)結(jié)構(gòu)
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前進(jìn)行定義 // 使用vuex插件,讓這個(gè)插件在mutation執(zhí)行結(jié)束后再執(zhí)行 const myPlugin = store => {// subscribe方法監(jiān)聽mutation的執(zhí)行store.subscribe((mutation, state) => {// 這里的mutation格式為:{ type:'命名空間模塊名/mutation名', paylod:{參數(shù)} }// 判斷當(dāng)前執(zhí)行的mutation是否是cart模塊下的if (mutation.type.startsWith('cart/')) {// !!! 當(dāng)cart模塊下的mutation中觸發(fā)時(shí)將數(shù)據(jù)存到本地(防止頁面刷新,導(dǎo)致 vuex 數(shù)據(jù)重置)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' // 記錄所有的商品數(shù)據(jù) const state = {products: [] } const getters = {}// 在mutation中定義方法,修改商品數(shù)據(jù) const mutations = {setProducts (state, payload) {state.products = payload} }// 在action中添加方法,異步請求商品數(shù)據(jù) const actions = {async getProducts ({ commit }) { // 解構(gòu)出context中的commit方法,否則后續(xù)得使用context.commit書寫比較麻煩const { data } = await axios({method: 'GET',url: 'http://127.0.0.1:3000/products'})// 服務(wù)端返回的數(shù)據(jù)結(jié)果結(jié)構(gòu):有id,title,price這三個(gè)屬性// 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) // 當(dāng)數(shù)據(jù)請求成功后修提交mutation修改state中的數(shù)據(jù)} }export default {namespaced: true,state,getters,mutations,actions }⑶. 購物車
// src/store/modules/cart.jsconst state = {// cartProducts :[] 這種形式的數(shù)組時(shí)寫在vuex中的,怕刷新// 改成從本地中拿去數(shù)據(jù)cartProducts: JSON.parse(window.localStorage.getItem('cart-products')) || [] }// 類似于計(jì)算屬性,這里將購物車中商品的屬性進(jìn)行了一些操作再進(jìn)行輸出,在組件中使用mapGetter進(jìn)行映射后可直接使用,并且當(dāng)數(shù)組發(fā)生變化時(shí)會(huì)重新進(jìn)行計(jì)算getter中的操作 const getters = {// 統(tǒng)計(jì)商品總數(shù),放到購物車右上角徽章圓點(diǎn)顯示那塊totalCount (state) {return state.cartProducts.reduce((sum, prod) => sum + prod.count, 0)},// 統(tǒng)計(jì)商品總價(jià)格,將購物車中的商品價(jià)格全部加一起(商品數(shù)量是在加入購物車處進(jìn)行的處理,// 所以這里不是采用數(shù)量*單價(jià)的形式進(jìn)行計(jì)算的,而是遍歷所有商品進(jìn)行累加計(jì)算,這樣處理的原因是在添加購物車時(shí)已經(jīng)將同樣的商品進(jìn)行了求總價(jià)計(jì)算拿到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中的回調(diào)函數(shù)第一個(gè)參數(shù)sum是求和變量,會(huì)不斷累加。// 而prod是cartProducts數(shù)組的每一項(xiàng),會(huì)被不斷遍歷加到sum上,sum會(huì)記錄上次的返回結(jié)果不斷進(jìn)行累加if (prod.isChecked) {sum += prod.totalPrice}return sum}, 0) // 這個(gè)0是sum的初始值,如果不指定則會(huì)去數(shù)組的第一元素,第二個(gè)參數(shù)會(huì)從數(shù)組的第二項(xiàng)開始} }// 添加購物車,向購物車cartProducts數(shù)組中添加商品 const mutations = {// 將商品加入購物車的方法addToCart (state, product) {// 分為兩種情況// 1. cartProducts 中還沒有該商品,把該商品添加到數(shù)組,并增加 count,isChecked,totalPrice// 2. cartProducts 有該商品,讓商品的數(shù)量加1,選中,并重新計(jì)算小計(jì)const prod = state.cartProducts.find(item => item.id === product.id) // 使用數(shù)組的find方法來找當(dāng)前購物車?yán)锩媸欠翊嬖谶@個(gè)商品// 如果存在返回該商品,如果不存在返回undefinedif (prod) {prod.count++prod.isChecked = trueprod.totalPrice = prod.count * prod.price} else { // 購物車一開始肯定是空的,里面沒有商品,所以每件商品都會(huì)先經(jīng)過else里面的操作進(jìn)行處理,也就是在原有的三個(gè)屬性基礎(chǔ)上進(jìn)行拓展,新增了count、isChecked、totalPrice屬性state.cartProducts.push({...product, count: 1,isChecked: true,totalPrice: product.price})}},// 刪除購物車中的商品的方法deleteFromCart (state, prodId) {// findIndex 是找到當(dāng)前項(xiàng)在數(shù)組中的索引位置const index = state.cartProducts.findIndex(item => item.id === prodId)index !== -1 && state.cartProducts.splice(index, 1)},// 更新(改變)所有商品的選中狀態(tài),調(diào)用時(shí)傳入?yún)?shù)checked,將checked(具體是啥調(diào)用時(shí)候出入)參數(shù)賦值給每一個(gè)商品的isCheckedupdateAllProductChecked (state, checked) {state.cartProducts.forEach(prod => {prod.isChecked = checked})},// 更新(改變)某一商品的選中狀態(tài),調(diào)用時(shí)需要傳入一個(gè)對象作為參數(shù)(這個(gè)對象成員一般用插槽scope.row來獲取當(dāng)前操作的項(xiàng)目),這里將商品對象中的屬性進(jìn)行了解構(gòu)updateProductChecked (state, {checked,prodId}) {const prod = state.cartProducts.find(prod => prod.id === prodId)prod && (prod.isChecked = checked)},// 通過文本框更新購物車中某個(gè)商品的數(shù)量以及商品的總價(jià)小計(jì)updateProduct (state, {prodId,count}) {const prod = state.cartProducts.find(prod => prod.id === prodId) // 找到對應(yīng)的商品if (prod) {prod.count = count // 這個(gè)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="價(jià)格"></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']) // 從對應(yīng)的vuex模塊中拿數(shù)據(jù)},methods: {...mapActions('products', ['getProducts']), // 異步獲取商品數(shù)據(jù),觸發(fā)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是個(gè)計(jì)算屬性,并對表單控件checkbox的值進(jìn)行了雙向數(shù)據(jù)綁定,由于只在當(dāng)前組件中使用,所以不再定義到vuex中 --><el-checkbox v-model="checkedAll" size="mini"></el-checkbox></template><!--@change="updateProductChecked" 默認(rèn)參數(shù):更新后的值@change="updateProductChecked(productId, $event)" 123, 原來那個(gè)默認(rèn)參數(shù)當(dāng)你傳遞了自定義參數(shù),也就是下面的這個(gè)對象的時(shí)候,如果還想得到原來那個(gè)默認(rèn)參數(shù)(change觸發(fā)時(shí)會(huì)默認(rèn)傳遞改變后的checkbox的value值),就手動(dòng)傳遞一個(gè) $event,這個(gè)$event就是true或者false用來表示點(diǎn)擊checkbox狀態(tài)改變后的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="單價(jià)"></el-table-column><el-table-column prop="count" label="數(shù)量"><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="小計(jì)"></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> 件商品,總價(jià):<span>{{ checkedPrice }}</span></p><el-button type="danger">結(jié)算</el-button></div></div> </template><script> import { mapState, mapMutations, mapGetters } from 'vuex' export default {name: 'Cart',computed: {...mapState('cart', ['cartProducts']),...mapGetters('cart', ['checkedCount', 'checkedPrice']),// 計(jì)算屬性中可以定義get和setcheckedAll: {get () {return this.cartProducts.every(prod => prod.isChecked) // 有一個(gè)沒選中就是false,全選中了才是true},set (value) {this.updateAllProductChecked(value) // 將全選box設(shè)置為value,value是用戶調(diào)用時(shí)傳入的}}},methods: {...mapMutations('cart', ['updateAllProductChecked', // 改變所有商品的checkebox狀態(tài)屬性'updateProductChecked', // 改變某個(gè)商品的checkebox狀態(tài)屬性'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="價(jià)格"></el-table-column><el-table-column property="count" width="50" label="數(shù)量"></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 }} 件商品 共計(jì)¥{{ 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 實(shí)現(xiàn)
替換 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 }下一篇:服務(wù)端渲染概念
總結(jié)
- 上一篇: JAVA SE (14)
- 下一篇: Vuex 4源码学习笔记 - 通过Vue