九. Vuex詳解

MPolaris發表於2020-12-06

1. 理解Vuex

1.1 Vuex功能

官方解釋

Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用 集中式儲存 管理應用的所有元件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。Vuex 也整合到 Vue 的官方除錯工具 devtools extension,提供了諸如零配置的 time-travel 除錯、狀態快照匯入匯出等高階除錯功能。

狀態管理到底是什麼?

狀態管理模式、集中式儲存管理這些名詞聽起來就非常高大上,讓人捉摸不透。其實你可以簡單的將其看成把需要多個元件共享的變數全部儲存在一個物件裡面。然後將這個物件放在頂層的Vue例項中讓其他元件可以使用。那麼多個元件是不是就可以共享這個物件中的所有變數屬性了呢?是的

如果是這樣的話為什麼官方還要專門出一個外掛Vuex呢?難道我們不能自己封裝一個物件來管理嗎?當然可以,只是我們要先想想VueJS帶給我們最大的便利是什麼呢?沒錯,就是響應式。如果你自己封裝實現一個物件能不能保證它裡面所有的屬性做到響應式呢?當然也可以,只是自己封裝可能稍微麻煩一些。不用懷疑,Vuex就是為了提供這樣一個在多個元件間共享狀態的外掛,用它就可以了。

管理什麼狀態?

但是有什麼狀態是需要我們在多個元件間共享的呢?如果你做過大型開放一定遇到過多個狀態在多個介面間的共享問題。比如使用者的登入狀態、使用者名稱稱、頭像、地理位置資訊等。比如商品的收藏、購物車中的物品等。這些狀態資訊都可以放在統一的地方對它進行儲存和管理,而且它們還是響應式的。

OK,從理論上理解了狀態管理之後,讓我們從實際的程式碼再來看看狀態管理。

1.2 單介面的狀態管理

理解

我們知道,要在單個元件中進行狀態管理是一件非常簡單的事情。

什麼意思呢?我們來看下面的圖片。這圖片中的三種東西,怎麼理解呢?

  • State:不用多說,就是我們的狀態。(你姑且可以當做就是data中的屬性)

  • View:檢視層,可以針對State的變化,顯示不同的資訊。(這個好理解)

  • Actions:這裡的Actions主要是使用者的各種操作:點選、輸入等會導致狀態的改變。

image-20201205210034289

實現

在下面案例中,我們有木有狀態需要管理呢?沒錯就是counter。counter需要某種方式被記錄下來,也就是我們的State。counter目前的值需要被顯示在介面中,也就是我們的View部分。
介面發生某些操作時(我們這裡是使用者的點選,也可以是使用者的input),需要去更新狀態,也就是我們的Actions,這不就是上面的流程圖了嗎?

<template>
  <div id="app">
      <div>當前計數:{{ counter }}</div>
      <button @click="counter+=1">+1</button>
      <button @click="counter-=1">-1</button>
  </div>
</template>

<script>
export default {
  name: "app",
  data() {
    return {
      counter: 0
    }
  }
}
</script>

<style scoped></style>
1.3 多介面狀態管理

Vue已經幫我們做好了單個介面的狀態管理,但是如果是多個介面呢?

  • 多個檢視都依賴同一個狀態(一個狀態改了,多個介面需要進行更新)
  • 不同介面的Actions都想修改同一個狀態(Home.vue需要修改,Profile.vue也需要修改這個狀態)

也就是說對於某些狀態(狀態1/狀態2/狀態3)來說只屬於我們某一個試圖,但是也有一些狀態(狀態a/狀態b/狀態c)屬於多個試圖共同想要維護的。

  • 狀態1/狀態2/狀態3你放在自己的房間中,你自己管理自己用沒問題。
  • 但是狀態a/狀態b/狀態c我們希望交給一個大管家來統一幫助我們管理!
  • 沒錯,Vuex就是為我們提供這個大管家的工具。

全域性單例模式(大管家)

  • 我們現在要做的就是將共享的狀態抽取出來,交給我們的大管家統一進行管理。
  • 之後每個試圖按照大管家規定好的規定,進行訪問和修改等操作。這就是Vuex背後的基本思想。
1.4 Vuex狀態管理圖例
image-20201205210951073

2. Vuex基本使用

2.1 安裝Vuex
npm install vuex --save
2.2 簡單的案例

使用Vuex實現一下之前的計數器案例

store/index.js

import VueX from 'vuex'
import Vue from 'vue'

Vue.use(VueX)

const store = new VueX.Store({
    state: {
        count: 0
    },
    mutations: {
        increment(state) {
            state.count++;
        },
        decrement(state) {
            state.count--;
        }
    }
})

export default store

將store掛載到Vue例項中

我們讓所有的Vue元件都可以使用這個store物件,來到main.js檔案匯入store物件,並且放在new Vue中。這樣在其他Vue元件中,我們就可以通過this.$store的方式獲取到這個store物件了。

//...
import store from '@/store'
//...
new Vue({
  store,
  render: h => h(App),
}).$mount('#app')

元件中使用Vuex的count

<template>
  <div id="app">
      <div>當前計數:{{ count }}</div>
      <button @click="increment">+1</button>
      <button @click="decrement">-1</button>
  </div>
</template>

<script>
export default {
  name: "app",
  computed: {
    count() {
      return this.$store.state.count
    }
  },
  methods: {
    increment() {
      this.$store.commit('increment')
    },
    decrement() {
      this.$store.commit('decrement')
    }
  }
}
</script>

<style scoped></style>

好的,上面就是使用Vuex最簡單的方式了。

我們來對使用步驟,做一個簡單的總結:

  • 提取出一個公共的store物件,用於儲存在多個元件中共享的狀態。
  • 將store物件放置在new Vue物件中,這樣可以保證在所有的元件中都可以使用到。
  • 在其他元件中使用store物件中儲存的狀態即可
    • 通過this.$store.state屬性的方式來訪問狀態
    • 通過this.$store.commit('mutation中的方法')來修改狀態

注意:
① 我們通過提交mutation的方式,而非直接改變store.state.count
② 這是因為Vuex可以更明確的追蹤狀態的變化,所以不要直接改變store.state.count的值。

2.3 Vux的幾個核心概念

我們來對這幾個概念一一理解

  • State
  • Getters
  • Mutation
  • Action
  • Module

3. State

State單一狀態樹

Vuex提出使用單一狀態樹, 什麼是單一狀態樹呢?英文名稱是Single Source of Truth,也可以翻譯成單一資料來源。

但是它是什麼呢?我們來看一個生活中的例子。我們知道在國內我們有很多的資訊需要被記錄,比如上學時的個人檔案,工作後的社保記錄,公積金記錄,結婚後的婚姻資訊,以及其他相關的戶口、醫療、文憑、房產記錄等等(還有很多資訊)。這些資訊被分散在很多地方進行管理,有一天你需要辦某個業務時(比如入戶某個城市),你會發現你需要到各個對應的工作地點去列印、蓋章各種資料資訊,最後到一個地方提交證明你的資訊無誤。這種儲存資訊的方案不僅僅低效而且不方便管理,以及日後的維護也是一個龐大的工作(需要大量的各個部門的人力來維護,當然國家目前已經在完善我們的這個系統了)。

這個和我們在應用開發中比較類似:如果你的狀態資訊是儲存到多個Store物件中的,那麼之後的管理和維護等等都會變得特別困難。所以Vuex也使用了單一狀態樹來管理應用層級的全部狀態。單一狀態樹能夠讓我們最直接的方式找到某個狀態的片段,而且在之後的維護和除錯過程中也可以非常方便的管理和維護。

4. Getters

類似於元件中的計算屬性computed

有時候我們需要從store中獲取一些state變化後的狀態,比如下面的Store中獲取學生年齡大於20的學生個數。

state: {
  students: [
       {id: 110, name: 'polaris',age: 18},
       {id: 111, name: 'rose',age: 22},
       {id: 112, name: 'jack',age: 34},
       {id: 113, name: 'tom',age: 11},
   ]
},

我們可以在Store中定義getters

getters: {
   greateAgesCount: state => {
       return state.students.filter(s => s.age >= 20).length;
   }
}
//如果我們已經有了一個獲取所有年齡大於20歲學生列表的getters, 那麼程式碼也可以這樣來寫
getters: {
   greateAgesStudents: state => {
       return state.students.filter(s => s.age >= 20);
   },
   greateAgesCount: (state, getters) => {
       return getters.greateAgesStudents.length;
   }   
}
//元件中獲取getters計算後的值
computed: {
  greateAgesCount() {
     return this.$store.getters.greateAgesCount
  }
},

getters預設是不能傳遞引數的,如果希望傳遞引數,那麼只能讓getters本身返回另一個函式。比如上面的案例中我們希望根據ID獲取使用者的資訊。

getters: {
   studentById: state => {
       return id => {
           return state.students.find(s => s.id === id)
       }
   }  
}
computed: {
  studentById() {
    return this.$store.getters.studentById(112)
  }
}

5. Mutation

5.1 狀態更新

Vuex的store狀態的更新唯一方式:提交Mutation

Mutation主要包括兩部分:

  • 字串的 事件型別type

  • 一個 回撥函式 handler,該回撥函式的第一個引數就是state。

mutation的定義方式:

//如下:increment就是事件型別,(state) {state.count++;}是回撥函式
mutations: {
    increment(state) {
        state.count++;
    },
    decrement(state) {
        state.count--;
    }
}

在某個元件中通過mutation更新state值

increment: function() {
    this.$store.commit('increment');
}
5.2 傳遞引數

在通過mutation更新資料的時候,有可能我們希望攜帶一些 額外的引數,引數被稱為是mutation的載荷(Payload)

Mutation中的程式碼:

mutations: {
    increment(state,n) {
        state.count += n;
    },
    decrement(state,n) {
        state.count -= n;
    }
}

在某個元件中通過mutation更新state值

methods: {
    increment() {
      this.$store.commit('increment',2)
    },
    decrement() {
      this.$store.commit('decrement',2)
    }
}

但是如果引數不是一個呢?比如我們有很多引數需要傳遞,這個時候我們通常會以物件的形式傳遞,也就是payload是一個物件。

changeCount(state,payload) {
    state.count = payload.count
}
changeCount() {
   this.$store.commit('changeCount',{count: 5})
}
5.3 提交風格

上面的通過commit進行提交是一種普通的方式

Vue還提供了另外一種風格, 它是一個包含type屬性的物件

changeCount() {
   this.$store.commit({
      type: 'changeCount',
      count: 100
   })
}

Mutation中的處理方式是將整個commit的物件作為payload使用, 所以程式碼沒有改變依然如下:

changeCount(state,payload) {
     state.count = payload.count
}
5.4 響應規則

Vuex的store中的state是響應式的,當state中的資料發生改變時Vue元件會自動更新。

這就要求我們必須遵守一些Vuex對應的規則

  • 提前在store中初始化好所需的屬性

  • 當給state中的物件新增新屬性時,,使用下面的方式

    • 方式一:使用Vue.set(obj, 'newProp', 123)
  • 方式二:用新物件給舊物件重新賦值

state中的物件新增新屬性的案例

我們來看一個例子:當我們點選更新資訊時介面並沒有發生對應改變,如何才能讓它改變呢?

import VueX from 'vuex'
import Vue from 'vue'

Vue.use(VueX)

const store = new VueX.Store({
    state: {
        info: { name: 'polaris', age: 18 }
    },
    mutations: {
        updateInfo(state,payload) {
            state.info['height'] = payload.height
        }
    }
})

export default store
<template>
  <div id="app">
      <p>我的個人資訊:{{info}}</p>
      <button @click="updateInfo">更新資訊</button>
  </div>
</template>

<script>
export default {
  name: "app",
  computed: {
    info() {
      return this.$store.state.info
    }
  },
  methods: {
    updateInfo() {
      this.$store.commit('updateInfo',{height: 1.88})
    }
  }
}
</script>

<style scoped></style>

下面程式碼的方式一和方式二,都可以讓state中的屬性是響應式的

mutations: {
    // updateInfo(state,payload) {
    //     state.info['height'] = payload.height
    // }
    updateInfo(state, payload) {
        //方式一
//      Vue.set(state.info,'height',payload.height)
        //方式二
         state.info = {...state.info, 'height': payload.height}
    }
}

我們也可以響應式的刪除某個物件的屬性如:Vue.delete(state.info,'height')

5.5 常量型別

概念

我們來考慮一個問題,在mutation中我們定義了很多事件型別(也就是其中的方法名稱)。當我們的專案增大時Vuex管理的狀態越來越多,需要更新狀態的情況越來越多,那麼意味著Mutation中的方法越來越多。方法過多使用者需要花費大量的經歷去記住這些方法甚至是多個檔案間來回切換檢視方法名稱,甚至如果不是複製可能還會出現寫錯的情況。

如何避免上述的問題呢?在各種Flux實現中,一種很常見的方案就是 使用常量替代Mutation事件的型別 。我們可以將這些常量放在一個單獨的檔案中方便管理以及讓整個app所有的事件型別一目瞭然。

具體怎麼做呢?我們可以建立一個檔案 mutation-types.js, 並且在其中定義我們的常量。
定義常量時我們可以使用ES2015中的風格,使用一個常量來作為函式的名稱。

程式碼

image-20201205225307575
5.6 同步函式

通常情況下Vuex要求我們Mutation中的方法必須是同步方法。

主要的原因是當我們使用devtools時,利用devtools幫助我們捕捉mutation的快照,但是如果是非同步操作那麼devtools將不能很好的追蹤這個操作什麼時候會被完成。即如果Vuex中的程式碼我們使用了非同步函式,你會發現state中的info資料一直沒有被改變因為它無法追蹤到。所以通常情況下不要在mutation中進行非同步的操作。

mutations: {
   updateInfo(state) {
       setTimeout(() => {
           state.info.name = "GG";
       },1000)
   }
},
image-20201206194141790

6. Action

6.1 基本定義

前面我們強調不要再Mutation中進行非同步操作,但是某些情況我們確實希望在Vuex中進行一些非同步操作,比如網路請求必然是非同步的,這個時候怎麼處理呢?

Action類似於Mutation,但是是用來代替Mutation進行非同步操作的。

Action的基本使用程式碼如下

mutations: {
   updateInfo(state) {
       // setTimeout(() => {
       //     state.info.name = "GG";
       // },1000)
       state.info.name = "GG";
   }
},
actions: {
    actUpdateInfo(context) {
       setTimeout(() => {
           context.commit('updateInfo');
       },1000)
    }
}

context是什麼?context是和store物件具有相同方法和屬性的物件,也就是說我們可以通過context去進行commit相關的操作,也可以獲取context.state等。但是注意這裡它們並不是同一個物件,為什麼呢? 我們後面學習Modules的時候再具體說。

這樣的程式碼是否多此一舉呢?我們定義了actions,然後又在actions中去進行commit,這不是脫褲放屁嗎?事實上並不是這樣,如果在Vuex中有非同步操作那麼我們就可以在actions中完成了。

6.2 分發

在Vue元件中, 如果我們呼叫action中的方法,那麼就需要使用dispatch,同樣的dispatch也是支援傳遞payload

methods: {
  updateInfo() {
     // this.$store.commit('updateInfo');
     this.$store.dispatch('actUpdateInfo');
  }
}
6.3 物件的解構寫法
const obj = {
	name: 'why',
	age: 18,
	height: 1.88
};
//順序可變
const {age, name, height} = obj;
console.log(name);

在Actions中使用物件的解構寫法

getters 和 mutations 當然也可以使用物件的解構寫法

actions: {
    actUpdateInfo({commit}) {
       setTimeout(() => {
           commit('updateInfo');
       },1000)
    }
}
6.4 Action返回的Promise

不清楚promise的用法請回看第八章

引入

當我們的store中非同步操作執行結束後,是否能夠提醒一下呼叫者已經成功執行了呢?

我們可以這樣實現:

actions: {
   actUpdateInfo(context,success) {
       setTimeout(() => {
           context.commit('updateInfo');
           success();
       },1000)
   }
},
methods: {
  updateInfo() {
    this.$store.dispatch('actUpdateInfo',() => {
      console.log('執行成功!'); 
    });
  }
}

但是這樣就不能傳入其他引數了,那我們再換種寫法!

actions: {
   actUpdateInfo(context,payload) {
       setTimeout(() => {
           context.commit('updateInfo',payload.message);
           console.log(payload.message);
           payload.success();
       },1000)
   }
},
methods: {
  updateInfo() {
    this.$store.dispatch('actUpdateInfo', {
        message: '我是攜帶的引數',
        success: () => {
        	console.log('執行成功!');
      	}
    });
  }
}

雖然可以實現,但是回撥的資訊和攜帶的引數寫到一起去了,這種做法是不夠優雅的,下面我們通過Promise實現!

使用Promise

前面我們學習ES6語法的時候說過Promise經常用於非同步操作。在Action中我們可以將非同步操作放在一個Promise中,並且在成功或者失敗後呼叫對應的resolve或reject。

actions: {
  actUpdateInfo(context, payload) {
    return new Promise((resolve, reject) => {
       setTimeout(() => {
           context.commit('updateInfo');
           console.log(payload);
           resolve('執行成功!');
       }, 1000)
    })
  }
},
methods: {
  updateInfo() {
    this.$store.dispatch("actUpdateInfo", '我是攜帶的資訊').then(res => {
      console.log(res);
    });
  },
},

7. Module

7.1 理解

Module是模組的意思,為什麼在Vuex中我們要使用模組呢?

Vue使用單一狀態樹,那麼也意味著很多狀態都會交給Vuex來管理。當應用變得非常複雜時store物件就有可能變得相當臃腫。為了解決這個問題Vuex允許我們將store分割成模組(Module),而每個模組擁有自己的state,mutations,actions,getters等。

我們按照什麼樣的方式來組織模組呢?看下面程式碼

//注意:模組中mutation和getters接收的第一個引數state,context是區域性狀態物件。
const moduleA = {
    state: {
        name: 'polaris'
    },
    mutations: {
        updateName(state) {
            state.name = 'GG';
		}
    },
    actions: {
        actUpdateName(context) {
            setTimeout(() => {
                context.commit('updateName')
            },1000)
        }
    },
    getters: {
        fullName(state) {
            return state.name + "hahaha";
        }
    }
}
              
const moduleB = {
    state: {
       name: 'rose'       
    },
    mutations: {},
    actions: {},
    getters: {}
}
              
const store = new Vuex.store({
     modules: {
        a: moduleA,
        b: moduleB
     }         
})              
//state,呼叫時必須加上模組名,不同模組間可以有相同的值
this.$store.state.a.name  //獲取moduleA的狀態中的值
this.$store.state.b.name  //獲取moduleB的狀態中的值
//mutations,不同模組間可以有相同的值但是不要這麼寫,因為外部會同時呼叫不同模組的mutations方法
updateName() {
    this.$store.commit('updateName'); //依次去模組中找
}
//getters,不同模組間不能有相同的值,會報錯
this.$store.getters.fullName //依次去模組中找
//actions,不同模組有相同的mutations方法時,會同時呼叫不同模組的mutations方法
actUpdateName() {
   this.$store.dispatch('actUpdateName')
}

//=> 總結:除了state,各個模組中的其他內容不要重名!
7.2 store推薦的專案結構
image-20201206220857081

相關文章