深入Vuex最佳实践
前言
在开始正文之前先废话一段,我上周写的一篇面试文章居然火了🤓, 开心了一上午,其实我也才刚开始写文章肯定是比不上那些大佬的质量,所以能火我也感到很意外,很感谢支持我的朋友, 我也会努力花时间在这方面为读者产出更好的文章,很多朋友问是我怎么自学的,大家可以看下我的博客,我有写过自己自学的方法虽然说不上多好但也是自己自学这么久的一些好的学习方式,还是可以给一些学前端不久的小伙伴一些参考,希望大家加油!
起步
核心概念
好了,现在开始回到正文,在讲Vuex之前我们先来了解下Vuex是什么?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化
上面是官方给的解释,其实讲的已经很清楚了,它就是专门为Vue.js开发的一套集中式状态管理模式的库。
面试常问,我通过全局定义Store对象也可以实现全局数据共享,使用Vuex管理数据有啥好处?它还能解决什么问题呢?
- 能够在Vuex中集中管理共享的数据,便于开发和后期进行维护(体现在Vuex单项数据流且可预测的数据变化)
- 能够高效的实现组件之间的数据共享且集成到
devtools extension方便调试,提高开发效率 - 存储在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>
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);
},
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(数据源)重新渲染数据改变组件(视图)
这段话就很好的体现了上图,Vuex是已单项数据流来管理数据,已可预测且单一的方式来变动数据,怎么理解呢?
我想去修改state中的数据:
- dispatch来触发action中的方法
- actions中commit来触发mutations
- 最后mutations来修改state中的数据
不难看出我为了变动state中数据执行了这么多操作,但最后能直接修改state数据的只有commit来触发mutations这种唯一的方式可以去修改的我state中的数据,中间那些方法就很好的为我们提供了写middleware(中间件)的操作,比如vuex-persis(持久化存储数据)
好了, 现在我们来写一个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上有很多人解答,对于不同的版本环境可能解决的方案不一样。
我的解决方案: 创建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')
}
}
```js
然后打开store下的`index.js`编写`addItem`
```js
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,添加getters,mutations,state
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源码实现
首先Vuex是以插件的方式挂载的,接下来一步一步实现这个Vuex插件,总体下来有这几步
- 实现Store类
- 维持⼀个响应式状态state
- 实现commit()
- 实现dispatch()
- 实现dgetters
- 挂载$store
修改mian.js
// 使用自己实现的Vuex插件
import store from './my-store'
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
添加my-store/index.js
import Vue from 'vue'
import Vuex from './my-vuex' // 引入我们自己实现的Vuex插件
Vue.use(Vuex)
export default new Vuex.Store({
state: {
counter: 1
},
mutations: {
// state从何而来
add(state) {
state.counter++
console.log(this);
}
},
actions: {
// commit -> 说明我们需要将store实例传入进来
add({commit}) {
setTimeout(() => {
commit('add')
}, 1000);
}
},
getters: {
doubleCounter: state => {
return state.counter * 2;
}
},
modules: { // 命名空间暂时先不实现
}
})
在Vue中使用一个插件我们需要通过Vue.use的方式去使用,并且插件要是:提供install方法的对象|一个函数。
接下来开始实现Store这个类(插件)
添加my-store/index.js
class Store {
constructor(options) {
// 通过传递的选项初始化Store实例
}
commit(type, payload) {
// type:muations的方法名称,
// payload: 要传递的数据
}
dispatch(type, payload) {
// type:actions的方法名称,
// payload: 要传递的数据
}
}
function install(_Vue, options, ....) {
// _Vue 对象
// options -> Vue.use(Vuex, 可以传递插件配置项)
}
export default { Store, install }
install
接下来先实现install方法
my-store/index.js
let Vue // 全局共享下Vue对象
function install(_Vue) {
Vue = _Vue
Vue.mixin({
beforeCreate() {
// 此处this指的是组件实例
if (this.$options.store) {
Vue.prototype.$store = this.$options.store;
}
},
})
}
上面我们通过向使用Vuex传递进来Vue对象全局混入mixin提供beforeCreate钩子,判断下只要当前实例提供的store选项说明是根实例,这样的话是不是我就可以将根组件实例的store对象挂载对Vue的原型上,保证此后所有通过Vue对象实例化的实例通过原型链查找机制都可以找到初始化根组件实例提供的store对象
就是mian.js中的这段
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
// 可以通过实例对象 this.$options来获取初始化Vue提供的配置项
// 只要初始化的时候有store就认为你是组件根实例
install方法完事
Store初始化
接下来实现Store初始化的操作
- 保存选项
- 对state做响应式处理
my-store/index.js
class Store {
constructor(options) {
let {
mutations
actions
getters
state
// ... 其他选项
} = options
// 保存选项
this._mutations = mutations
this._actions = actions
// 对state做响应式处理
this._vm = new Vue({
data() {
return {
// $$不做代理 -> 这样data中定义的数据就不会放在实例的最外层(防止用户篡改state对象)
$$state: state,
}
},
})
}
get state() {
return this._vm._data.$$state
}
set state(v) {
throw Error('请使用repalceState重置state')
}
}
来,细品一下我们在最开始写的那句——Vuex的原理关键: 使用Vue实例来管理状态(数据),达到数据响应式
通过 this._vm = new Vue()将state对象当做data数据传递进去,Vue自然帮我们实现了数据响应式,此后this.vm_data$$state的数据变动都会去影响试图,这招接鸡生蛋,玩的非常完美
效果图 来看下数据响应式是否成功
my-store/index.js
class Store {
constructor(options) {
+ setInterval(() => {
+ this.state.counter++
+ }, 1000);
+ }
}
App.vue
<div id="app">
<!-- ... -->
<p>{{ $store.state.counter }}</p>
</div>
效果图💗
commit/dispatch
接着实现下commit/dispatch方法提供给用户可以去调用mutations方法来操作state数据的API
my-store/index.js
class Store {
commit(type, payload) {
// 根据type从用户配置的mutations中获取那个函数
const entry = this._mutations[type]
if (!entry) {
console.error('unknown mutation!');
return
}
// commit的上下文是Store实例
entry.call(this, this.state, payload)
}
dispatch(type, payload) {
const entry = this._actions[type]
if (!entry) {
console.error('unknown action!');
return
}
// dispatch的上下文是Store实例
entry.call(this, this, payload)
}
}
app.vue
<template>
<div id="app">
<!-- ... -->
<p @click="$store.commit('add')"> commit: {{$store.state.counter}}</p>
<p @click="$store.dispatch('add')">dispatch: {{$store.state.counter}}</p>
</div>
</template>
效果图💗
这里先实现了commit/dispatch实现, 我们可以通过
$store.commit/dispatch('add')一步一步推导到上去,其实Vuex这原理非常简单
getters
my-store/index.js
getters提供给每个成员都是方法,我们通过传递state数据,让用户可以对state数据做二次处理。
function setGetters(state, getters) {
for (let key in getters) {
return Object.defineProperty({}, key, {
get () {
let fn = Reflect.get(getters, key)
if (typeof fn === 'function') {
return fn(state)
}
return undefined
},
set(key) {
console.error(`Cannot set ${key}`)
}
})
}
}
class Store {
constructor(options) {
+ this.getters = setGetters(
+ this._vm._data.$$state,
+ options.getters
+ )
}
}
App.vue
+ <div>getters: {{$store.getters.doubleCounter}}</div>
写在最后
因为是是实践文,所以这整篇文章都是通过代码的方式来讲的,对于一些概念性和基础性语法的东西讲的比较少。如果这篇文章对你有帮助请点个赞🤓
业精于勤,荒于嬉
如果文章中有那块写的不太好或有问题欢迎大家指出,我也会在后面的文章不停修改。也希望自己进步的同时能跟你们一起成长。喜欢我文章的朋友们也可以关注一下
我会很感激第一批关注我的人。此时,年轻的我和你,轻装上阵;而后,富裕的你和我,满载而归。