一文弄懂前端状态管理,扫描面试障碍

389 阅读5分钟

写这篇文章是因为很久没写前端业务了,导致一些很简单的概念都慢慢忘记,于是想用文字的方式梳理一遍,加深印象。岁月不饶人啊。

现代前端框架都是使用组件化机制来搭建整个项目,每个组件内部有自己的数据和模板。

但是总有些数据是需要共享的,比如当前登录的用户名、权限等数据,如果都在组件内部传递,会变得非常混乱。

因此,需要一个东西来集中式存储管理应用的公用状态,这就是前端状态管理。

本文以前端状态管理库 Vuex 为例,来讲清楚前端状态管理是如何实现的。

Vuex 的使用

先来看下 Vuex 的使用

import { createStore } from 'vuex'

const store = createStore({
  // 注意state是一个函数
  state () {
    return {
      count: 666
    }
  },
  mutations: {
    add (state) {
      state.count++
    }
  }
})

我们使用 createStore 来创建一个数据存储,称之为 store

store 内部除了数据,还需要一个 mutation 配置去修改数据,mutation 内部的函数会把 state 作为参数,操作 state.count 就可以完成数据的修改。

现在,在 Vue 的组件系统之外,多了一个数据源,里面只有一个变量 count,并且有一个方法可以累加这个 count

然后,在 Vue 中注册这个数据源,在项目入口文件 src/main.js 中,使用 app.use(store) 进行注册,这样 VueVuex 就连接上了。

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

在组件中使用Vuex定义的state


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

count 不是使用 ref 直接定义,而是使用计算属性返回了 store.state.count,也就是刚才在 src/store/index.js 中定义的 count

add 函数是用来修改数据,修改数据可以像 store.state.count += 1 这样写吗?

其实是可以的,但是如果这样做了,Vuex 不能够记录每一次 state 的变化记录,影响我们的调试。

Vuex 开启严格模式的时候,直接修改 state 会抛出错误,所以官方建议我们开启严格模式strict: true,所有的 state 变更都在 Vuex 内部进行,所以我们要使用 store.commit('add') 去触发 Vuex 中的 mutation 去修改数据。

现在就产生了一个问题:什么时候的数据用 Vuex 管理,什么时候数据要放在组件内部使用 ref 管理呢?

对于一个数据,如果只是组件内部使用就是用 ref 管理;

如果我们需要跨组件,跨页面共享的时候,我们就需要把数据从 Vue 的组件内部抽离出来,放在 Vuex 中去管理。

知道了 Vuex 的使用,接下来我们实现一个迷你的 Vuex, 明白Vuex 的大致原理。

手写迷你 Vuex

实现思路也比较简单,其实就是创建一个变量 store 用来存储数据,然后把这个 store 的数据包转成响应式的数据,并且提供给 Vue 组件使用,这样当数据变化后,组件也随之变化。

Vue 中有 provide/inject 这两个函数专门用来做数据共享,provide 注册了数据后,所有的子组件都可以通过 inject 获取数据

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 }

上面的代码导出了两个函数createStoreuseStore

createStore 去创建 Store 的实例。

useStore可以在任意组件的 setup 函数内去获取 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)
  }
}

补全其他代码,完整代码如下:


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
    // _state私有变量
    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 }

这样借助 Vue 的插件机制和 reactive 响应式功能,这里只用 30 行代码,就实现了一个最迷你的数据管理工具Vuex

Pinia VS Vuex

PiniaVuex 一样都是是 vue 的全局状态管理器,其实 Pinia 就是 Vuex5,只不过为了尊重原作者的贡献就沿用了这个看起来很甜的名字。

首先看下 Pinia 的使用:

import { defineStore } from "pinia";

export const storeA = defineStore("storeA", {
  state: () => {
    return {
      msg: "hello pinia",
    };
  },
  getters: {},
  actions: {},
});

修改 state 里面的数据有三种方式:

  1. 直接修改
import { storeA } from '@/piniaStore/storeA' 
let piniaStoreA = storeA() 
piniaStoreA.piniaMsg = 'hello juejin'
  1. $patch

使用$patch方法可以修改多个state中的值

import { storeA } from '@/piniaStore/storeA'
let piniaStoreA = storeA()
piniaStoreA.$patch({
  piniaMsg: 'hello juejin',
  name: 'daming'
})

// $patch还可以使用函数的方式进行修改状态
cartStore.$patch((state) => { 
  state.name = 'daming' 
  state.piniaMsg = 'hello juejin' 
})
  1. actions 中进行修改

不同于Vuex的是,Pinia去掉了 mutations

import { defineStore } from "pinia";
export const storeA = defineStore("storeA", {
  state: () => {
    return {
      piniaMsg: "hello pinia",
      name: "xiao yue",
    };
  },
  actions: {
    setName(data) {
      this.name = data;
    },
  },
});

// 使用
import { storeA } from '@/piniaStore/storeA'
let piniaStoreA = storeA()
piniaStoreA.setName('daming')

这也是比较推荐的一种修改状态的方式,就像上面说的,这样可以实现整个数据流程都在状态管理器内部,便于管理。

Pinia 解构

当我们组件中需要用到 state 中多个参数时,使用解构的方式取值往往是很方便的,但是传统的ES6解构会使 state 失去响应式,比如组件App.vue,我们先解构取得 name 值,然后再去改变name值。

<template>
  <div>{{ name }}</div>
</template>
<script setup>
import { storeA } from '@/piniaStore/storeA'
let piniaStoreA = storeA()
let { piniaMsg, name } = piniaStoreA
piniaStoreA.$patch({
  name: 'daming'
})
</script>

发现浏览器上显示的值并没有更新。为了解决这个问题,Pinia提供了一个结构方法 storeToRefs

import { storeA } from '@/piniaStore/storeA' 
import { storeToRefs } from 'pinia' 

let piniaStoreA = storeA() 
let { piniaMsg, name } = storeToRefs(piniaStoreA)

modules

如果项目比较大,使用单一状态库,项目的状态库就会集中到一个大对象上,显得十分臃肿难以维护。所以Vuex就允许我们将其分割成模块(modules),每个模块都拥有自己state,mutations,actions...

Pinia每个状态库本身就是一个模块,它没有modules,如果想使用多个store,直接定义多个store传入不同的id即可,如:

import { defineStore } from "pinia";

export const storeA = defineStore("storeA", {...});
export const storeB = defineStore("storeB", {...});
export const storeC = defineStore("storeB", {...});

总结

本文首先回顾了 Vuex 的使用,然后按照使用方式手写了一个简单的迷你版的 Vuex

核心原理就是在全局维护一个响应式的数据包 store,当组件使用某个数据时,这个数据通过依赖收集收集到这个组件,这样当数据更新时组件就会自动更新。最后通过provide/inject来引用这个数据包。

文章的最后简单介绍了下 Pinia 的使用,它的使用非常简单,它没有 Vuex 中的mutations ,所有的数据操作通过commit来修改。另外,它也没有 modules,如果想使用多个store,直接定义通过defineStore多个store传入不同的id即可。