深入Vuex最佳實踐


前言

在開始正文之前先廢話一段,我上周寫的一篇面試文章居然火了🤓, 開心了一上午,其實我也才剛開始寫文章肯定是比不上那些大佬的質量,所以能火我也感到佷意外,很感謝支持我的朋友, 我也會努力花時間在這方面為讀者產出更好的文章,很多朋友問是我怎么自學的,大家可以看下我的博客,我有寫過自己自學的方法雖然說不上多好但也是自己自學這么久的一些好的學習方式,還是可以給一些學前端不久的小伙伴一些參考,希望大家加油!

tip: 文章首發於掘金並做了排版美化推薦掘金閱讀體驗更好 戳我跳轉

起步

核心概念

好了,現在開始回到正文,在講Vuex之前我們先來了解下Vuex是什么?

Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式。它采用集中式存儲管理應用的所有組件的狀態,並以相應的規則證狀態以一種可預測的方式發生變化

上面是官方給的解釋,其實講的已經很清楚了,它就是專門為Vue.js開發的一套集中式狀態管理模式的庫,那它解決了什呢?

使用Vuex管理數據的好處

  • 能夠在Vuex 中集中管理共享的數據,便於開發和后期進行維護
  • 能夠高效的實現組件之間的數據共享,提高開發效率
  • 存儲在Vuex中的數據是響應式的,當數據發生改變時,視圖中的數據也會同步更新

所以基於上面三個優點我們就能明白,基於Vuex集中管理共享的數據,解決了多個組件之間的數據共享問題,並且因為數據是響應式的,所以數據變化視圖也會更新,所以我們使用Vuex之后就不需要關注不同視圖(組件)依賴同一狀態(數據)的問題, 我們可以將所有精力放在狀態(數據)更新上就可以了,剩下的Vuex會幫我們解決。

講明白Vuex的慨念后,我們來看下面兩張官方給的圖

  • state,驅動應用的數據源;
  • view,以聲明方式將 state 映射到視圖;
  • actions,響應在 view 上的用戶輸入導致的狀態變化

這是一個單項數據流的簡單示意圖,它的問題在於:

  • 多個視圖依賴於同一狀態。
  • 來自不同視圖的行為需要變更同一狀態。

所以有了Vuex集中式狀態管理模式`

Vuex的核心特性

上圖很好的解釋了Vuex的特性,在解釋圖片想要表達的意思之前我們先來解釋下圖中出現單詞都代表什么角色。

  • State

    State提供唯一的公共數據源,所有共享的數據都要統一放到Store中的State中存儲

  • Mutation

    Mutation用於修改變更$store中的數據

  • Action

    在mutations中不能編寫異步的代碼,會導致vue調試器的顯示出錯。
    在vuex中我們可以使用Action來執行異步操作。

  • Getter

    Getter用於對Store中的數據進行加工處理形成新的數據
    它只會包裝Store中保存的數據,並不會修改Store中保存的數據,當Store中的數據發生變化時,Getter生成的內容也隨之變化

手動實踐

接下來我們通過實踐的方式來體驗下上面這張圖的完整流程。

tip: 本文章因為是寫實踐方面,所以代碼量會有點多,建議大家邊看文章邊動手操作。

 // 創建一個項目
 vue create vuex(你的項目名稱) 
 創建好的之后項目中有個store文件夾下面的index就是你的狀態管理庫, 接下來我們體驗下完整的vuex狀態管理流程。

修改index.js

 import Vue from 'vue'
 import Vuex from 'vuex'
 import state from './state'
 import getters from './getters'
 import actions from './actions'
 import mutations from './mutations'
 
 Vue.use(Vuex)
 // 導出Store的實例
 export default new Vuex.Store({ ********
   state, // 數據源
   getters, // 可以對數據源進行二次處理
   actions, // 用於觸發mutations中函數來修改state中的數據, 主要用於彌補mutations不能編寫異步代碼的問題
   mutations // 用於修改數據源中的數據
 })

創建state.js文件

 const defaultLevel = '初級前端開發'
 const salary = '5000'
 const ages = [3, 2, 1, 4, 52, 20, 22, 10];
 
 
 export default { // 提供了3個數據源
   defaultLevel,
   salary,
   ages
 }

創建mutations.js

 const upgrade = (state, newLevel) => {
   state.defaultLevel = newLevel
 }
 
 const upSalary = (state, newSalary) => {
   state.salary = newSalary
 }
 
 export default { // 提供了2個修改數據源的方法
   upgrade,
   upSalary
 }

創建actions.js

 export default {
   changeLevel({commit}, newLevel) { // actions中可以編寫異步代碼
       return new Promise((resolve) => {
         setTimeout(() => {
          commit('upgrade', newLevel)
          console.log('打怪升級...');
          console.log('打怪升級...');
          console.log('打怪升級...');
          resolve('升級完成了')
         }, 2000);
       })
   },
 
 }

創建getters.js

 /* eslint-disable */
 const filterAge = (state) => (term) => state.ages.filter((age) => age > term) // ES6語法
 
 export default { // 提供了一個對數據源進行過濾的方法
   filterAge
 }
 

最后的整體目錄

app.js

 <template>
   <div class="app">
     <div class="example1">
         <h1>例子1</h1>
        <div>目前等級: {{defaultLevel}} 薪資{{salary}}</div>
       <button @click="upgradeHandler">升級</button>
     </div>
     <div class="example2">
       <h1>例子2</h1>
       <ul>
         <li v-for="item in ages" :key="item.toString()">{{item}}</li>
       </ul>
       <button @click="filterHandler">篩選</button>
       <div>
         <span>符合條件的數</span>
         <ul>
           <li v-for="item in newAge" :key='item.toString()'>{{item}}</li>
         </ul>
       </div>
     </div>
   </div>
 </template>
 
 <script>
 /* eslint-disable */
 import {
   mapState,
   mapGetters,
   mapActions,
   mapMutation,
   mapMutations,
 } from 'vuex'
 export default {
   name: 'app',
   data() {
     return {
       newAge: []
     }
   },
   computed: { // 在計算屬性中通過輔助函數, 將state中的兩個數據導出來
     ...mapState(['defaultLevel', 'salary', 'ages']),
   },
   methods: {
     ...mapActions(['changeLevel', 'changeLevel2']),
     ...mapMutations(['upSalary']),
     async upgradeHandler() {
       let ret = await this.changeLevel('中級前端開發') // 等待異步執行的結果
       console.log(ret);
       
       setTimeout(() => {
         this.upSalary(10000) // 同步代碼可以不通過actions的方式
       }, 1500);
       
       
       // this.newAge = this.getAge('10')
     },
 
     filterHandler() {
      this.newAge = this.$store.getters.filterAge(10)
     }
   }
 }
 </script>
 
 <style>
 .example1 {
   margin-bottom: 50px;
   padding-bottom: 30px;
   border-bottom: 2px solid #000;
 }
 </style>

最后我們來看看上面兩個例子的效果

例子1

例子2

建議先動手寫下代碼再來看效果

寫完上面那些代碼相信大家已經體會到了Vuex帶來的好處,接下來我用大白話解釋下Vuex

Vuex解決上面說的問題,組件(視圖)引用state(數據源)展示視圖,我通過手動dispatch來觸發actions中的方法commit(提交)觸發mutations來修改state(數據源)重新渲染數據改變組件(視圖)

好了, 現在我們來寫一個todoList案列鞏固一下。

方便大家對照代碼看效果,可以點這里看實現的效果,源碼倉庫:

案列

A.初始化案例

可以選擇重新初始化一個vuex的項目,也可以用現在這個,我們就用這個來吧。

然后打開public文件夾創建api文件夾,創建一個list.json文件模擬一下數據,文件代碼如下:

[
    {
        "id": 0,
        "info": "Racing car sprays burning fuel into crowd.",
        "done": false
    },
    {
        "id": 1,
        "info": "Japanese princess to wed commoner.",
        "done": false
    },
    {
        "id": 2,
        "info": "Australian walks 100km after outback crash.",
        "done": false
    },
    {
        "id": 3,
        "info": "Man charged over missing wedding girl.",
        "done": false
    },
    {
        "id": 4,
        "info": "Los Angeles battles huge wildfires.",
        "done": false
    }
]

接着安裝下項目所需要的庫和插件

$ npm install vue-router axios ant-design-vue babel-plugin-import less-loader node-less --save-dev

注意: 如果less版本在3.x以上使用ant-design-vue是會報錯的,我的版本是3.10.3報錯了對於這個問題issue上有很多人解答,對於不同的版本環境可能解決的方案不一樣。

issue地址

我的解決方案: 創建vue.config.js添加如下代碼

module.exports = {
  css: {
      loaderOptions: {
          less: {
            lessOptions:{
              javascriptEnabled: true,
            }
          }
      }
  },
}

再接着,打開main.js,添加store下的index.js``的引入,如下:

import Vue from 'vue'
import App from './Doto.vue'
import store from './store下的`index.js`'


/* 完整引入方式 **/
// 1. 導入 ant-design-vue 組件庫
import Antd from 'ant-design-vue'
// 2. 導入組件庫的樣式表
import 'ant-design-vue/dist/antd.css'
// 3. 安裝組件庫
Vue.use(Antd)

/** 按需加載方式: 引入模塊即可,無需單獨引入樣式**/
import { List, Button, Input, Checkbox} from 'ant-design-vue'
// 使用組件
Vue.use(List)
Vue.use(Button)
Vue.use(Input)
Vue.use(Checkbox)

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

再接着打開store文件夾下的index.js,添加axios請求json文件獲取數據的代碼,如下:

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    //所有任務列表
    list: [],
    //文本輸入框中的值
    inputValue: 'Beige'
  },
  mutations: {
    initList(state, list) {
      state.list = list
    },
    setInputValue(state,value){
      state.inputValue = value
    }
  },
  actions: {
    getList(context) {
      axios.get('api/list.json').then(({ data }) => {
        console.log(data);
        context.commit('initList', data)
      })
    }
  }
})

最后,創建Doto.vue並配置路由, 將store中的數據獲取並展示:

<template>
  <div class="doto">
    <a-input placeholder="請輸入任務" class="my_ipt" :value="inputValue" @change="handleInputChange" />
    <a-button type="primary">添加事項</a-button>

    <a-list bordered :dataSource="list" class="dt_list">
      <a-list-item slot="renderItem" slot-scope="item">
        <!-- 復選框 -->
        <a-checkbox :checked="item.done">{{item.info}}</a-checkbox>
        <!-- 刪除鏈接 -->
        <a slot="actions">刪除</a>
      </a-list-item>

      <!-- footer區域 -->
      <div slot="footer" class="footer">
        <!-- 未完成的任務個數 -->
        <span>0條剩余</span>
        <!-- 操作按鈕 -->
        <a-button-group>
          <a-button type="primary">全部</a-button>
          <a-button>未完成</a-button>
          <a-button>已完成</a-button>
        </a-button-group>
        <!-- 把已經完成的任務清空 -->
        <a>清除已完成</a>
      </div>
    </a-list>
  </div>
</template>

<script>
import { mapState } from 'vuex'
export default {
  name: 'app',
  data() {
    return {
      // list:[]
    }
  },
  created(){
    // console.log(this.$store);
    this.$store.dispatch('getList')
  },
  methods:{
    handleInputChange(e){
      // console.log(e.target.value)
      this.$store.commit('setInputValue',e.target.value)
    }
  },
  computed:{
    ...mapState(['list','inputValue'])
  }
}
</script>

<style scoped>
.doto {
  margin: 20px 50px;
}

.my_ipt {
  width: 500px;
  margin-right: 10px;
}

.dt_list {
  width: 500px;
  margin-top: 10px;
}

.footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
</style>

B.完成添加事項

首先,打開Doto.vue文件,給“添加事項”按鈕綁定點擊事件也可以給表單添加鍵盤事件,編寫處理函數

//綁定事件
<a-button type="primary" @click="addItemToList">添加事項</a-button>
 <a-input placeholder="請輸入任務" class="my_ipt" :value="inputValue" @change="handleInputChange"  @keydown.enter="addItemToList"/>

//編寫事件處理函數
methods:{
    ......
    addItemToList(){
      //向列表中新增事項
      if(this.inputValue.trim().length <= 0){
        return this.$message.warning('文本框內容不能為空')
      }

      this.$store.commit('addItem')
    }
  }

然后打開store下的index.js編寫addItem

export default new Vuex.Store({
  state: {
    //所有任務列表
    list: [],
    //文本輸入框中的值
    inputValue: 'AAA',
    //下一個id
    nextId:5
  },
  mutations: {
    ........
    //添加列表項
    addItem(state){
      const obj = {
        id :state.nextId,
        info: state.inputValue.trim(),
        done:false
      }
      //將創建好的事項添加到數組list中
      state.list.push(obj)
      //將nextId值自增
      state.nextId++
      state.inputValue = ''
    }
  }
  ......
})


C.完成刪除事項

首先,打開Doto.vue文件,給“刪除”按鈕綁定點擊事件,編寫處理函數

//綁定事件
<a slot="actions" @click="removeItemById(item.id)">刪除</a>

//編寫事件處理函數
methods:{
    ......
    removeItemById(id){
      //根據id刪除事項
      this.$store.commit('removeItem',id)
    }
  }

然后打開store下index編寫removeItem

export default new Vuex.Store({
  ......
  mutations: {
    ........
    removeItem(state,id){
      //根據id刪除事項數據
      const index = state.list.findIndex( x => x.id === id )
      // console.log(index);
      if(index != -1) state.list.splice(index,1);
    }
  }
  ......
})

D.完成選中狀態的改變

首先,打開Doto.vue文件,給“復選”按鈕綁定點擊事件,編寫處理函數

//綁定事件
<a-checkbox :checked="item.done" @change="cbStateChanged(item.id,$event)">{{item.info}}</a-checkbox>

//編寫事件處理函數
methods:{
    ......
    cbStateChanged(id,e){
      //復選框狀態改變時觸發
      const param = {
        id:id,
        status:e.target.checked
      }

      //根據id更改事項狀態
      this.$store.commit('changeStatus',param)
    }
  }

然后打開store下的index.js編寫changeStatus

export default new Vuex.Store({
  ......
  mutations: {
    ........
    changeStatus(state,param){
      //根據id改變對應事項的狀態
      const index = state.list.findIndex( x => x.id === param.id )
      if(index != -1) state.list[index].done = param.status
    }
  }
  ......
})

E.剩余項統計

打開store下的index.js,添加getters完成剩余項統計

getters:{
  unDoneLength(state){
    const temp = state.list.filter( x => x.done === false )
    console.log(temp)
    return temp.length
  }
}

打開Doto.vue,使用getters展示剩余項

//使用映射好的計算屬性展示剩余項
<!-- 未完成的任務個數 -->
<span>{{unDoneLength}}條剩余</span>

//導入getters
import { mapState,mapGetters } from 'vuex'
//映射
computed:{
  ...mapState(['list','inputValue']),
  ...mapGetters(['unDoneLength'])
}

F.清除完成事項

首先,打開Doto.vue文件,給“清除已完成”按鈕綁定點擊事件,編寫處理函數

<!-- 把已經完成的任務清空 -->
<a @click="clean">清除已完成</a>

//編寫事件處理函數
methods:{
  ......
  cleanDone(){
    //清除已經完成的事項
    this.$store.commit('cleanDone')
  }
}

然后打開store下的index.js編寫cleanDone

export default new Vuex.Store({
  ......
  mutations: {
    ........
    cleanDone(state){
      state.list = state.list.filter( x => x.done === false )
    }
  }
  ......
})

G.點擊選項卡切換事項

打開Doto.vue,給“全部”,“未完成”,“已完成”三個選項卡綁定點擊事件,編寫處理函數
並將列表數據來源更改為一個getters。

<a-list bordered :dataSource="infoList" class="dt_list">
  ......
  <!-- 操作按鈕 -->
  <a-button-group>
    <a-button :type="viewKey ==='all'?'primary':'default'" @click="changeList('all')">全部</a-button>
    <a-button :type="viewKey ==='undone'?'primary':'default'" @click="changeList('undone')">未完成</a-button>
    <a-button :type="viewKey ==='done'?'primary':'default'" @click="changeList('done')">已完成</a-button>
  </a-button-group>
  ......
</a-list>

//編寫事件處理函數以及映射計算屬性
methods:{
  ......
  changeList( key ){
    //點擊“全部”,“已完成”,“未完成”時觸發
    this.$store.commit('changeKey',key)
  }
},
computed:{
  ...mapState(['list','inputValue','viewKey']),
  ...mapGetters(['unDoneLength','infoList'])
}

打開store下的index.js,添加gettersmutationsstate

export default new Vuex.Store({
  state: {
    ......
    //保存默認的選項卡值
    viewKey:'all'
  },
  mutations: {
    ......
    changeKey(state,key){
      // 當用戶點擊“全部”,“已完成”,“未完成”選項卡時觸發
      state.viewKey = key
    }
  },
  ......
  getters:{
    .......
    infoList(state){
      if(state.viewKey === 'all'){
        return state.list
      }
      if(state.viewKey === 'undone'){
        return state.list.filter( x => x.done === false )
      }
      if(state.viewKey === 'done'){
        return state.list.filter( x => x.done === true )
      }
    }
  }
})

Vuex原理解析

好了, 經過上面的案列相信大家已經對Vuex的使用了解的差不多了,接下來我們來講下Vuex它的原理是這么樣的?

Vuex的原理關鍵: 使用Vue實例來管理狀態

我們先來看下效果:

接下來還是通過代碼的形式來解析下Vuex內部的原理

<html>
  <head>
    <title>vuex 原理解析</title>
    <script src='./vue.js'></script>
  </head>
  <body>
   <!-- 首先在dom的層面上定義了三個vue的實例 -->
    <div id="root">{{data}}</div>
    <div id="root2">{{data2}}</div>
    <div id="root3">
      <button @click="change">change</button>
    </div>
      
    <script>
     // 定義一個實現Vuex的插件
     function registerPlugin(Vue) {...}
     // 使用這個插件
     Vue.use(registerPlugin)
     new Vue({
        el: '#root',
        computed: { // 通過計算屬性根據數據變化來實時改變引用的視圖
          data() {
            return this.$store.state.message
          }
        }
      })
      new Vue({
        el: '#root2',
        computed: {
          data2() {
            return this.$store.state.message
          }
        }
      })
      new Vue({
        el: '#root3',
        methods: {
          change() { // 提供一個change方法來改變store(倉庫)中的state(數據源)
            const newValue = this.$store.state.message + '.'
            this.$store.mutations.setMessage(newValue)
          }
        }
      })
    </script>
  </body>

模仿Vuex源碼實現

<script>
	// 定義一個實現Vuex的插件
      function registerPlugin(Vue) {
        // 自定義一個對象來模仿Vuex, 本質上的Vuex也就是一個對象
        const vuex = {}
        // 狀態管理的核心, 通過一個純粹的Vue實例來提供數據,
        vuex._vm = new Vue({
          data: {
            message: 'hello vue.js'
          }
        })
        // 定義state來指向vue構造出來的vue實例
        vuex.state = vuex._vm

        /* 定義一個mutations方法來更新state中的數據 */
        vuex.mutations = {
          setMessage(value) {
            vuex.state.message = value
          }
        }
        // 將所有實例上都掛載一個$store屬性指向vuex對象, 所以每個實例都可以直接通過this.$store來引用vuex
        function init() {
          this.$store = vuex
        }
        // 通過一個全局的mixin方法在每個實例beforeCreate階段調用init方法
        Vue.mixin({
          beforeCreate: init
        })
      }
</script>

寫在最后

因為是是實踐文,所以這整篇文章都是通過代碼的方式來講的,對於一些概念性和基礎性語法的東西講的比較少。如果 這篇文章對你有幫助請點個贊🤓

看完兩件小事

如果你覺得我的文章對你挺有幫助,我想請你幫我兩個小忙:

  1. 關注我的 GitHub 博文,讓我們成為長期關系
  2. 關注公眾號「前端自學驛站」,所有文章、資料第一時間首發公眾號,公眾號后台回復「教程」 免費領取我精心整理的前端視頻教程

img


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM