本文整理來自深入Vue3+TypeScript技術(shù)棧-coderwhy大神新課,只作為個(gè)人筆記記錄使用,請(qǐng)大家多支持王紅元老師紧阔。
什么是狀態(tài)管理
在開發(fā)中辰狡,我們會(huì)的應(yīng)用程序需要處理各種各樣的數(shù)據(jù)涣澡,這些數(shù)據(jù)需要保存在我們應(yīng)用程序中的某一個(gè)位置纠脾,對(duì)于這些數(shù)據(jù)的管理我們就稱之為狀態(tài)管理锤窑。
在前面我們是如何管理自己的狀態(tài)呢?
- 在Vue開發(fā)中大诸,我們使用組件化的開發(fā)方式,而在組件中我們定義data或者在setup中返回使用的數(shù)據(jù)贯卦,這些數(shù)據(jù)我們稱之為state资柔;
- 在模塊template中我們可以使用這些數(shù)據(jù),模塊最終會(huì)被渲染成DOM撵割,我們稱之為View贿堰;
- 在模塊中我們會(huì)產(chǎn)生一些行為事件,處理這些行為事件時(shí)啡彬,有可能會(huì)修改state羹与,這些行為事件我們稱之為actions;
復(fù)雜的狀態(tài)管理
JavaScript需要管理的狀態(tài)越來越多庶灿,越來越復(fù)雜纵搁,這些狀態(tài)包括服務(wù)器返回的數(shù)據(jù)、緩存數(shù)據(jù)往踢、用戶操作產(chǎn)生的數(shù)據(jù)等等腾誉,也包括一些UI的狀態(tài),比如某些元素是否被選中峻呕,是否顯示加載動(dòng)效利职,當(dāng)前分頁。
當(dāng)我們的應(yīng)用遇到多個(gè)組件共享狀態(tài)時(shí)瘦癌,單向數(shù)據(jù)流的簡潔性很容易被破壞猪贪,因?yàn)槎鄠€(gè)視圖依賴于同一狀態(tài)佩憾,來自不同視圖的行為需要變更同一狀態(tài)哮伟。
我們是否可以通過組件數(shù)據(jù)的傳遞來完成呢干花?
對(duì)于一些簡單的狀態(tài),確實(shí)可以通過props的傳遞或者Provide的方式來共享狀態(tài)楞黄,但是對(duì)于復(fù)雜的狀態(tài)管理來說池凄,顯然單純通過傳遞和共享的方式是不足以解決問題的,比如兄弟組件如何共享數(shù)據(jù)呢鬼廓?
Vuex的狀態(tài)管理
管理不斷變化的state本身是非常困難的肿仑,因?yàn)闋顟B(tài)之間相互會(huì)存在依賴,一個(gè)狀態(tài)的變化會(huì)引起另一個(gè)狀態(tài)的變化碎税,View頁面也有可能會(huì)引起狀態(tài)的變化尤慰。當(dāng)應(yīng)用程序復(fù)雜時(shí),state在什么時(shí)候雷蹂,因?yàn)槭裁丛蚨l(fā)生了變化伟端,發(fā)生了怎么樣的變化,會(huì)變得非常難以控制和追蹤匪煌。
因此责蝠,我們是否可以考慮將組件的內(nèi)部狀態(tài)抽離出來,以一個(gè)全局單例的方式來管理呢萎庭?
在這種模式下霜医,我們的組件樹構(gòu)成了一個(gè)巨大的 “視圖View”,不管在樹的哪個(gè)位置驳规,任何組件都能獲取狀態(tài)或者觸發(fā)行為肴敛,通過定義和隔離狀態(tài)管理中的各個(gè)概念,并通過強(qiáng)制性的規(guī)則來維護(hù)視圖和狀態(tài)間的獨(dú)立性吗购,我們的代碼邊會(huì)變得更加結(jié)構(gòu)化和易于維護(hù)医男、跟蹤。
這就是Vuex背后的基本思想巩搏,它借鑒了Flux昨登、Redux、Elm(純函數(shù)語言贯底,redux有借鑒它的思想)丰辣。
Vuex有五大核心:state、getters禽捆、mutations笙什、actions、modules胚想,下面我們慢慢講琐凭。
Vuex的安裝
首先第一步需要安裝vuex,我們這里使用的是vuex4.x浊服,安裝的時(shí)候需要添加 next 指定版本统屈。
npm install vuex@next
創(chuàng)建Store
每一個(gè)Vuex應(yīng)用的核心就是store(倉庫)胚吁,store本質(zhì)上是一個(gè)容器,它包含著你的應(yīng)用中大部分的狀態(tài)(state)愁憔。
Vuex和單純的全局對(duì)象有什么區(qū)別呢腕扶?
第一:Vuex的狀態(tài)存儲(chǔ)是響應(yīng)式的。當(dāng)Vue組件從store中讀取狀態(tài)的時(shí)候吨掌,若store中的狀態(tài)發(fā)生變化半抱,那么相應(yīng)的組件也會(huì)被更新。
第二:你不能直接改變store中的狀態(tài)膜宋。改變store中的狀態(tài)的唯一途徑就是提交 (commit) mutation窿侈,這樣使得我們可以方便的跟蹤每一個(gè)狀態(tài)的變化,從而讓我們能夠通過一些工具幫助我們更好的管理應(yīng)用的狀態(tài)秋茫。
使用步驟:
① 創(chuàng)建Store對(duì)象史简;
② 在app中通過插件安裝;
組件中使用store
在組件中使用store学辱,我們按照如下的方式:
- 在模板中使用乘瓤;
- 在options api中使用环形,比如computed策泣;
- 在setup中使用;
Vue devtool
vue其實(shí)提供了一個(gè)devtools抬吟,方便我們對(duì)組件或者vuex進(jìn)行調(diào)試萨咕。我們需要安裝beta版本支持vue3,目前是6.0.0 beta15火本。
有兩種常見的安裝方式:
方式一:通過chrome的商店危队;
方式二:手動(dòng)下載代碼,編譯钙畔、安裝茫陆;
由于某些原因我們可能不能正常登錄Chrome商店,所以可以選擇第二種擎析;
手動(dòng)安裝devtool
手動(dòng)下載代碼簿盅,編譯、安裝:
- https://github.com/vuejs/devtools/tree/v6.0.0-beta.15下載代碼揍魂;
- 打開項(xiàng)目桨醋,終端執(zhí)行 yarn install 安裝相關(guān)的依賴;
- 終端執(zhí)行 yarn run build 打包现斋;
- 打開瀏覽器喜最,點(diǎn)擊加載已解壓的擴(kuò)展程序;
- 選擇剛才項(xiàng)目中的shell-chrome文件夾庄蹋,點(diǎn)擊選擇即可安裝成功瞬内;
單一狀態(tài)樹
Vuex 使用單一狀態(tài)樹迷雪,用一個(gè)對(duì)象就包含了全部的應(yīng)用層級(jí)的狀態(tài),采用的是SSOT虫蝶,Single Source of Truth振乏,也可以翻譯成單一數(shù)據(jù)源,這也意味著秉扑,每個(gè)應(yīng)用將僅僅包含一個(gè) store 實(shí)例慧邮,單狀態(tài)樹和模塊化并不沖突,后面我們會(huì)講到module的概念舟陆。
單一狀態(tài)樹的優(yōu)勢(shì):如果你的狀態(tài)信息是保存到多個(gè)Store對(duì)象中的误澳,那么之后的管理和維護(hù)等等都會(huì)變得特別困難,所以Vuex也使用了單一狀態(tài)樹來管理應(yīng)用層級(jí)的全部狀態(tài)秦躯。單一狀態(tài)樹能夠讓我們最直接的方式找到某個(gè)狀態(tài)的片段忆谓,而且在之后的維護(hù)和調(diào)試過程中,也可以非常方便的管理和維護(hù)踱承。
Vuex的簡單使用
和使用vue-router一樣倡缠,我們需要?jiǎng)?chuàng)建一個(gè)store文件夾,然后在store文件夾里創(chuàng)建一個(gè)index.js文件茎活,在index.js文件里面將store對(duì)象導(dǎo)出昙沦。然后在main.js文件里面使用store。
main.js文件代碼如下:
import { createApp } from 'vue'
import App from './App.vue'
//導(dǎo)入創(chuàng)建的store
import store from './store'
//使用store
createApp(App).use(store).mount('#app')
index.js代碼如下:
import { createStore } from "vuex"
const store = createStore({
// 保存的數(shù)據(jù)
// state是個(gè)函數(shù)载荔,返回一個(gè)對(duì)象盾饮,和data類似
// 以前是個(gè)對(duì)象,現(xiàn)在我們推薦寫成一個(gè)函數(shù)
state() {
return {
counter: 100,
name: "why",
age: 18,
height: 1.88
}
},
// 對(duì)數(shù)據(jù)進(jìn)行一些加工
getters: {
doubleCounter(state) {
return state.counter * 2
}
},
//修改數(shù)據(jù)
mutations: {
increment(state) {
state.counter++
}
}
});
export default store;
使用數(shù)據(jù)和修改數(shù)據(jù):
//使用vuex中的數(shù)據(jù)
<h2>App:{{ $store.state.counter }}</h2>
//通過commit調(diào)用mutations中的increment方法懒熙,從而修改數(shù)據(jù)
this.$store.commit("increment")
獲取組件狀態(tài):state
在前面我們已經(jīng)學(xué)習(xí)過如何在組件中獲取狀態(tài)了丘损,當(dāng)然,如果覺得那種方式有點(diǎn)繁瑣(表達(dá)式過長)工扎,我們可以使用計(jì)算屬性徘钥。
但是,如果我們有很多個(gè)狀態(tài)都需要獲取話肢娘,一個(gè)一個(gè)寫計(jì)算屬性會(huì)很麻煩呈础,我們可以使用mapState輔助函數(shù)。mapState接收的參數(shù)可以是數(shù)組類型或者對(duì)象類型蔬浙。我們也可以使用展開運(yùn)算符和來原有的computed混合在一起猪落。
在computed中使用mapState
<template>
<div>
<h2>Home:{{ $store.state.counter }}</h2>
<h2>Home:{{ sCounter }}</h2>
<h2>Home:{{ sName }}</h2>
<!-- <h2>Home:{{ age }}</h2>
<h2>Home:{{ height }}</h2> -->
</div>
</template>
<script>
import { mapState } from 'vuex'
export default {
computed: {
fullName() {
return "Kobe Bryant"
},
// 其他的計(jì)算屬性, 從state獲取,會(huì)自動(dòng)映射成計(jì)算屬性
// ...mapState(["counter", "name", "age", "height"])
// 傳入對(duì)象可以重命名
...mapState({
sCounter: state => state.counter,
sName: state => state.name
})
}
}
</script>
<style scoped>
</style>
在setup中使用mapState
在setup中如果我們單個(gè)獲取狀態(tài)是非常簡單的,通過useStore拿到store后去獲取某個(gè)狀態(tài)即可畴博,但是如果我們需要使用 mapState 的功能呢笨忌?
<template>
<div>
<h2>Home:{{ $store.state.counter }}</h2>
<hr>
<h2>{{sCounter}}</h2>
<h2>{{counter}}</h2>
<h2>{{name}}</h2>
<h2>{{age}}</h2>
<h2>{{height}}</h2>
<hr>
</div>
</template>
<script>
import { mapState, useStore } from 'vuex'
import { computed } from 'vue'
export default {
computed: {
fullName: function() {
return "1fdasfdasfad"
},
...mapState(["name", "age"])
},
setup() {
const store = useStore()
// setup中寫成計(jì)算屬性,這也是常用的方法
const sCounter = computed(() => store.state.counter)
// const sName = computed(() => store.state.name)
// const sAge = computed(() => store.state.age)
//返回的是函數(shù)數(shù)組
const storeStateFns = mapState(["counter", "name", "age", "height"])
// 我們需要做如下操作,將 {name: function, age: function, height: function} 轉(zhuǎn)成 {name: ref, age: ref, height: ref}
const storeState = {}
Object.keys(storeStateFns).forEach(fnKey => {
// 雖然我們使用的是mapState的方式,vue內(nèi)部還是會(huì)通過this.$store.state獲取數(shù)據(jù),所以我們給它綁定this
const fn = storeStateFns[fnKey].bind({$store: store})
// 將fn用computed包裹一下
storeState[fnKey] = computed(fn)
})
return {
sCounter,
...storeState
}
}
}
</script>
<style scoped>
</style>
默認(rèn)情況下,在setup中俱病,Vuex并沒有提供非常方便的使用mapState的方式官疲,這里我們進(jìn)行了一個(gè)函數(shù)的封裝袱结,新建useState.js文件,代碼如下:
import { computed } from 'vue'
import { mapState, useStore } from 'vuex'
export function useState(mapper) {
// 拿到store對(duì)象
const store = useStore()
// 獲取到對(duì)應(yīng)的對(duì)象的functions: {name: function, age: function}
const storeStateFns = mapState(mapper)
// 對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換
const storeState = {}
Object.keys(storeStateFns).forEach(fnKey => {
const fn = storeStateFns[fnKey].bind({$store: store})
storeState[fnKey] = computed(fn)
})
return storeState
}
使用useState函數(shù)如下:
<template>
<div>
<h2>Home:{{ $store.state.counter }}</h2>
<hr>
<h2>{{counter}}</h2>
<h2>{{name}}</h2>
<h2>{{age}}</h2>
<h2>{{height}}</h2>
<h2>{{sCounter}}</h2>
<h2>{{sName}}</h2>
<hr>
</div>
</template>
<script>
//導(dǎo)入函數(shù)
import { useState } from '../hooks/useState'
export default {
setup() {
// useState可以傳數(shù)組或者對(duì)象,這是因?yàn)閮?nèi)部的mapState可以傳數(shù)組或者對(duì)象
const storeState = useState(["counter", "name", "age", "height"])
const storeState2 = useState({
sCounter: state => state.counter,
sName: state => state.name
})
return {
...storeState,
...storeState2
}
}
}
</script>
<style scoped>
</style>
這樣我們就使用useState函數(shù)實(shí)現(xiàn)了原來computed中的mapState函數(shù)的功能途凫。
getters的基本使用
某些屬性我們可能需要經(jīng)過變化后來使用垢夹,這個(gè)時(shí)候可以使用getters。
getters: {
// 第一個(gè)參數(shù)是state,第二個(gè)參數(shù)是getters
// 返回一個(gè)值,使用:<h2>總價(jià)值: {{ $store.getters.totalPrice }}</h2>
totalPrice(state, getters) {
let totalPrice = 0
for (const book of state.books) {
totalPrice += book.count * book.price
}
//訪問另外一個(gè)getters
return totalPrice * getters.currentDiscount
},
currentDiscount(state) {
return state.discount * 0.9
}
}
使用如下:
<h2>總價(jià)值: {{ $store.getters.totalPrice }}</h2>
getters第二個(gè)參數(shù)
getters可以接收第二個(gè)參數(shù)维费,從而實(shí)現(xiàn)在getters里面訪問另外一個(gè)getters果元,代碼如上。
getters的返回函數(shù)
getters中的函數(shù)本身犀盟,可以返回一個(gè)函數(shù)而晒,那么在使用的地方相當(dāng)于可以調(diào)用這個(gè)函數(shù):
getters: {
//除了返回一個(gè)值,還可以返回一個(gè)函數(shù)
//使用的時(shí)候調(diào)用函數(shù),傳入?yún)?shù)即可:<h2>總價(jià)值: {{ $store.getters.totalPriceCountGreaterN(1) }}</h2>
totalPriceCountGreaterN(state, getters) {
return function(n) {
let totalPrice = 0
for (const book of state.books) {
if (book.count > n) {
totalPrice += book.count * book.price
}
}
return totalPrice * getters.currentDiscount
}
}
}
使用如下:
<h2>總價(jià)值: {{ $store.getters.totalPriceCountGreaterN(3) }}</h2>
getters返回模板字符串
getters: {
// 返回模板字符串
nameInfo(state) {
return `name: ${state.name}`
},
ageInfo(state) {
return `age: ${state.age}`
},
heightInfo(state) {
return `height: ${state.height}`
}
}
mapGetters輔助函數(shù)
和state一樣,如果我們感覺通過$store.getters.totalPrice
獲取getters有點(diǎn)麻煩阅畴,可以將其寫成計(jì)算屬性(代碼如下)倡怎,但是如果getters特別多,我們一個(gè)一個(gè)寫計(jì)算屬性也會(huì)很麻煩贱枣,所以我們需要mapGetters輔助函數(shù)监署。
computed: {
totalPrice() {
return this.$store.getters.totalPrice
}
}
在computed中使用mapGetters
computed: {
//傳入數(shù)組
...mapGetters(["nameInfo", "ageInfo", "heightInfo"]),
//傳入對(duì)象,重命名
...mapGetters({
//這里和mapState的對(duì)象寫法不一樣,mapState的對(duì)象寫法是傳入一個(gè)函數(shù)纽哥,這里直接傳入一個(gè)字符串就可以
sNameInfo: "nameInfo",
sAgeInfo: "ageInfo"
})
}
使用如下:
<h2>{{ ageInfo }}</h2>
<h2>{{ heightInfo }}</h2>
<h2>{{ sNameInfo }}</h2>
<h2>{{ sAgeInfo }}</h2>
在setup中使用mapGetters
和mapState一樣钠乏,在setup中我們可以直接將其寫成計(jì)算屬性,這也是常用的寫法:
setup() {
const store = useStore()
// setup中寫成計(jì)算屬性,這也是常用的方法
const sCounter = computed(() => store.getters.counter)
// const sName = computed(() => store.getters.name)
// const sAge = computed(() => store.getters.age)
return {
sCounter,
}
}
但是如果屬性多了昵仅,我們就需要寫很多的計(jì)算屬性缓熟,所以在setup中使用mapGetters,我們可以封裝一個(gè)useGetters函數(shù)摔笤,新建useGetters.js文件,代碼如下:
import { computed } from 'vue'
import { mapGetters, useStore } from 'vuex'
export function useGetters(mapper) {
// 拿到store對(duì)象
const store = useStore()
// 獲取到對(duì)應(yīng)的對(duì)象的functions: {name: function, age: function}
const storeStateFns = mapGetters(mapper)
// 對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換
const storeState = {}
Object.keys(storeStateFns).forEach(fnKey => {
const fn = storeStateFns[fnKey].bind({$store: store})
storeState[fnKey] = computed(fn)
})
return storeState
}
使用如下:
<template>
<div>
<h2>{{ nameInfo }}</h2>
<h2>{{ ageInfo }}</h2>
<h2>{{ heightInfo }}</h2>
</div>
</template>
<script>
// 導(dǎo)入函數(shù)
import { useGetters } from '../hooks/useGetters'
export default {
computed: {
},
setup() {
//使用useGetters函數(shù)
const storeGetters = useGetters(["nameInfo", "ageInfo", "heightInfo"])
return {
...storeGetters
}
}
}
</script>
<style scoped>
</style>
封裝useState.js和useGetters.js
我們發(fā)現(xiàn)useState.js和useGetters.js代碼幾乎是一樣的垦写,所以我們新建useMapper.js文件吕世,重新封裝一下,代碼如下:
import { computed } from 'vue'
import { useStore } from 'vuex'
//具體是使用mapState還是使用mapGetters我們不清楚,所以讓外界傳進(jìn)來mapFn
export function useMapper(mapper, mapFn) {
// 拿到store對(duì)象
const store = useStore()
// 獲取到對(duì)應(yīng)的對(duì)象的functions: {name: function, age: function}
const storeStateFns = mapFn(mapper)
// 對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換
const storeState = {}
Object.keys(storeStateFns).forEach(fnKey => {
const fn = storeStateFns[fnKey].bind({$store: store})
storeState[fnKey] = computed(fn)
})
return storeState
}
這時(shí)候useState.js代碼就是:
import { mapState } from 'vuex'
import { useMapper } from './useMapper'
export function useState(mapper) {
return useMapper(mapper, mapState)
}
useGetters.js代碼就是:
import { mapGetters } from 'vuex'
import { useMapper } from './useMapper'
export function useGetters (mapper) {
return useMapper(mapper, mapGetters)
}
這時(shí)候我們只需要新建一個(gè)index.js文件作為統(tǒng)一的出口梯投,下次我們使用的時(shí)候直接導(dǎo)入這個(gè)文件就行了命辖,代碼如下:
import { useGetters } from './useGetters';
import { useState } from './useState';
export {
useGetters,
useState
}
Mutations基本使用
Mutations定義如下:
mutations: {
increment(state) {
state.counter++;
},
decrement(state) {
state.counter--;
},
[INCREMENT_N](state, payload) {
state.counter += payload.n
},
addBannerData(state, payload) {
state.banners = payload
}
}
更改 Vuex 的 store 中的狀態(tài)的唯一方法是提交 mutation。
<template>
<div>
<h2>當(dāng)前計(jì)數(shù): {{ $store.state.counter }}</h2>
<hr>
<!-- 不傳參數(shù) -->
<button @click="$store.commit('increment')">+1</button>
<button @click="$store.commit('decrement')">-1</button>
<!-- 傳參數(shù) -->
<button @click="addTen">+10</button>
<hr>
</div>
</template>
<script>
export default {
methods: {
addTen() {
// 傳入一個(gè)參數(shù)
// this.$store.commit('incrementN', 10)
// 傳入多個(gè)參數(shù)就寫成一個(gè)對(duì)象
// this.$store.commit('incrementN', {n: 10, name: "why", age: 18})
// 這時(shí)候后面的參數(shù)就會(huì)被傳到incrementN函數(shù)的第二個(gè)參數(shù)
// 另外一種提交風(fēng)格
this.$store.commit({
type: 'incrementN',
n: 10,
name: "why",
age: 18
})
}
}
}
</script>
<style scoped>
</style>
Mutation常量類型
上面代碼還有個(gè)小問題分蓖,就是函數(shù)名incrementN容易寫錯(cuò)尔艇,而且寫錯(cuò)之后很難發(fā)現(xiàn),這時(shí)候我們可以將函數(shù)名incrementN定義成常量么鹤。
在store文件夾中新建mutation-types.js文件终娃,代碼如下:
export const INCREMENT_N = "increment_n"
定義mutation:
import { INCREMENT_N } from './mutation-types'
[INCREMENT_N](state, payload) {
state.counter += payload.n
}
提交mutation:
import { INCREMENT_N } from '../store/mutation-types'
this.$store.commit(INCREMENT_N, 10)
mapMutations輔助函數(shù)
上面代碼,直接通過this.$store.commit(INCREMENT_N, 10)
調(diào)用會(huì)很長蒸甜,封裝成一個(gè)addTen函數(shù)又會(huì)多了一個(gè)函數(shù)棠耕,這時(shí)候我們可以使用mapMutations輔助函數(shù)將我們需要使用的函數(shù)映射到methods里面余佛。當(dāng)我們使用mapMutations映射之后,調(diào)用方法窍荧,方法內(nèi)部會(huì)調(diào)用$store.commit
辉巡,這樣我們就不用寫this.$store.commit(INCREMENT_N, 10)
這樣很長的代碼了。
<template>
<div>
<h2>當(dāng)前計(jì)數(shù): {{ $store.state.counter }}</h2>
<hr>
<!-- 當(dāng)我們使用mapMutations映射之后,調(diào)用方法,方法內(nèi)部會(huì)調(diào)用$store.commit -->
<button @click="increment">+1</button>
<button @click="add">+1</button>
<button @click="decrement">-1</button>
<button @click="increment_n({n: 10})">+10</button>
<hr>
</div>
</template>
<script>
import { mapMutations, mapState } from 'vuex'
import { INCREMENT_N } from '../store/mutation-types'
export default {
//在methods中使用
methods: {
// 傳入數(shù)組
...mapMutations(["increment", "decrement", INCREMENT_N]),
// 傳入對(duì)象,改名字
...mapMutations({
add: "increment"
})
},
setup() {
// 在setup中使用就比mapState和mapGetters簡單多了
const storeMutations = mapMutations(["increment", "decrement", INCREMENT_N])
return {
...storeMutations
}
}
}
</script>
<style scoped>
</style>
Mutation重要原則
一條重要的原則:mutation 必須是同步函數(shù)蕊退,這是因?yàn)閐evtool工具會(huì)記錄mutation的日記郊楣,每一條mutation被記錄,devtools都需要捕捉到前一狀態(tài)和后一狀態(tài)的快照瓤荔,但是在mutation中執(zhí)行異步操作痢甘,就無法追蹤到數(shù)據(jù)的變化,所以Vuex的重要原則中要求 mutation 必須是同步函數(shù)茉贡。
Actions的基本使用
Actions類似于Mutations塞栅,不同在于:
- Action提交的是mutation,而不是直接變更狀態(tài)腔丧;
- Action可以包含任意異步操作放椰;
如果一些網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)是直接存到Vuex里面,那么網(wǎng)絡(luò)請(qǐng)求就沒必要寫到組件中了愉粤,直接寫到actions中砾医,這樣在組件中我們只需要做一次事件派發(fā)就行了。
actions: {
// 第一個(gè)參數(shù)是context衣厘,第二個(gè)參數(shù)是我們調(diào)用dispatch傳遞過來的參數(shù){count: 100}
incrementAction(context, payload) {
console.log(payload)
// 延遲1s
setTimeout(() => {
context.commit('increment')
}, 1000);
},
// context的其他屬性
decrementAction({ commit, dispatch, state, rootState, getters, rootGetters }) {
commit("decrement")
},
//如果一些網(wǎng)絡(luò)請(qǐng)求的數(shù)據(jù)是直接存到Vuex里面如蚜,那么網(wǎng)絡(luò)請(qǐng)求就沒必要寫到組件中了,直接寫到actions中即可
getHomeMultidata(context) {
//返回Promise對(duì)象影暴,好在then或者catch里面處理其他事情
return new Promise((resolve, reject) => {
axios.get("http://123.207.32.32:8000/home/multidata").then(res => {
//拿到數(shù)據(jù)后提交commit
context.commit("addBannerData", res.data.data.banner.list)
resolve({name: "coderwhy", age: 18})
}).catch(err => {
reject(err)
})
})
}
}
這里有一個(gè)非常重要的參數(shù)context错邦,context是一個(gè)和store實(shí)例均有相同方法和屬性的context對(duì)象。所以我們可以從其中獲取到commit方法來提交一個(gè)mutation型宙,或者通過 context.state 和 context.getters 來獲取 state 和 getters撬呢。但是為什么它不是store對(duì)象呢?這個(gè)等到我們講Modules時(shí)再具體來說妆兑。
Actions的分發(fā)操作
進(jìn)行action的分發(fā)使用的是 store 上的dispatch函數(shù):
<template>
<div>
<h2>當(dāng)前計(jì)數(shù): {{ $store.state.counter }}</h2>
<hr>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
<hr>
</div>
</template>
<script>
import axios from 'axios'
export default {
methods: {
increment() {
//分發(fā),攜帶參數(shù)
this.$store.dispatch("incrementAction", {count: 100})
},
decrement() {
//派發(fā)風(fēng)格(對(duì)象類型),攜帶參數(shù)
this.$store.dispatch({
type: "decrementAction",
count: 100
})
}
},
mounted() {
//分發(fā)
this.$store.dispatch("getHomeMultidata")
},
setup() {
}
}
</script>
<style scoped>
</style>
mapActions輔助函數(shù)
和Mutations一樣魂拦,如果我們不想寫this.$store.dispatch("incrementAction", {count: 100})
這些代碼,可以使用mapActions輔助函數(shù)搁嗓,它也有兩種寫法:數(shù)組類型和對(duì)象類型芯勘。
<template>
<div>
<h2>當(dāng)前計(jì)數(shù): {{ $store.state.counter }}</h2>
<hr>
<button @click="incrementAction">+1</button>
<button @click="decrementAction">-1</button>
<button @click="add">+1</button>
<button @click="sub">-1</button>
<hr>
</div>
</template>
<script>
import { mapActions } from 'vuex'
export default {
// 在methods中使用
methods: {
...mapActions(["incrementAction", "decrementAction"]),
...mapActions({
add: "incrementAction",
sub: "decrementAction"
})
},
// 在setup中使用
setup() {
const actions = mapActions(["incrementAction", "decrementAction"])
const actions2 = mapActions({
add: "incrementAction",
sub: "decrementAction"
})
return {
...actions,
...actions2
}
}
}
</script>
<style scoped>
</style>
Actions的異步操作
Action 通常是異步的,那么如何知道 Action 什么時(shí)候結(jié)束呢腺逛?
我們可以通過讓Action返回Promise荷愕,在Promise的then中來處理完成后的操作。
actions: {
getHomeMultidata(context) {
//返回Promise對(duì)象,好在then或者catch里面處理其他事情
return new Promise((resolve, reject) => {
axios.get("http://123.207.32.32:8000/home/multidata").then(res => {
//拿到數(shù)據(jù)后提交commit
context.commit("addBannerData", res.data.data.banner.list)
//回調(diào)then
resolve({name: "coderwhy", age: 18})
}).catch(err => {
//回調(diào)catch
reject(err)
})
})
}
}
<template>
<div>
<h2>當(dāng)前計(jì)數(shù): {{ $store.state.counter }}</h2>
<hr>
<button @click="incrementAction">+1</button>
<button @click="decrementAction">-1</button>
<button @click="add">+1</button>
<button @click="sub">-1</button>
<hr>
</div>
</template>
<script>
import { onMounted } from "vue";
import { useStore } from 'vuex'
export default {
setup() {
const store = useStore()
onMounted(() => {
// 派發(fā)操作
const promise = store.dispatch("getHomeMultidata")
promise.then(res => {
//請(qǐng)求成功的操作
console.log(res)
}).catch(err => {
//請(qǐng)求失敗的操作
console.log(err)
})
})
}
}
</script>
<style scoped>
</style>
Module的基本使用
由于使用單一狀態(tài)樹路翻,應(yīng)用的所有狀態(tài)會(huì)集中到一個(gè)比較大的對(duì)象狈癞,當(dāng)應(yīng)用變得非常復(fù)雜時(shí),store 對(duì)象就有可能變得相當(dāng)臃腫茂契。為了解決以上問題厌小,Vuex 允許我們將 store 分割成模塊(module)透硝,每個(gè)模塊擁有自己的 state罪治、getter、mutation屠缭、action、甚至是嵌套子模塊赘被。
在store文件夾中新建modules文件夾和index.js文件,modules文件夾用于存放模塊js文件瓦糟。
home.js文件如下:
const homeModule = {
state() {
return {
homeCounter: 100
}
},
getters: {
},
mutations: {
},
actions: {
}
}
export default homeModule
user.js文件如下:
const userModule = {
state() {
return {
userCounter: 10
}
},
getters: {
},
mutations: {
},
actions: {
}
}
export default userModule
index.js文件代碼如下:
import { createStore } from "vuex"
// 導(dǎo)入某個(gè)模塊相關(guān)的狀態(tài)管理
import home from './modules/home'
import user from './modules/user'
const store = createStore({
// 保存的數(shù)據(jù)
state() {
return {
rootCounter: 100
}
},
// 對(duì)數(shù)據(jù)進(jìn)行一些加工
getters: {
doubleRootCounter(state) {
return state.rootCounter * 2
}
},
//修改數(shù)據(jù)
mutations: {
increment(state) {
state.rootCounter++
}
},
// 狀態(tài)管理,模塊劃分
modules: {
home,
user
}
});
export default store;
組件中使用如下:
<template>
<div>
<h2>{{ $store.state.rootCounter }}</h2>
<!-- 先獲取home模塊,再通過home模塊拿數(shù)據(jù) -->
<h2>{{ $store.state.home.homeCounter }}</h2>
<h2>{{ $store.state.user.userCounter }}</h2>
</div>
</template>
<script>
export default {
setup() {
}
}
</script>
<style scoped>
</style>
module的命名空間
先說兩個(gè)問題:
問題一:比如index.js里面的mutations內(nèi)有一個(gè)increment方法先嬉,home.js里面的mutations內(nèi)也有一個(gè)increment方法轧苫,當(dāng)我們提交commit觸發(fā)increment方法的時(shí)候,這兩個(gè)increment方法都會(huì)被調(diào)用疫蔓。這里有個(gè)問題含懊,因?yàn)橛袝r(shí)候我們不想兩個(gè)increment方法都被調(diào)用。
問題二:當(dāng)我們?cè)趆ome.js里面的getters里面定義一個(gè)doubleHomeCounter衅胀,無論你是通過$store.state.home.doubleHomeCounter
還是$store.state.home.getters.doubleHomeCounter
還是$store.getters.home.doubleHomeCounter
其實(shí)都是拿不到doubleHomeCounter岔乔,這是因?yàn)槟J(rèn)做了個(gè)合并,我們需要通過$store.getters.doubleHomeCounter
才能拿到doubleHomeCounter滚躯。這里有個(gè)問題雏门,因?yàn)槲覀儾恢纃oubleHomeCounter到底是哪個(gè)模塊里面的嘿歌。
這是因?yàn)椋J(rèn)情況下茁影,模塊內(nèi)部的mutation和action仍然是注冊(cè)在全局的命名空間中的宙帝,這樣使得多個(gè)模塊能夠?qū)ν粋€(gè)mutation或action作出響應(yīng),Getters 同樣也默認(rèn)注冊(cè)在全局命名空間募闲。
如果我們希望模塊具有更高的封裝度和復(fù)用性步脓,可以添加 namespaced: true
的方式使其成為帶命名空間的模塊,這樣當(dāng)模塊被注冊(cè)后蝇更,它的所有 getter沪编、mutation 及 action 都會(huì)自動(dòng)根據(jù)模塊注冊(cè)的路徑調(diào)整命名。
當(dāng)我們加上namespaced: true
后年扩,每個(gè)模塊就是獨(dú)立的了蚁廓,我們就要指定是哪個(gè)模塊,如下:
<template>
<div>
<!-- 獲取根的rootCounter -->
<h2>root:{{ $store.state.rootCounter }}</h2>
<!-- 獲取home的homeCounter -->
<h2>home:{{ $store.state.home.homeCounter }}</h2>
<!-- 獲取user的userCounter -->
<h2>user:{{ $store.state.user.userCounter }}</h2>
<hr>
<!-- 獲取home的getters里面的doubleHomeCounter -->
<h2>{{ $store.getters["home/doubleHomeCounter"] }}</h2>
<button @click="homeIncrement">home+1</button>
<button @click="homeIncrementAction">home+1</button>
</div>
</template>
<script>
export default {
methods: {
homeIncrement() {
// 指定home模塊的commit
this.$store.commit("home/increment")
},
homeIncrementAction() {
// 指定home模塊的dispatch
this.$store.dispatch("home/incrementAction")
}
}
}
</script>
<style scoped>
</style>
子module的一些參數(shù)
對(duì)于模塊內(nèi)部的 mutation 和 getter厨幻,接收的第一個(gè)參數(shù)是模塊的局部狀態(tài)對(duì)象相嵌,其他參數(shù)如下:
home.js文件代碼如下:
const homeModule = {
namespaced: true,
state() {
return {
homeCounter: 100
}
},
getters: {
// 前面我們講了在根里面的getters里面有state和getters兩個(gè)參數(shù), 其實(shí)在子模塊的getters里面還有更多的參數(shù)
// state就是上面的state
// getters用戶獲取其他的getters
// rootState拿到根的state
// 拿到根getters
doubleHomeCounter(state, getters, rootState, rootGetters) {
return state.homeCounter * 2
},
otherGetter(state) {
return 100
}
},
mutations: {
increment(state) {
state.homeCounter++
}
},
actions: {
// 一個(gè)參數(shù)對(duì)齊進(jìn)行解構(gòu)
// commit: 提交
// dispatch: 分發(fā)
// state: 當(dāng)前state
// rootState: 根state
// getters: 當(dāng)前getters
// rootGetters: 根getters
incrementAction({commit, dispatch, state, rootState, getters, rootGetters}) {
// 這里提交commit,默認(rèn)提交的是當(dāng)前模塊的commit
commit("increment")
// 如果我們想提交根模塊的commit,需要第三個(gè)參數(shù)
// 參數(shù)一: 提交的方法名字
// 參數(shù)二: payload,也就是傳遞給increment方法的參數(shù)
// 參數(shù)三: {root: true}代表是根的commit
commit("increment", null, {root: true})
// 同理dispatch也是一樣
// dispatch("incrementAction", null, {root: true})
// 上面incrementAction的參數(shù)其實(shí)是一個(gè)context,它和store不一樣,它會(huì)引用root的一些東西,比如rootState和rootGetters
// 但是store就沒這些參數(shù),這也是他們的區(qū)別
}
}
}
export default homeModule
上面incrementAction的參數(shù)其實(shí)是一個(gè)context,它和store不一樣况脆,它會(huì)引用root的一些東西饭宾,比如rootState和rootGetters,但是store就沒這些參數(shù)格了,這也是它們的區(qū)別看铆。
module的輔助函數(shù)
前面我們使用輔助函數(shù)是這樣使用的:
computed: {
...mapState(["homeCounter"]),
...mapGetters(["doubleHomeCounter"])
},
methods: {
...mapMutations(["increment"]),
...mapActions(["incrementAction"]),
},
這樣寫只是獲取根里面的,如果不是根里面盛末,就需要指定模塊名了弹惦。
方式一:通過完整的模塊空間名稱來查找(用的少);
方式二:第一個(gè)參數(shù)傳入模塊空間名稱悄但,后面寫上要使用的屬性(用的多)棠隐;
方式三:通過 createNamespacedHelpers 生成一個(gè)模塊的輔助函數(shù)(用的多);
關(guān)于方式一檐嚣、方式二的示例如下助泽,一般方式二用的比較多。
<script>
import { mapState, mapGetters, mapMutations, mapActions } from "vuex";
export default {
computed: {
// 1.寫法一:
// ...mapState({
homeCounter: state => state.home.homeCounter
}),
...mapGetters({
doubleHomeCounter: "home/doubleHomeCounter"
})
// 2.寫法二:
...mapState("home", ["homeCounter"]),
...mapGetters("home", ["doubleHomeCounter"])
},
methods: {
// 1.寫法一:
...mapMutations({
increment: "home/increment"
}),
...mapActions({
incrementAction: "home/incrementAction"
}),
// 2.寫法二
...mapMutations("home", ["increment"]),
...mapActions("home", ["incrementAction"]),
},
}
</script>
第三種方式:我們可以使用vuex里的createNamespacedHelpers函數(shù)嚎京,然后解構(gòu)函數(shù)的返回值嗡贺。
<script>
// 導(dǎo)入函數(shù)
import { createNamespacedHelpers } from "vuex";
// 解構(gòu)返回值
const { mapState, mapGetters, mapMutations, mapActions } = createNamespacedHelpers("home")
export default {
computed: {
// 3.寫法三:
...mapState(["homeCounter"]),
...mapGetters(["doubleHomeCounter"])
},
methods: {
// 3.寫法三:
...mapMutations(["increment"]),
...mapActions(["incrementAction"]),
},
}
</script>
通過createNamespacedHelpers函數(shù),我們的寫法就和以前在根里面寫的一樣了鞍帝,更推薦這種方式暑刃。
在setup中使用
對(duì)于state和getters,如果在setup中使用我們可以用以前封裝的useState.js和useGetters.js膜眠,但是以前我們封裝useState.js和useGetters.js的時(shí)候沒有考慮模塊,所以現(xiàn)在我們要重新封裝。
useState.js文件代碼:
import { mapState, createNamespacedHelpers } from 'vuex'
import { useMapper } from './useMapper'
export function useState(moduleName, mapper) {
let mapperFn = mapState
// 傳模塊名和數(shù)組
if (typeof moduleName === 'string' && moduleName.length > 0) {
mapperFn = createNamespacedHelpers(moduleName).mapState
} else {
// 只傳數(shù)組就是從根里面獲取
mapper = moduleName
}
return useMapper(mapper, mapperFn)
}
useGetters.js文件代碼:
import { mapGetters, createNamespacedHelpers } from 'vuex'
import { useMapper } from './useMapper'
export function useGetters(moduleName, mapper) {
let mapperFn = mapGetters
if (typeof moduleName === 'string' && moduleName.length > 0) {
mapperFn = createNamespacedHelpers(moduleName).mapGetters
} else {
mapper = moduleName
}
return useMapper(mapper, mapperFn)
}
組件中使用如下:
setup() {
// {homeCounter: function}
const state = useState(["rootCounter"])
const rootGetters = useGetters(["doubleRootCounter"])
const getters = useGetters("home", ["doubleHomeCounter"])
const mutations = mapMutations(["increment"])
const actions = mapActions(["incrementAction"])
return {
...state,
...getters,
...rootGetters
...mutations,
...actions
}
}
nexttick
官方解釋:將回調(diào)推遲到下一個(gè) DOM 更新周期之后執(zhí)行宵膨。在更改了一些數(shù)據(jù)以等待 DOM 更新后立即使用它架谎。
比如我們有下面的需求:點(diǎn)擊一個(gè)按鈕,我們會(huì)修改在h2中顯示的message辟躏,message被修改后谷扣,獲取h2的高度。
實(shí)現(xiàn)上面的案例我們有三種方式:
方式一:在點(diǎn)擊按鈕后立即獲取到h2的高度(錯(cuò)誤的做法)捎琐;
方式二:在updated生命周期函數(shù)中獲取h2的高度(但是其他數(shù)據(jù)更新会涎,也會(huì)執(zhí)行該操作);
方式三:使用nexttick函數(shù)瑞凑;
<template>
<div>
<h2>{{counter}}</h2>
<button @click="increment">+1</button>
<h2 class="title" ref="titleRef">{{message}}</h2>
<button @click="addMessageContent">添加內(nèi)容</button>
</div>
</template>
<script>
import { ref, onUpdated, nextTick } from "vue";
export default {
setup() {
const message = ref("")
const titleRef = ref(null)
const counter = ref(0)
const addMessageContent = () => {
message.value += "哈哈哈哈哈哈哈哈哈哈"
// 先更新DOM
nextTick(() => {
// 再打印高度
console.log(titleRef.value.offsetHeight)
})
}
// nextTick就相當(dāng)于將我們要執(zhí)行的操作延遲到DOM更新完之后再執(zhí)行
const increment = () => {
for (let i = 0; i < 100; i++) {
counter.value++
}
}
// 方式二:在updated生命周期函數(shù)中獲取h2的高度(但是其他數(shù)據(jù)更新末秃,也會(huì)執(zhí)行該操作)
// 比如點(diǎn)擊+1的操作,onUpdated也會(huì)回調(diào)
onUpdated(() => {
console.log(titleRef.value.offsetHeight)
})
return {
message,
counter,
increment,
titleRef,
addMessageContent
}
}
}
</script>
<style scoped>
.title {
width: 120px;
}
</style>
nextTick就相當(dāng)于將我們要執(zhí)行的操作延遲到DOM更新完之后再執(zhí)行。
那么nexttick是如何做到的呢籽御?事件循環(huán)练慕,也就是event loop。
historyApiFallback
historyApiFallback是開發(fā)中一個(gè)非常常見的屬性技掏,它主要的作用是解決SPA頁面在路由跳轉(zhuǎn)之后铃将,進(jìn)行頁面刷新時(shí),返回404的錯(cuò)誤哑梳。
- boolean值劲阎,默認(rèn)是false,如果設(shè)置為true鸠真,那么在刷新時(shí)悯仙,返回404錯(cuò)誤時(shí),會(huì)自動(dòng)返回 index.html 的內(nèi)容弧哎。
- object類型的值雁比,可以配置rewrites屬性,可以配置from來匹配路徑撤嫩,決定要跳轉(zhuǎn)到哪個(gè)頁面偎捎。
事實(shí)上devServer中實(shí)現(xiàn)historyApiFallback功能是通過 connect-history-api-fallback 庫實(shí)現(xiàn)的,可以自己查看文檔序攘。Nginx配置的截圖如下:
實(shí)際我們開發(fā)中也沒有進(jìn)行配置茴她,但是刷新的時(shí)候也不會(huì)有404錯(cuò)誤,這是因?yàn)閣ebpack默認(rèn)幫我們配置了historyApiFallback: true
程奠,如下:
如果把true改成false丈牢,重新運(yùn)行項(xiàng)目,刷新瞄沙,就會(huì)發(fā)現(xiàn)報(bào)錯(cuò)了:
那么如果我們真想把historyApiFallback改成false己沛,還要去修改源碼嗎慌核?修改源碼固然可以,但是不推薦申尼,我們可以新建vue.config.js
文件垮卓,這個(gè)文件的內(nèi)容會(huì)被讀取最后合并到webpack內(nèi)部,代碼如下:
module.exports = {
configureWebpack: {
devServer: {
historyApiFallback: true
}
}
}