深入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

posted @ 2020-07-10 15:15  听闻北歌有初心  阅读(765)  评论(0编辑  收藏  举报