Vue状态管理入门第四章:组合式store和SSR风险

一、store 不一定非得是 reactive 对象

上一章我们用了:

js 复制代码
export const store = reactive({
  count: 0
});

这很好懂,但不是唯一写法。

Vue 的响应式系统和组件层是解耦的。也就是说,ref()、reactive()、computed() 这些 API 不只能写在组件里,也能写在普通 JavaScript 模块里。

所以我们可以写:

js 复制代码
import { computed, ref } from "vue";

const count = ref(0);

const doubleCount = computed(() => count.value * 2);

function increment() {
  count.value++;
}

export function useCounterStore() {
  return {
    count,
    doubleCount,
    increment
  };
}

这就是一个组合式函数风格的 store。

二、模块作用域决定了"全局状态"

注意这行代码的位置:

js 复制代码
const count = ref(0);

它写在 useCounterStore() 外面。

这意味着它在模块加载时创建一次,后续每个组件调用 useCounterStore(),拿到的都是同一个 count。

flowchart TB Module[模块作用域<br/>const count = ref(0)] Module --> A[组件 A 调 useCounterStore] Module --> B[组件 B 调 useCounterStore] Module --> C[组件 C 调 useCounterStore]

所以,模块作用域里的响应式数据,就是共享状态。

三、函数内部创建的是"局部状态"

对比一下:

js 复制代码
export function useCounterStore() {
  const count = ref(0);

  function increment() {
    count.value++;
  }

  return {
    count,
    increment
  };
}

这次 count 写在函数里面。

每个组件调用 useCounterStore(),都会创建一份新的 count。

flowchart TB A[组件 A 调用] --> AState[count A] B[组件 B 调用] --> BState[count B] C[组件 C 调用] --> CState[count C]

这不是共享状态,而是可复用的局部逻辑。

四、一眼区分全局状态和局部状态

看代码位置就行:

js 复制代码
import { ref } from "vue";

// 全局状态:模块作用域,只创建一次
const globalCount = ref(0);

export function useCount() {
  // 局部状态:函数每调用一次就创建一次
  const localCount = ref(0);

  return {
    globalCount,
    localCount
  };
}

组件 A 和组件 B 调用后:

text 复制代码
globalCount 是同一份
localCount 是各自一份

这个区别非常重要。很多"为什么两个组件数据互相影响"或者"为什么两个组件数据不同步"的问题,根源都在这里。

五、组合式 store 的写法更适合组织复杂逻辑

比如购物车可以这样写:

js 复制代码
// src/stores/useCartStore.js
import { computed, ref } from "vue";

const items = ref([]);

const totalCount = computed(() =>
  items.value.reduce((sum, item) => sum + item.quantity, 0)
);

const isEmpty = computed(() => items.value.length === 0);

function addItem(product) {
  const matched = items.value.find((item) => item.id === product.id);

  if (matched) {
    matched.quantity++;
    return;
  }

  items.value.push({
    ...product,
    quantity: 1
  });
}

function removeItem(id) {
  items.value = items.value.filter((item) => item.id !== id);
}

function clearCart() {
  items.value = [];
}

export function useCartStore() {
  return {
    items,
    totalCount,
    isEmpty,
    addItem,
    removeItem,
    clearCart
  };
}

组件中使用:

vue 复制代码
<script setup>
import { useCartStore } from "../stores/useCartStore.js";

const cart = useCartStore();
</script>

<template>
  <p>购物车数量:{{ cart.totalCount }}</p>
  <button :disabled="cart.isEmpty" @click="cart.clearCart">
    清空
  </button>
</template>

它的好处是:

  • ref、computed、方法可以自然组合。
  • 返回值很明确。
  • 以后迁移到 Pinia 的组合式写法时,心智负担更小。

六、SSR 场景为什么要小心全局单例

如果你的 Vue 应用只跑在浏览器里,上面的全局状态通常没问题。

但如果你做 SSR,也就是服务端渲染,就要小心了。
免费好用的热门在线工具

想象一下:

js 复制代码
// 危险示意:SSR 中不要这样保存用户状态
export const userStore = reactive({
  currentUser: null
});

如果用户 A 的请求把 currentUser 改成 A,用户 B 的请求又复用了同一个服务端模块实例,就有机会读到错误状态。

图解一下:

sequenceDiagram participant A as 用户A请求 participant S as 服务端全局store participant B as 用户B请求 A->>S: currentUser = A B->>S: 读取 currentUser S-->>B: 可能拿到 A 的数据

这就是 SSR 中全局单例的风险。

七、SSR 下更安全的思路

SSR 项目里,通常要为每个请求创建独立的应用实例,也要让状态跟着请求隔离。

概念上是这样:

flowchart TB ReqA[请求 A] --> AppA[App 实例 A] --> StoreA[Store A] ReqB[请求 B] --> AppB[App 实例 B] --> StoreB[Store B]

不要让所有请求共享同一个"用户状态盒子"。

这也是为什么真正生产级项目更推荐 Pinia:它对 SSR、开发工具、热更新、团队约定等场景都有更完整的支持。

八、这一章怎么选

你可以按这张表判断:

场景 推荐写法
单组件内部状态 ref() / reactive() 写在组件里
多组件共享的小状态 模块作用域 reactive() 或组合式 store
可复用但每个组件独立的逻辑 状态写在组合式函数内部
SSR 或大型生产项目 优先 Pinia

九、练习

写一个 useThemeStore():

  • 全局状态:theme,可选值是 "light" 或 "dark"。
  • 计算属性:isDark。
  • 动作:toggleTheme()。
  • 两个组件都调用 useThemeStore(),一个负责按钮切换,一个负责显示当前主题。

做完后,再把 theme 移到函数内部,观察两个组件是否还共享同一份状态。

这个实验能帮你彻底理解"模块作用域"和"函数内部"的区别。

相关推荐
考虑考虑2 小时前
nginx路由重写
运维·后端·自动化运维
曹牧2 小时前
C#:空合并运算符
java·前端·数据库
JaguarJack3 小时前
PHP 紧急修复两项严重 RCE 漏洞,8.2 至 8.5 用户应立即升级
后端·php·服务端
小羊没烦恼!3 小时前
铁道部新客票系统设计(二)
开发语言·前端·数据库·python·oracle·c#·php
BingoGo3 小时前
PHP 紧急修复两项严重 RCE 漏洞,8.2 至 8.5 用户应立即升级
后端·php
金銀銅鐵3 小时前
[Java] 展示添加元素时 ArrayList 的内部变化
java·后端·ai编程
斌糖雪梨4 小时前
spring auth 核心流程详解
java·后端·spring
高频因子挖掘机5 小时前
本地已有一年历史数据,第二天更新别只拉“昨天一天”:量化 K 线增量同步实践
后端·github·api
高频因子挖掘机5 小时前
只补缺失日期,不重拉全部历史:增量更新 K 线应该怎么设计?
后端·github·api
智能直播6 小时前
RK3518:1.5G 内存 + 8G 存储,如何跑起硬件编解码、Web、AI 识别、绿幕特效
前端·人工智能·5g