Vuex 學習記錄
Vuex 狀態管理模式+庫
三部分。
1.狀態 :來源
2.檢視。:宣告對映的狀態
3.動作:狀態可能因檢視中的使用者輸入而改變的方式
把共同的狀態抽取出來,作為一個全域性單例模式管理。
從而構成一個巨大的檢視,無論處於哪個位子都可以觸發狀態和行為。
通過定義隔離狀態並強制遵守一定的規則,便於維護。
Vuex的核心是商店。
容納應用程式狀態的容器。
有兩件事使得商店與全域性物件不同:
1.商店是被動的,當Vue元件從中檢索狀態時,如果商店中的狀態發生變化,將會對應反饋到相應的更新。
2.改變商店的狀態的辦法,唯一方法是,提交突變。
Vuex 建立一個商店的基本:
// Make sure to call Vue.use(Vuex) first if using a module system
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
}})
methods裡面可以呼叫
單一狀態樹
每一個應用僅僅包含一個Store 例項,作為唯一資料來源,SSOT single source of truth
在元件中獲得VUEX的狀態,最好的辦法就是用計算屬性。
當對應的狀態改變,會重新計算求值。
// 建立一個 Counter 元件const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return store.state.count
}
}}
這樣需要頻繁的匯入,因此可以通過store選項,將跟元件注入到每一個子元件中。
const app = new Vue({
el: '#app',
// 把 store 物件提供給 “store” 選項,這可以把 store 的例項注入所有的子元件
store,
components: { Counter },
template: `
<div class="app">
<counter></counter>
</div>
`})
通過在根例項中註冊store選項,該商店例項會注入到根元件下的所有子元件中,且子元件能通過this.$store訪問到。讓我們更新下Counter的實現:
const Counter = {
template: `<div>{{ count }}</div>`,
computed: {
count () {
return this.$store.state.count
}
}}
子元件能通過this.$store訪問到。
mapState輔助函式
一個元件需要多個狀態時候,計算屬性會有些重複和冗餘。
計算屬性,讓你少按幾次鍵:
// 在單獨構建的版本中輔助函式為 Vuex.mapStateimport { mapState } from 'vuex'
export default {
// ...
computed: mapState({
// 箭頭函式可使程式碼更簡練
count: state => state.count,
// 傳字串引數 'count' 等同於 `state => state.count`
countAlias: 'count',
// 為了能夠使用 `this` 獲取區域性狀態,必須使用常規函式
countPlusLocalState (state) {
return state.count + this.localCount
}
})}
經過 mapState 函式呼叫後的結果,如下所示:
import { mapState } from 'vuex'export default {
// ...
computed: {
count() {
return this.$store.state.count
},
countAlias() {
return this.$store.state['count']
},
countPlusLocalState() {
return this.$store.state.count + this.localCount
}
}}
同樣mapState也可以傳入一個字串陣列。
computed: mapState([
// 對映 this.count 為 store.state.count
'count'])
物件展開運算子:
將mapState與區域性的計算屬性混合使用。
computed: {
localComputed () { /* ... */ },
// 使用物件展開運算子將此物件混入到外部物件中
...mapState({
// ...
})}
Getter
當我們需要從狀態中派生出一些狀態。
可以使用getter。
Getter接受狀態作為其第一個引數:
const store = new Vuex.Store({
state: {
todos: [
{ id: 1, text: '...', done: true },
{ id: 2, text: '...', done: false }
]
},
getters: {
doneTodos: state => {
return state.todos.filter(todo => todo.done)
}
}})
Getter也可以接受其他getter作為第二個引數:
getters: {
// ...
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}}
store.getters.doneTodosCount // -> 1
同時,Getter會暴露為store.getters的物件。
你可以直接通過store.getters.doneTodos來訪問。
在計算屬性中進行使用:
computed: {
doneTodosCount () {
return this.$store.getters.doneTodosCount
}}
可以通過getter返回函式進行傳參,可以通過這樣查詢到裡面通過ID或者其他要求得到的資料。有點像指定條件查詢某一個元素這樣。
getters: {
// ...
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}}
store.getters.getTodoById(2) // -> { id: 2, text: '...', done: false }
需要注意的是, getter通過方法訪問時,每次都會進行方法的呼叫,而且不會進行對快取結果進行快取。
mapGetter僅僅就是將store中的getter方法 對映到區域性計算屬性中去。
如果想將getter屬性另外取一個名字,可以使用物件的形式。
mapGetters 輔助函式僅僅是將store中的getter對映到區域性計算屬性:
import { mapGetters } from 'vuex'
export default {
// ...
computed: {
// 使用物件展開運算子將 getter 混入 computed 物件中
...mapGetters([
'doneTodosCount',
'anotherGetter',
// ...
])
}}
如果你想將一個getter屬性另取一個名字,使用物件形式:
mapGetters({
// 把 `this.doneCount` 對映為 `this.$store.getters.doneTodosCount`
doneCount: 'doneTodosCount'})
突變
改變 VueX中的狀態的辦法是提交突變。
每一個突變都有一個字串的事件型別和一個回撥函式。
這個回撥函式就是我們實際進行狀態更改的地方,並且它會接受狀態作為第一個引數:
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 變更狀態
state.count++
}
}})
當需要使用該突變的時候,需要使用
同時可以像其中傳入額外的引數,為突變的載荷(payload)
// ...
mutations: {
increment (state, n) {
state.count += n
}}
store.commit('increment', 10)
在大多數情況下,載荷應該是一個物件,這樣可以包含多個欄位並且記錄的變異會更易讀:
// ...
mutations: {
increment (state, payload) {
state.count += payload.amount
}}
store.commit('increment', {
amount: 10})
同時提交時,還支援另外一種方式為包含type屬性的物件。
物件風格的提交方式
store.commit({
type: 'increment',
amount: 10})
處理器的部分是不變的。
同時突變仍然需要遵守響應的規則。
1.最好一開始就初始化好相應的屬性。
2.當需要為物件新增屬性時候,Vue.set(obj,'newProp',123)
或者以新代替老。
state.obj={..state.obj,newProp:123}
使用常量代替突變事件型別
使用常量替代變異事件型別在各種Flux實現中是很常見的模式。這樣可以使linter之類的工具發揮作用,同時把這些常量放在單獨的檔案中可以讓你的程式碼合作者對整個app包含的變異一目瞭然:
// mutation-types.jsexport const SOME_MUTATION = 'SOME_MUTATION'
// store.jsimport Vuex from 'vuex'import { SOME_MUTATION } from './mutation-types'
const store = new Vuex.Store({
state: { ... },
mutations: {
// 我們可以使用 ES2015 風格的計算屬性命名功能來使用一個常量作為函式名
[SOME_MUTATION] (state) {
// mutate state
}
}})
突變必須是同步函式。
元件中提交突變:
使用到mapMutations:
可以在元件中使用this.$store.commit('xxx')提交變異,或者使用mapMutations輔助函式將元件中的方法對映為store.commit呼叫(需要在根節點注入store)。
import { mapMutations } from 'vuex'
export default {
// ...
methods: {
...mapMutations([
'increment', // 將 `this.increment()` 對映為 `this.$store.commit('increment')`
// `mapMutations` 也支援載荷:
'incrementBy' // 將 `this.incrementBy(amount)` 對映為 `this.$store.commit('incrementBy', amount)`
]),
...mapMutations({
add: 'increment' // 將 `this.add()` 對映為 `this.$store.commit('increment')`
})
}}
行動Action:
動作類似於突變,但是有所不同,
動作是提交變異,而不是直接改變狀態。
動作可以包含任意非同步操作。
讓我們來註冊一個簡單的動作:
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++
}
},
actions: {
increment (context) {
context.commit('increment')
}
}})
Action函式接受一個與store例項具有相同的方法和屬性的context物件,可以通過 context ,context.commit提交變異,context.state,context.getters 來獲得狀態和 getters.
actions: {
increment ({ commit }) {
commit('increment')
}}
分發行動:
突變必須同步執行的限制,行動不受限制不受約束,可以在行動內部執行非同步操作:
store.dispatch('increment')
我們可以在行動內部執行非同步操作:
actions: {
incrementAsync ({ commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}}
來看一個更加實際的購物車示例,涉及到呼叫非同步API和分發多重變異:
actions: {
checkout ({ commit, state }, products) {
// 把當前購物車的物品備份起來
const savedCartItems = [...state.cart.added]
// 發出結賬請求,然後樂觀地清空購物車
commit(types.CHECKOUT_REQUEST)
// 購物 API 接受一個成功回撥和一個失敗回撥
shop.buyProducts(
products,
// 成功操作
() => commit(types.CHECKOUT_SUCCESS),
// 失敗操作
() => commit(types.CHECKOUT_FAILURE, savedCartItems)
)
}}
this.$store.dispatch('xxx')
import { mapActions } from 'vuex'
export default {
// ...
methods: {
...mapActions([
'increment', // 將 `this.increment()` 對映為 `this.$store.dispatch('increment')`
// `mapActions` 也支援載荷:
'incrementBy' // 將 `this.incrementBy(amount)` 對映為 `this.$store.dispatch('incrementBy', amount)`
]),
...mapActions({
add: 'increment' // 將 `this.add()` 對映為 `this.$store.dispatch('increment')`
})
}}
???操作中的動作部分好多不懂 emm(後續補充)
元件仍然保留有區域性狀態。
Vuex不意味著你將所有的狀態放入Vuex,如果有些狀態嚴格屬於單個元件,最好還是作為元件的區域性狀態。
模組
Vuex允許我們將商店分割模組。每一個模組擁有自己的狀態,變異,動作,getter ,甚至巢狀子模組。
分割:
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 的狀態
模組的區域性狀態
在模組內部,接受的第一個引數是模組區域性狀態state物件。
同樣,對於模組內部的動作,區域性狀態通過context.state暴露出來,根節點狀態則為context.rootState:
const moduleA = {
// ...
actions: {
incrementIfOddOnRootSum ({ state, commit, rootState }) {
if ((state.count + rootState.count) % 2 === 1) {
commit('increment')
}
}
}}
對於模組內部的getter,根節點狀態會作為第三個引數暴露出來:
const moduleA = {
// ...
getters: {
sumWithRootCount (state, getters, rootState) {
return state.count + rootState.count
}
}}
名稱空間
模組具有更高的封裝性和複用性。通過namespaced:true。
你在使用模組內容(模組資產)時不需要在同一模組內額外新增空間名字首。更改namespaced屬性後不需要修改模組內的程式碼。
const store = new Vuex.Store({
modules: {
account: {
namespaced: true,
// 模組內容(module assets)
state: { ... }, // 模組內的狀態已經是巢狀的了,使用 `namespaced` 屬性不會對其產生影響
getters: {
isAdmin () { ... } // -> getters['account/isAdmin']
},
actions: {
login () { ... } // -> dispatch('account/login')
},
mutations: {
login () { ... } // -> commit('account/login')
},
// 巢狀模組
modules: {
// 繼承父模組的名稱空間
myPage: {
state: { ... },
getters: {
profile () { ... } // -> getters['account/profile']
}
},
// 進一步巢狀名稱空間
posts: {
namespaced: true,
state: { ... },
getters: {
popular () { ... } // -> getters['account/posts/popular']
}
}
}
}
}})
帶名稱空間的模組內訪問全域性內容
引數傳給dispatch或commit即可。
modules: {
foo: {
namespaced: true,
getters: {
// 在這個模組的 getter 中,`getters` 被區域性化了
// 你可以使用 getter 的第四個引數來呼叫 `rootGetters`
someGetter (state, getters, rootState, rootGetters) {
getters.someOtherGetter // -> 'foo/someOtherGetter'
rootGetters.someOtherGetter // -> 'someOtherGetter'
},
someOtherGetter: state => { ... }
},
actions: {
// 在這個模組中, dispatch 和 commit 也被區域性化了
// 他們可以接受 `root` 屬性以訪問根 dispatch 或 commit
someAction ({ dispatch, commit, getters, rootGetters }) {
getters.someGetter // -> 'foo/someGetter'
rootGetters.someGetter // -> 'someGetter'
dispatch('someOtherAction') // -> 'foo/someOtherAction'
dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
commit('someMutation') // -> 'foo/someMutation'
commit('someMutation', null, { root: true }) // -> 'someMutation'
},
someOtherAction (ctx, payload) { ... }
}
}}
帶名稱空間的繫結函式
當使用mapState,mapGetters,mapActions狀語從句:mapMutations這些函式來繫結帶名稱空間的模組時,寫起來可能比較繁瑣:
computed: {
...mapState({
a: state => state.some.nested.module.a,
b: state => state.some.nested.module.b
})},
methods: {
...mapActions([
'some/nested/module/foo', // -> this['some/nested/module/foo']()
'some/nested/module/bar' // -> this['some/nested/module/bar']()
])}
對於這種情況,你可以將模組的空間名稱字串作為第一個引數傳遞給上述函式,這樣所有繫結都會自動將該模組作為上下文於是上面的例子可以簡化為:
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})},
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])}
模組動態註冊
store.registerModule
模組動態註冊
在 store 建立之後,你可以使用 store.registerModule 方法註冊模組:
// 註冊模組 `myModule`
store.registerModule('myModule', {
// ...})// 註冊巢狀模組 `nested/myModule`
store.registerModule(['nested', 'myModule'], {
// ...})
之後就可以通過 store.state.myModule 和 store.state.nested.myModule 訪問模組的狀態。
模組動態註冊使得其他Vue外掛可以在store中附加新模組的方式來訪問Vuex。
store.registerModule 來註冊模組。
store.unregisterModule來解除安裝模組。
對於保留過去的state 可以通過preserveState來將其歸檔。
store.registerModule('a', module, { preserveState: true })
模組重用
對於一個模組多個例項,其中的state類似 data的問題,同樣採用return 一個函式的做法。
state(){
return {
foo: 'bar'
}
}
用一個函式來宣告模組狀態,避免模組之間的相互汙染。。
專案結構
1.應用層的狀態分別集中在單個store中。
2.mutation 是改變狀態的唯一方法,過程是同步的。
3.非同步邏輯的封裝應該封裝到action裡面。
外掛
vuex 的store 接受plugins 的選項,從而暴露出mutation 的鉤子。
const myPlugin = store => {
// 當 store 初始化後呼叫
store.subscribe((mutation, state) => {
// 每次 mutation 之後呼叫
// mutation 的格式為 { type, payload }
})}
然後像這樣使用:
const store = new Vuex.Store({
// ...
plugins: [myPlugin]})
提交mutation:
實際上 createPlugin 方法可以有更多選項來完成複雜任務
生成state快照。
生成狀態快照的外掛應該只在開發階段使用,使用 webpack 或 Browserify,讓構建工具幫我們處理:
const store = new Vuex.Store({
// ...
plugins: process.env.NODE_ENV !== 'production'
? [myPluginWithSnapshot]
: []})
上面外掛會預設啟用。在釋出階段,你需要使用 webpack 的 DefinePlugin 或者是 Browserify 的 envify 使 process.env.NODE_ENV !== 'production' 為 false
內建 Logger 外掛
Vuex 自帶一個日誌外掛用於一般的除錯:
import createLogger from 'vuex/dist/logger'
const store = new Vuex.Store({
plugins: [createLogger()]})
createLogger 函式有幾個配置項:
const logger = createLogger({
collapsed: false, // 自動展開記錄的 mutation
filter (mutation, stateBefore, stateAfter) {
// 若 mutation 需要被記錄,就讓它返回 true 即可
// 順便,`mutation` 是個 { type, payload } 物件
return mutation.type !== "aBlacklistedMutation"
},
transformer (state) {
// 在開始記錄之前轉換狀態
// 例如,只返回指定的子樹
return state.subTree
},
mutationTransformer (mutation) {
// mutation 按照 { type, payload } 格式記錄
// 我們可以按任意方式格式化
return mutation.type
},
logger: console, // 自定義 console 實現,預設為 `console`})
嚴格模式
在嚴格模式下,狀態的更變如果不是mutation提交的話,就會報錯。
const store = new Vuex.Store({
// ...
strict: true})
開發環境與釋出環境
const store = new Vuex.Store({
// ...
strict: process.env.NODE_ENV !== 'production'})
表單處理
對於input 的v-model 屬於非mutation觸發的改變,應該怎麼做呢?
<input :value="message" @input="updateMessage">
// ...
computed: {
...mapState({
message: state => state.obj.message
})},
methods: {
updateMessage (e) {
this.$store.commit('updateMessage', e.target.value)
}}
// ...
mutations: {
updateMessage (state, message) {
state.obj.message = message
}}
雙向繫結的計算屬性:
<input v-model="message">
// ...
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}}
通過get,set來計算屬性。
測試
(未看)
熱過載
對於 mutation 和模組,你需要使用 store.hotUpdate() 方法:
// store.jsimport Vue from 'vue'import Vuex from 'vuex'import mutations from './mutations'import moduleA from './modules/a'
Vue.use(Vuex)
const state = { ... }
const store = new Vuex.Store({
state,
mutations,
modules: {
a: moduleA
}})
if (module.hot) {
// 使 action 和 mutation 成為可熱過載模組
module.hot.accept(['./mutations', './modules/a'], () => {
// 獲取更新後的模組
// 因為 babel 6 的模組編譯格式問題,這裡需要加上 `.default`
const newMutations = require('./mutations').default
const newModuleA = require('./modules/a').default
// 載入新模組
store.hotUpdate({
mutations: newMutations,
modules: {
a: newModuleA
}
})
})}