概述
在实际的前端项目开发中,我遇到了这样的状态管理场景:
- 多实例组件:用户可以创建多个相同类型的组件实例
- 共用组件:所有实例都使用同一个共用组件
- 动态子组件:共用组件包含多个动态生成的子组件
- 数据共享:这些子组件之间需要实时共享数据
对于 Vue 2 项目,我们通常使用 Vuex 进行状态管理,但 Vuex 的代码冗余较多,学习成本较高。对于 Vue 3 项目,Pinia 成为了官方推荐的状态管理方案,它基于 Composition API,更加简洁易用。
然而,无论是 Vuex 还是 Pinia,对于一些小型项目或特定场景,都可能显得过于重量级。基于此,我们基于 Vue Composition API 实现了一个轻量级的状态管理方案,能够优雅地解决这些问题。
本文将详细介绍这个轻量级状态管理方案的实现原理、设计思想和在实际项目中的应用。
核心实现分析
让我们先来看一下这个轻量级状态管理方案的核心实现代码:
import { effectScope, onScopeDispose, getCurrentScope, inject } from 'vue-demi'
export default function createStateStore(id, setup) {
let storeMap = new Map()
return () => {
let INSTANCE_UID
try {
INSTANCE_UID = inject('INSTANCE_UID', 'default')
} catch (e) {
INSTANCE_UID = 'default'
console.warn(
'Reusing Vue components without INSTANCE_UID poses a risk of data pollution`.',
INSTANCE_UID
)
}
const MODULE_ID = `${INSTANCE_UID}_${id}`
if (!storeMap.has(MODULE_ID)) {
const scope = effectScope(true)
const store = scope.run(setup)
// 如果当前有 scope(例如在组件中),则注册清理逻辑
if (getCurrentScope()) {
onScopeDispose(() => {
scope.stop()
storeMap.delete(MODULE_ID)
})
}
storeMap.set(MODULE_ID, store)
}
return storeMap.get(MODULE_ID)
}
}
实现原理解析
这个轻量级状态管理方案的核心实现原理如下:
-
闭包存储:使用闭包中的
Map对象storeMap来存储所有的 store 实例,确保状态在多次调用间保持一致性。 -
状态隔离:通过
inject('INSTANCE_UID', 'default')获取组件的唯一标识,结合传入的id生成MODULE_ID,确保不同组件实例的状态互不干扰。 -
响应式管理:使用
effectScope(true)创建一个响应式作用域,在其中运行setup函数,确保 store 中的状态是响应式的。 -
自动清理:通过
getCurrentScope()检查是否在组件的作用域内,如果是,则注册onScopeDispose回调,在组件销毁时清理相关资源。 -
单例模式:通过检查
storeMap.has(MODULE_ID)确保每个MODULE_ID只创建一个 store 实例,实现单例模式。
使用方法
1. 定义 Store
首先,我们需要定义一个 store:
// stores/counter.js
import createStateStore from '@/utils/createStateStore'
export const useCounterStore = createStateStore('counter', () => {
const count = ref(0)
const increment = () => {
count.value++
}
const decrement = () => {
count.value--
}
return {
count,
increment,
decrement
}
})
2. 在组件中使用
然后,我们可以在组件中使用这个 store:
<template>
<div>
<p>Count: {{ counterStore.count }}</p>
<button @click="counterStore.increment">Increment</button>
<button @click="counterStore.decrement">Decrement</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
</script>
3. 在多个组件间共享
由于 createStateStore 实现了单例模式,我们可以在多个组件中使用同一个 store,实现状态共享:
<!-- ComponentA.vue -->
<template>
<div>
<p>Component A Count: {{ counterStore.count }}</p>
<button @click="counterStore.increment">Increment</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
</script>
<!-- ComponentB.vue -->
<template>
<div>
<p>Component B Count: {{ counterStore.count }}</p>
<button @click="counterStore.decrement">Decrement</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
const counterStore = useCounterStore()
</script>
实际应用场景
项目背景
在实际项目开发中,我们需要实现一个表单创建功能,用户可以创建多个相同类型的表单实例,每个表单实例都由一个共用组件实现,该组件包含多个动态生成的子组件,这些子组件之间需要实时共享数据。
具体应用
1. 定义表单 Store
我们使用 createStateStore 为每个表单实例创建一个独立的状态管理实例:
// stores/formStore.js
import createStateStore from '@/utils/createStateStore'
export const useFormStore = createStateStore('form', () => {
const formData = ref({
basicInfo: {},
conditions: [],
rewards: [],
penalties: []
})
const currentTab = ref('basicInfo')
const updateFormData = (data) => {
formData.value = { ...formData.value, ...data }
}
const setCurrentTab = (tab) => {
currentTab.value = tab
}
return {
formData,
currentTab,
updateFormData,
setCurrentTab
}
})
2. 在共用组件中使用
在表单的共用组件中,我们使用 useFormStore 来管理状态:
<!-- FormComponent.vue -->
<template>
<div class="form-component">
<el-tabs v-model="formStore.currentTab">
<el-tab-pane label="基本信息" name="basicInfo">
<BasicInfoForm />
</el-tab-pane>
<el-tab-pane label="条件设置" name="conditions">
<ConditionsForm />
</el-tab-pane>
<el-tab-pane label="奖励设置" name="rewards">
<RewardsForm />
</el-tab-pane>
<el-tab-pane label="惩罚设置" name="penalties">
<PenaltiesForm />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script setup>
import { useFormStore } from '@/stores/formStore'
import BasicInfoForm from './components/BasicInfoForm.vue'
import ConditionsForm from './components/ConditionsForm.vue'
import RewardsForm from './components/RewardsForm.vue'
import PenaltiesForm from './components/PenaltiesForm.vue'
const formStore = useFormStore()
</script>
3. 在动态子组件中共享数据
在各个动态生成的子组件中,我们可以共享同一个表单实例的状态:
<!-- BasicInfoForm.vue -->
<template>
<div class="basic-info-form">
<el-form :model="formStore.formData.basicInfo">
<el-form-item label="表单名称">
<el-input v-model="formStore.formData.basicInfo.name" />
</el-form-item>
<el-form-item label="表单描述">
<el-input type="textarea" v-model="formStore.formData.basicInfo.description" />
</el-form-item>
<!-- 其他表单字段 -->
</el-form>
</div>
</template>
<script setup>
import { useFormStore } from '@/stores/formStore'
const formStore = useFormStore()
</script>
<!-- ConditionsForm.vue -->
<template>
<div class="conditions-form">
<el-button type="primary" @click="addCondition">添加条件</el-button>
<div v-for="(condition, index) in formStore.formData.conditions" :key="index">
<el-form :model="condition">
<!-- 条件表单字段 -->
</el-form>
<el-button type="danger" @click="removeCondition(index)">删除</el-button>
</div>
</div>
</template>
<script setup>
import { useFormStore } from '@/stores/formStore'
const formStore = useFormStore()
const addCondition = () => {
formStore.formData.conditions.push({})
}
const removeCondition = (index) => {
formStore.formData.conditions.splice(index, 1)
}
</script>
4. 多表单实例管理
由于 createStateStore 使用 INSTANCE_UID 和 id 的组合来标识不同的 store 实例,我们可以在同一个页面中创建多个表单实例,每个表单实例都有自己独立的状态:
<!-- FormList.vue -->
<template>
<div class="form-list">
<el-button type="primary" @click="addForm">添加表单</el-button>
<div v-for="(form, index) in forms" :key="index">
<h3>表单 {{ index + 1 }}</h3>
<FormComponent />
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import FormComponent from './FormComponent.vue'
const forms = ref([])
const addForm = () => {
forms.value.push({})
}
</script>
解决的问题
- 状态隔离:每个表单实例都有自己独立的状态,不会相互干扰
- 数据共享:同一个表单实例的不同子组件之间可以实时共享数据
- 轻量级:不需要引入重量级的状态管理库,代码简洁易维护
- 自动清理:当表单组件销毁时,相关的状态会被自动清理,避免内存泄漏
优势体现
- 开发效率:简化了状态管理的代码,提高了开发效率
- 代码可读性:基于 Composition API,代码结构清晰,易于理解
- 灵活性:可以根据需要灵活地定义和使用 store
- 可扩展性:可以轻松添加新的状态和方法
通过 createStateStore,我们成功解决了实际项目中的状态管理问题,实现了多表单实例创建、共用组件和动态子组件间的数据共享,同时保持了代码的简洁性和可维护性。
优势分析
1. 轻量级
这个状态管理方案的实现非常简洁,只有不到 30 行代码,没有引入额外的依赖(只依赖 Vue 自身的 API),非常适合小型项目或不需要复杂状态管理的场景。
2. 灵活性
基于 Composition API,我们可以在 setup 函数中使用任何 Composition API 的特性,如 ref、reactive、computed、watch 等,实现各种复杂的状态逻辑。
3. 状态隔离
通过 INSTANCE_UID 和 id 的组合,确保不同组件实例的状态互不干扰,避免了状态污染的问题。
4. 自动清理
当组件销毁时,相关的 store 实例会被自动清理,避免了内存泄漏的问题。
5. 类型安全
如果使用 TypeScript,我们可以为 store 添加类型定义,获得更好的类型提示和类型检查。
6. 易于集成
由于实现简单,这个状态管理方案可以轻松集成到任何 Vue 项目中,不需要复杂的配置。
与其他状态管理方案的对比
| 方案 | 适用版本 | 优点 | 缺点 |
|---|---|---|---|
| Vuex | Vue 2 | 功能完善,生态成熟 | 代码冗余,学习成本高,Composition API 支持不够友好 |
| Pinia | Vue 3 | 简洁易用,类型友好,Composition API 支持好 | 对于小型项目可能过于重量级,依赖较多 |
| 轻量级状态管理方案 | Vue 3 | 轻量级,灵活性高,Composition API 支持好,易于集成 | 功能相对简单,适合小型项目或特定场景 |
优化建议
1. 添加持久化支持
我们可以为 createStateStore 添加持久化支持,将状态保存到 localStorage 或 sessionStorage 中:
export default function createStateStore(id, setup, options = {}) {
let storeMap = new Map()
return () => {
let INSTANCE_UID
try {
INSTANCE_UID = inject('INSTANCE_UID', 'default')
} catch (e) {
INSTANCE_UID = 'default'
console.warn(
'Reusing Vue components without INSTANCE_UID poses a risk of data pollution`.',
INSTANCE_UID
)
}
const MODULE_ID = `${INSTANCE_UID}_${id}`
if (!storeMap.has(MODULE_ID)) {
const scope = effectScope(true)
// 从持久化存储中加载状态
let persistedState = null
if (options.persist) {
const storageKey = `store_${MODULE_ID}`
try {
persistedState = JSON.parse(localStorage.getItem(storageKey) || 'null')
} catch (e) {
console.warn('Failed to load persisted state', e)
}
}
const store = scope.run(() => setup(persistedState))
// 持久化状态
if (options.persist) {
watch(
store,
(newState) => {
const storageKey = `store_${MODULE_ID}`
localStorage.setItem(storageKey, JSON.stringify(newState))
},
{ deep: true }
)
}
// 如果当前有 scope(例如在组件中),则注册清理逻辑
if (getCurrentScope()) {
onScopeDispose(() => {
scope.stop()
storeMap.delete(MODULE_ID)
})
}
storeMap.set(MODULE_ID, store)
}
return storeMap.get(MODULE_ID)
}
}
2. 添加模块化支持
我们可以为 createStateStore 添加模块化支持,允许嵌套使用 store:
export default function createStateStore(id, setup) {
let storeMap = new Map()
return () => {
let INSTANCE_UID
try {
INSTANCE_UID = inject('INSTANCE_UID', 'default')
} catch (e) {
INSTANCE_UID = 'default'
console.warn(
'Reusing Vue components without INSTANCE_UID poses a risk of data pollution`.',
INSTANCE_UID
)
}
const MODULE_ID = `${INSTANCE_UID}_${id}`
if (!storeMap.has(MODULE_ID)) {
const scope = effectScope(true)
const store = scope.run(setup)
// 递归处理嵌套的 store
Object.keys(store).forEach(key => {
if (typeof store[key] === 'function' && store[key]._isStore) {
store[key] = store[key]()
}
})
// 标记为 store
store._isStore = true
// 如果当前有 scope(例如在组件中),则注册清理逻辑
if (getCurrentScope()) {
onScopeDispose(() => {
scope.stop()
storeMap.delete(MODULE_ID)
})
}
storeMap.set(MODULE_ID, store)
}
return storeMap.get(MODULE_ID)
}
}
技术要点
-
闭包:使用闭包存储所有的 store 实例,确保状态在多次调用间保持一致性。
-
Effect Scope:使用
effectScope管理响应式状态的生命周期,确保响应式状态能够被正确清理。 -
依赖注入:使用
inject获取组件的唯一标识,确保不同组件实例的状态互不干扰。 -
单例模式:通过检查
storeMap.has(MODULE_ID)确保每个MODULE_ID只创建一个 store 实例。 -
生命周期管理:通过
onScopeDispose注册清理逻辑,在组件销毁时清理相关资源。
注意事项
-
INSTANCE_UID:确保在组件中提供
INSTANCE_UID,否则会使用默认值 'default',可能会导致状态污染。 -
响应式状态:在
setup函数中,确保使用ref、reactive等 API 创建响应式状态,否则状态变化不会触发组件更新。 -
清理逻辑:如果在非组件环境中使用
createStateStore,需要手动管理 store 的生命周期,避免内存泄漏。 -
性能考虑:对于大型应用,可能需要考虑使用更成熟的状态管理库,如 Pinia。
总结
在实际项目开发中,我们基于 Vue Composition API 实现了一个轻量级的状态管理方案,成功解决了多表单实例创建、共用组件和动态子组件间的数据共享问题。
核心优势
-
轻量级设计:仅依赖 Vue Composition API,代码简洁,无额外依赖,适合小型项目或特定场景。
-
状态隔离机制:通过
INSTANCE_UID和id的组合,确保每个表单实例都有独立的状态,避免了多实例之间的状态污染。 -
高效数据共享:同一表单实例的不同子组件可以实时共享数据,简化了组件间的通信。
-
自动生命周期管理:当表单组件销毁时,相关的状态会被自动清理,避免了内存泄漏。
-
灵活性与可扩展性:基于 Composition API,可以轻松添加新的状态和方法,适应项目的不断变化。
-
易于集成:实现简单,不需要复杂的配置,可以轻松集成到任何 Vue 3 项目中。
适用场景
- 小型项目:不需要复杂状态管理功能的小型项目
- 局部状态管理:某个页面或组件组的状态管理
- 组件库开发:在组件库中使用,避免引入重量级依赖
- 快速原型开发:快速构建原型时使用,减少配置和依赖
未来展望
这个轻量级状态管理方案虽然简单,但已经具备了基本的状态管理功能。在未来的开发中,我们可以根据项目的具体需求,对其进行进一步的扩展,如:
- 添加持久化支持
- 实现模块化管理
- 增加 devtools 支持
- 提供更多的工具函数
总之,这个轻量级状态管理方案为我们提供了一种新的思路,让状态管理变得更加简单、灵活和高效。它是一种适合特定场景的解决方案,特别是对于那些不需要复杂状态管理功能的小型项目。
如果你正在寻找一个轻量级的状态管理方案,不妨尝试一下这种基于 Composition API 的实现方式,它可能会给你带来惊喜!