由於狀態零散地分布在許多組件和組件之間的交互中,大型應用復雜度也經常逐漸增長。
- 如果多層組件嵌套使用,傳遞prop,和事件emit。都很不方便。
- 不方便對數據的修改進行歷史記錄。影響后續的調試!
為了解決這個問題,Vue 提供 vuex。
vuex 甚至集成到 vue-devtools,無需配置即可進行時光旅行調試。
Vuex
前置知識:理解什么是component!
組件就是函數。編程就是通過組織小的函數們來解決問題!類似組件!
於是問題就成為:如何傳遞arguments, 組件是怎樣和它的環境互動的?
就像parameters和返回函數中的值。
在Vue中我們使用props來傳遞data到組件和組件事件,來和周邊環境交互。
prop例子
message prop從父組件傳遞到子組件,通過一個template。
//childrenComponent { props: ['message'], created(){ console.log(message) }, template: '<div>{{ message }}</div>' } // Parent Component { template: `<child-component message="'hello'"></child-component>`, components: { childrenComponent } }
event例子(見鏈接) 主要使用$emit方法和v-on
一個問題:
這是很好的移動數據的辦法。只是有一個組件的問題:當嵌套3+層時,交互成了困難。
你不得不傳遞數據從組件A到B,然后從B到C,等等。 反向events, C emits to B, B to A等等。
於是使用state進行狀態管理出現了,但這有缺陷,所以出現了Vue, 往后看👇:
簡單狀態管理
經常被忽略的是,Vue 應用中原始數據
對象的實際來源 。
當訪問數據對象時,一個 Vue 實例只是簡單的代理訪問。
所以,如果你有一處需要被多個實例間共享的狀態,可以簡單地通過維護一份數據來實現共享:
const sourceOfTruth = {}
const vmA = new Vue({ data: sourceOfTruth }) const vmB = new Vue({ data: sourceOfTruth })
子組件的實例可以通過this.$root.$data來訪問sourceOfTruth.
問題出現:任何對數據的改變,不會留下變更過的記錄。這對調試是一個噩夢!!
因此可以使用store模式:
var store = {
debug: true, state: { message: 'Hello' }, setMessageAction(newVaule) { if (this.debug) console.log('setMessageAction triggered with', newValue) this.state.message = newValue }, clearMessageAction() { if (this.debug) console.log('clearMessageAction triggered') this.state.message = '' } }
store中的state的改變,都放置在store自身的函數去管理!這就是集中方式的狀態管理!
當錯誤出現時, 就有了log來記錄bug出現之前發生了什么。
此外,每個實例/組件,可以有自己的私有狀態:
var vma = new Vue({
data: {
privateState: {},
sharedState: store.state
}
})
var vmb = new Vue({ data: { privateState: {}, sharedState: store.state } })
接下來的延伸約定:
組件不應該直接修改屬於store實例的state ,而應該執行store實例內的action函數來dispatch事件通知store去改變。
這樣約定的好處:
可以記錄所有store中發生的state的改變,同時實現能做到記錄變更mutation, 保存狀態快照,歷史回滾/時光旅行的先進的調試工具:vuex.
Vuex
Vuex用於集中管理state。或者說是集中式的狀態管理Vue依賴庫。 Vuex所做的不是在組件內協調state的數據(coordinate state's data in components), 而是,在一個巨大的state object內,協調coordinate它們(data)。 任何組件需要一個portion of the state, 就傳遞它們。
這個state或大或小由程序決定,不過最開始就是一個空對象{ }。
安裝
npm install vuex //或者進入ui,在網頁上手動添加。 //然后配置到entry point,或自建一個store.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) //如果自建store.js, 加上: export default new Vuex.Store({ state: {}, mutations: {}, actions: {} });
Vuex是一個全局對象,還有2個額外的功能:
- Vuex的state儲存是響應式的。
- 不可以直接改變store中的state。需要使用(commit)mutation.
簡單計數:
const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } } }) store.commit('increment') store.commit('increment') store.state.count //輸出2
核心概念:
- State: 保存組件的共享的狀態。
- Getter:用於state的延伸,就是computed計算屬性的功能。
- Mutation: 唯一可以改變store的狀態的方法:(commit)mutation
- Action: 只能提交Mutation,可以包括async操作。
- Module: 解決單一狀態樹,導致對象過於龐大的問題。將store分割為多個module模塊。相當於是嵌套子模塊。
State
單一狀態樹,一個對象包含全部應用層級狀態。 每個應用只有一個store實例。
Vuex通過store選項,把狀態從根組件‘注入’到子組件中。
//輔助函數mapState的用法 //在子組件中,無需每個state都聲明computed屬性,使用輔助函數,幫助我們生成計算屬性! //計算屬性? //如果想要在子組件得到store實例中的狀態,最簡單的方法就是在計算屬性中返回某個狀態
const Counter = { template: `<div>{{ count }}</div>`, computed: { count () { return store.state.count } } }
store.state.count變化的時候,會從新求取計算屬性, 並觸發更新相關的DOM。
但是,這種模式有個缺陷!!❌!!
即簡單使用store實例,集中管理的缺陷
vue使用的是模塊化的構建方法,每個需要用到state的組件都會頻繁的導入store.state。過於頻繁降低效能。
Vuex通過store選項,提供了一種機制將state從根組件‘注入’到每個子組件中(調用Vue.use(Vuex))
const app = new Vue({ el: '#app', // 把 store 對象提供給 “store” 選項,這可以把 store 的實例注入所有的子組件 store,
子組件可以通過this.$store來訪問state。
例子:
根實例vm, 子組件Counter。
vm中注冊了store選項,獲得了store對象,然后把store實例注入到Counter子組件內。
Counter使用this.$store.state.count訪問到store實例。
(我的理解是每個子組件,都有一個store實例,這樣子組件內部可以反復調用這個實例狀態,無需再從store對象那里取數據)
Counter還可以使用this.$store.commit("increment")來修改store對象的state。
(如果是修改state,則調用store對象的action修改它的state)
const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } } }) const Counter = { template: `<div>count: {{ count }}</div>`, computed: { count() { return this.$store.state.count } } } var vm = new Vue({ el: "#app", store, components: { Counter }, })
mapState
輔助函數
當一個組件需要獲取多個狀態時候,將這些狀態都聲明為計算屬性會有些重復和冗余。為了解決這個問題,我們可以使用 mapState
輔助函數幫助我們生成計算屬性,讓你少按幾次鍵:
import { mapState} from 'vuex' export default{ computed: mapState({ count: state => state.count countAlias: 'count' //為了能夠使用 `this` 獲取局部狀態,必須使用常規函數 countPlusLocalState(state) { return state.count + this.localCount } }) }
如果映射的計算屬性的名字和state的子節點名字相同時,可以給mapState傳遞一個string數組
We can also pass a string array to mapState
when the name of a mapped computed property is the same as a state sub tree name.
computed: mapState([ // 映射 this.count 為 store.state.count // this是指子組件實例 'count' ])
對象展開符號...
使用..., 簡化了上面的代碼
computed: { localComputed() { /* ... */}, ...mapState({ // ... }) }
Getter
一個簡單的演示:mycode pen:https://codepen.io/chentianwei411/pen/JmyeGq
針對state的派生的狀態的操作:
(store的計算屬性),getter返回值根據它的依賴被緩存起來,只當它的依賴值改變了才會被重新計算。
Getter接受state為第一個參數
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) } } })
通過屬性訪問:
store.getters對象中的函數可以訪問:
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
Getter可以接受其他getter作為第二個參數:我的理解,就是store.getters對象被當作第二個參數
const mystore = new Vuex.Store({ //略。。 getters: { doneTodos: state => { return state.todos.filter(todo => todo.done) }, doneTodosCount: (state, getters) => { return getters.doneTodos.length } } })
我們可以在任何組件中使用getters中的函數:
const Counter = { template: `<div>count: {{ count }}</div>`, computed: { count() { return this.$store.getters.doneTodosCount } } } var vm = new Vue({ el: "#app", store: mystore components: { Counter }, })
通過方法訪問:返回的是一個函數 (state) => { return function(){} }
getters: { //..略 getTodoById: (state) => (id) => { return state.todos.find(todo => todo.id === id) } }, mystore.getters.getTodoById(2) //->返回todo對象一個。{ id: 2, text: '...', done: false }
Mutation
Mycodepen:2個演示:
https://codepen.io/chentianwei411/pen/PyKyNr
mutations屬性中的函數可以接受2個參數,
- 第一個參數是state.
- 第2個參數最好是一個包含record的對象
mutations: {//添加參數payload,約定它是對象。 incrementPayload (state, payload) { state.count += payload.amount } }
Mutation必須是同步函數,不能說異步函數,因為當mutation觸發時,回調函數沒能被調用,那么就不能記錄追蹤回調函數中進行的狀態的改變。
使用action處理異步操作:
Action
Action 類似於 mutation,不同在於:
- Action 提交的是 mutation,而不是直接變更狀態。
- Action 可以包含任意異步操作
const store = new Vuex.Store({ state: { count: 0 }, mutations: { increment (state) { state.count++ } }, actions: { increment (context) { setTimeout( () => { context.commit('increment') }, 1000) } } })
Action 函數接受一個與 store 實例具有相同方法和屬性的 context 對象,因此你可以調用 :
context.commit
提交一個 mutation。- context.state
- context.getters
當我們在之后介紹到 Modules 時,你就知道 context 對象為什么不是 store 實例本身了。
dispatch Action
不受必須同步執行的限制。
store.dispatch('increment')
同樣可以使用載荷,對象的方式分發:
我的理解:action中異步調用mutations中的函數。actions和mutations中的函數名字一一對應。
const store = new Vuex.Store({ state: { count: 0 }, mutations: { //...略... incrementPayload (state, payload) { state.count += payload.amount } }, actions: { //...略... incrementPayload (context, payload) { setTimeout( () => { context.commit('incrementPayload', payload) }, 1000) } } })
在組件中分發Action(未看)
組合 Action(未看)
項目結構:
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API請求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 我們組裝模塊並導出 store 的地方
├── actions.js # 根級別的 action
├── mutations.js # 根級別的 mutation
└── modules
├── cart.js # 購物車模塊
└── products.js # 產品模塊
插件
Vuex.Store的實例方法:
subscribe(handler: Function): Function
hander會在每個mutation完成后調用,接收mutation和經過mutation后的state作為參數: