【学习笔记】《玩转Vue3全家桶》数据流:如何使用Vuex设计你的数据流

前言:在Vue 3的生态中,包括Vuex、vue-router、Vue Devtools等生态库。今天,先来了解一下Vue全家桶必备的工具:Vuex,有了这个神兵利器,复杂项目设计也会变得条理更清晰。

一、前端数据管理

    现代Web应用都是由三大件构成,分别是:组件、数据和路由。当一些数据在组件之间需要共享的时候,如何实现呢?
    解决思路:专门定义一个全局变量,任何组件需要数据的时候都去这个全局变量中获取。如果我们使用_store这个全局变量存储数据。

window._store = {}

    数据存储的结构图大致如下,任何组件内部都可以通过window._store获取数据并且修改。
【学习笔记】《玩转Vue3全家桶》数据流:如何使用Vuex设计你的数据流_第1张图片
    但是这样会有一个问题,window._store并不是响应式的,如果在Vue项目中直接使用,就无法自动更新页面。所以,需要用ref和reactive去把数据包裹成响应式数据,并且提供统一的操作方法,这就是数据管理框架的雏形。

二、Vuex是什么

    其实,Vuex存在的意义,就是管理项目的数据。
    我们使用组件化机制来搭建整个项目,每个组件内部有自己的数据和模板。但是,总有些数据需要共享的,如当前登录的用户名、权限等数据,如果都在组件内部传递,会变得非常混乱。
    而且,当项目变得越来越复杂的时候,就需要一个专门的工具,来对数据做统一地申请和发放,这样才能方便做数据管理。Vuex就相当于项目中的大管家,集中式存储管理应用的所有组件的状态。
    先来上手使用一下Vuex。在项目结构中的src/store目录,就是留给Vuex的,在项目目录下,执行这个命令,来进行Vuex的安装工作。

npm install vuex@next

    安装完成后,我们在 src/store 中先新建 index.js,在下面的代码中,我们使用 createStore 来创建一个数据存储,我们称之为 store。
    store 内部除了数据,还需要一个 mutation 配置去修改数据,你可以把这个 mutation 理解为数据更新的申请单,mutation 内部的函数会把 state 作为参数,我们直接操作 state.count 就可以完成数据的修改。

import {createStore} from 'vuex';

const store = createStore({
	state () {
		return {
			count: 666
		}
	},
	mutations: {
		add(state) {
			state.count++
		}
	}
})

    在Vue的组件系统之外,多了一个数据源,多了一个数据源,里面只有一个变量 count,并且有一个方法可以累加这个 count。然后,我们在 Vue 中注册这个数据源,在项目入口文件 src/main.js 中,使用 app.use(store) 进行注册,这样 Vue 和 Vuex 就连接上了。
    然后,使用 .use 就可以对路由进行注册,使用 .mount 就可以把 Vue 这个应用挂载到页面上,代码如下:

const app = createApp(App)
app.use(store)
	.use(router)
	.mount('#app')

    在 src/components 文件夹下新建一个 Count.vue 组件,在下面的代码中,template 中的代码我们很熟悉了,就是一个 div 渲染了 count 变量,并且点击的时候触发 add 方法。在 script 中,我们使用 useStore 去获取数据源,初始化值和修改的函数有两个变化:

  • count不是使用ref直接定义,而是使用计算属性返回了store.state.count,也就是刚才在src/store/index.js中定义的count ;
  • add函数是用来修改数据,这里不能直接去操作store.state.count += 1,因为这个数据属于Vuex统一管理,所以要使用store.commit(‘add’)去触发Vuex中的mutation去修改数据。
<template>
<div @click="add">
	{{count}}
</div>
</template>

<script setup>
import { computed } from 'vue'
import { useStore } from 'vuex'
let store = useStore();
let count = computed(()=>store.state.count);

function add() {
	store.commit('add')
}
</script>

    什么时候的数据用Vuex管理,什么时候数据要放在组件内部使用ref管理呢?
    解答:对于一个数据,如果只是组件内部使用就是用ref管理;如果需要跨组件,跨页面共享的时候,就需要把数据从Vue的组件内部抽离出来,放在Vuex中去管理。
    比如,在项目初始化的时候没有登录状态,是在用户登录成功之后,才能获取用户名这个信息,去修改Vuex的数据,再通过Vuex派发到所有的组件中。
【学习笔记】《玩转Vue3全家桶》数据流:如何使用Vuex设计你的数据流_第2张图片

三、手写迷你Vuex

    接下来,让我们手动来实现一个迷你的Vuex,来看看Vuex的大致原理吧。
    首先,需要创建一个变量store用来存储数据。下一步,就是把这个store的数据包装秤响应式的数据,并且提供给Vue组件使用。在Vue中有provide/inject这两个函数专门用来做数据共享,provide 注册了数据后,所有的子组件都可以通过 inject 获取数据,这两个函数官方文档介绍得比较详细。
    完成刚才的数据转换之后,直接进入到 src/store 文件夹下,新建 gvuex.js。下面的代码中,我们使用一个 Store 类来管理数据,类的内部使用 _state 存储数据,使用 mutations 来存储数据修改的函数,注意这里的 state 已经使用 reactive 包裹成响应式数据了。

import { inject,reactive } from 'vue'

const STORE_KEY = '__store__'
function useStore() {
	return inject(STORE_KEY)
}
function createStore(options) {
	return new Store(options)
}
class Store {
	constructor(options) {
		this._state = reactive({
			data: options.state()
		})
		this._mutations = options.mutations
	}
}
export { createStore, useStore}

    上面的代码还暴露了 createStore 去创建 Store 的实例,并且可以在任意组件的 setup 函数内,使用 useStore 去获取 store 的实例。下一步我们回到 src/store/index.js 中,把 vuex 改成 ./gvuex。
    下面的代码中,我们使用 createStore 创建了一个 store 实例,并且实例内部使用 state 定义了 count 变量和修改 count 值的 add 函数。

// import { createStore } from 'vuex'
import { createStore } from './gvuex'
const store = ...
export default store

    最终我们使用 store 的方式,在项目入口文件 src/main.js 中使用 app.use(store) 注册。为了让 useStore 能正常工作,下面的代码中,我们需要给 store 新增一个 install 方法,这个方法会在 app.use 函数内部执行。我们通过 app.provide 函数注册 store 给全局的组件使用。

class Store {
  // main.js入口处app.use(store)的时候,会执行这个函数
  install(app) {
    app.provide(STORE_KEY, this)
  }
}

    下面的代码中,Store 类内部变量 _state 存储响应式数据,读取 state 的时候直接获取响应式数据 _state.data,并且提供了 commit 函数去执行用户配置好的 mutations。

import { inject, reactive } from 'vue'
const STORE_KEY = '__store__'
function useStore() {
  return inject(STORE_KEY)
}
function createStore(options) {
  return new Store(options)
}
class Store {
  constructor(options) {
    this.$options = options
    this._state = reactive({
      data: options.state
    })
    this._mutations = options.mutations
  }
  get state() {
    return this._state.data
  }
  commit = (type, payload) => {
    const entry = this._mutations[type]
    entry && entry(this.state, payload)
  }
  install(app) {
    app.provide(STORE_KEY, this)
  }
}
export { createStore, useStore }

    这样在组件内部,我们就可以使用这个迷你的 Vuex 去实现一个累加器了。下面的代码中,我们使用 useStore 获取 store 的实例,并且使用计算属性返回 count,在修改 count 的时候使用 store.commit(‘add’) 来修改 count 的值。

import {useStore} from '../store/gvuex'
let store =useStore()
let count = computed(()=>store.state.count)
function add(){
    store.commit('add')
}

    这样借助 vue 的插件机制和 reactive 响应式功能,我们只用 30 行代码,就实现了一个最迷你的数据管理工具,也就是一个迷你的 Vuex 实现,下面我们再结合例子,正式介绍一下 Vuex 看一看 Vuex 具体怎么用?

四、Vuex实战

    Vuex 就是一个公用版本的 ref,提供响应式数据给整个项目使用。除了简单的数据修改,还会有一些异步任务的触发,这些场景 Vuex 都有专门的处理方式。
    在 Vuex 中,你可以使用 getters 配置,来实现 computed 的功能,比如我们想显示累加器数字乘以 2 之后的值,那么我们就需要引入 getters 配置。
    下面的代码中,我们实现了计算累加器数字乘以 2 以后的值。我们在 Vuex 中新增了 getters 配置,其实 getters 配置和 Vue 中的 computed 是一样的写法和功能。我们配置了 doubule 函数,用于显示 count 乘以 2 的计算结果。

import { createStore } from 'vuex'
const store = createStore({
  state () {
    return {
      count: 666
    }
  },
  getters:{
    double(state){
          return state.count*2
      }
  },
  mutations: {
    add (state) {
      state.count++
    }
  }
})

export default store

    然后,我们可以很方便地在组件中使用 getters,把 double 处理和计算的逻辑交给 Vuex。

let double = computed(()=>store.getters.double)

    实际项目开发中,有很多数据我们都是从网络请求中获取到的。在 Vuex 中,mutation 的设计就是用来实现同步地修改数据。如果数据是异步修改的,我们需要一个新的配置 action。现在我们模拟一个异步的场景,就是点击按钮之后的 1 秒,再去做数据的修改。
    面对这种异步的修改需求,在 Vuex 中你需要新增 action 的配置,在 action 中你可以做任意的异步处理。这里我们使用 setTimeout 来模拟延时,然后在 action 内部调用 mutation 就可以了。
    下面来看这个例子:
    首先,我们在 createStore 的配置中,新增了 actions 配置,这个配置中所有的函数,可以通过解构获得 commit 函数。内部的异步任务完成后,就随时可以调用 commit 来执行 mutations 去更新数据。

const store = createStore({
  state () {
    return {
      count: 666
    }
  },
  ...
  actions:{
      asyncAdd({commit}){
          setTimeout(()=>{
            commit('add')
          },1000)
      }
  }
})

    action并不是直接修改数据,而是通过mutations去修改,这是需要注意的。actions的调用方式是使用stroe.dispatch,在下面的代码中你可以看到这样的变化效果:页面中新增了一个 asyncAdd 的按钮,点击后会延迟一秒做累加。

function asyncAdd(){
    store.dispatch('asyncAdd')
}

    代码执行的效果如下:
【学习笔记】《玩转Vue3全家桶》数据流:如何使用Vuex设计你的数据流_第3张图片
    Vuex在整体上的逻辑如下所示,从宏观来说,Vue的组件负责渲染页面,组件中用到跨页面的数据,就是用state来存储,但是Vue不能直接修改state,而是要通过actions/mutations去做数据的修改。
【学习笔记】《玩转Vue3全家桶》数据流:如何使用Vuex设计你的数据流_第4张图片
    下面Vuex 官方的结构图,很好地拆解了 Vuex 在 Vue 全家桶中的定位,我们项目中也会用 Vuex 来管理所有的跨组件的数据,并且我们也会在 Vuex 内部根据功能模块去做拆分,会把用户、权限等不同模块的组件分开去管理。
【学习笔记】《玩转Vue3全家桶》数据流:如何使用Vuex设计你的数据流_第5张图片
    由于Vuex所有的数据修改都是通过mutations来完成的,因而我们可以很方便地监控到数据的动态变化,后面我们可以借助官方的调试工具,非常方便地去调试项目中的数据变化。
    总体来说,我们在决定一个数据是否用Vuex来管理的时候,核心就是要思考清楚,这个数据是否有共享给其他页面或者是其他组件的需要。 如果需要,就放置在Vuex中管理;如果不需要,就放在组件内部使用ref或者reactive去管理。

五、下一代Vuex

    Vuex 由于在 API 的设计上,对 TypeScript 的类型推导的支持比较复杂,用起来很是痛苦。因为我们的项目一直用的都是 JavaScript,你可能感触并不深,但对于使用 TypeScript 的用户来说,Vuex 的这种问题是很明显的。
    为了解决 Vuex 的这个问题,Vuex 的作者最近发布了一个新的作品叫 Pinia,并将其称之为下一代的 Vuex。Pinia 的 API 的设计非常接近 Vuex5 的提案,首先,Pinia 不需要 Vuex 自定义复杂的类型去支持 TypeScript,天生对类型推断就非常友好,并且对 Vue Devtool 的支持也非常好,是一个很有潜力的状态管理框架。

总结:

    简单来说,Vuex是一个状态和数据管理的框架,负责管理项目中多个组件和多个页面共享的数据。在开发项目的时候,我们就会把数据分成两个部分,一种数据是在某个组件内部使用,我们使用ref或者reactive定义即可,另外一种数据需要跨页面共享,就需要使用Vuex来进行管理。
    Vuex的几个新概念,使用state定义数据,使用mutation定义修改数据的逻辑,并且在组件中使用commit去调用mutations。在此基础上,还可以用getters去实现Vuex世界的计算属性,使用actions来去定义异步任务,并且在内部调用mutation去同步数据。
    Vuex的出现,让项目中的数据流动变得非常自然。数据流向组件,但组件不能直接修改数据,而是要通过mutation提出申请,mutation去修改数据,形成了一个圆环。这种方式对于项目的开发、维护和调试都是有很大的帮助的。
参考博客: 《玩转 Vue 3 全家桶》

你可能感兴趣的:(玩转Vue3全家桶,vue.js,前端,javascript)