一、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。
所以,模块作用域里的响应式数据,就是共享状态。
三、函数内部创建的是"局部状态"
对比一下:
js
export function useCounterStore() {
const count = ref(0);
function increment() {
count.value++;
}
return {
count,
increment
};
}
这次 count 写在函数里面。
每个组件调用 useCounterStore(),都会创建一份新的 count。
这不是共享状态,而是可复用的局部逻辑。
四、一眼区分全局状态和局部状态
看代码位置就行:
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,也就是服务端渲染,就要小心了。
免费好用的热门在线工具
- 文件格式转换器 - 应用商店 | By cmdragon
- M3U8在线播放器 - 应用商店 | By cmdragon
- 快图设计 - 应用商店 | By cmdragon
- 高级文字转图片转换器 - 应用商店 | By cmdragon
- RAID 计算器 - 应用商店 | By cmdragon
- 在线PS - 应用商店 | By cmdragon
- Mermaid 在线编辑器 - 应用商店 | By cmdragon
- 数学求解计算器 - 应用商店 | By cmdragon
- 智能提词器 - 应用商店 | By cmdragon
- 魔法简历 - 应用商店 | By cmdragon
- Image Puzzle Tool - 图片拼图工具 | By cmdragon
- 字幕下载工具 - 应用商店 | By cmdragon
- 歌词生成工具 - 应用商店 | By cmdragon
- 网盘资源聚合搜索 - 应用商店 | By cmdragon
- ASCII字符画生成器 - 应用商店 | By cmdragon
- JSON Web Tokens 工具 - 应用商店 | By cmdragon
- Bcrypt 密码工具 - 应用商店 | By cmdragon
- GIF 合成器 - 应用商店 | By cmdragon
- GIF 分解器 - 应用商店 | By cmdragon
- 文本隐写术 - 应用商店 | By cmdragon
- CMDragon 在线工具 - 高级AI工具箱与开发者套件 | 免费好用的在线工具
- 应用商店 - 发现1000+提升效率与开发的AI工具和实用程序 | 免费好用的在线工具
- CMDragon 更新日志 - 最新更新、功能与改进 | 免费好用的在线工具
- 支持我们 - 成为赞助者 | 免费好用的在线工具
- AI文本生成图像 - 应用商店 | 免费好用的在线工具
- 临时邮箱 - 应用商店 | 免费好用的在线工具
- 二维码解析器 - 应用商店 | 免费好用的在线工具
- 文本转思维导图 - 应用商店 | 免费好用的在线工具
- 正则表达式可视化工具 - 应用商店 | 免费好用的在线工具
- 文件隐写工具 - 应用商店 | 免费好用的在线工具
- IPTV 频道探索器 - 应用商店 | 免费好用的在线工具
- 快传 - 应用商店 | 免费好用的在线工具
- 随机抽奖工具 - 应用商店 | 免费好用的在线工具
- 动漫场景查找器 - 应用商店 | 免费好用的在线工具
- 时间工具箱 - 应用商店 | 免费好用的在线工具
- 网速测试 - 应用商店 | 免费好用的在线工具
- AI 智能抠图工具 - 应用商店 | 免费好用的在线工具
- 背景替换工具 - 应用商店 | 免费好用的在线工具
- 艺术二维码生成器 - 应用商店 | 免费好用的在线工具
- Open Graph 元标签生成器 - 应用商店 | 免费好用的在线工具
- 图像对比工具 - 应用商店 | 免费好用的在线工具
- 图片压缩专业版 - 应用商店 | 免费好用的在线工具
- 密码生成器 - 应用商店 | 免费好用的在线工具
- SVG优化器 - 应用商店 | 免费好用的在线工具
- 调色板生成器 - 应用商店 | 免费好用的在线工具
- 在线节拍器 - 应用商店 | 免费好用的在线工具
- IP归属地查询 - 应用商店 | 免费好用的在线工具
- CSS网格布局生成器 - 应用商店 | 免费好用的在线工具
- 邮箱验证工具 - 应用商店 | 免费好用的在线工具
- 书法练习字帖 - 应用商店 | 免费好用的在线工具
- 金融计算器套件 - 应用商店 | 免费好用的在线工具
- 中国亲戚关系计算器 - 应用商店 | 免费好用的在线工具
- Protocol Buffer 工具箱 - 应用商店 | 免费好用的在线工具
- IP归属地查询 - 应用商店 | 免费好用的在线工具
- 图片无损放大 - 应用商店 | 免费好用的在线工具
- 文本比较工具 - 应用商店 | 免费好用的在线工具
- IP批量查询工具 - 应用商店 | 免费好用的在线工具
- 域名查询工具 - 应用商店 | 免费好用的在线工具
- DNS工具箱 - 应用商店 | 免费好用的在线工具
- 网站图标生成器 - 应用商店 | 免费好用的在线工具
- XML Sitemap
原因是:服务端进程会同时处理很多用户请求。如果你把用户相关状态放在模块作用域的全局单例里,就可能出现请求之间互相污染。
想象一下:
js
// 危险示意:SSR 中不要这样保存用户状态
export const userStore = reactive({
currentUser: null
});
如果用户 A 的请求把 currentUser 改成 A,用户 B 的请求又复用了同一个服务端模块实例,就有机会读到错误状态。
图解一下:
这就是 SSR 中全局单例的风险。
七、SSR 下更安全的思路
SSR 项目里,通常要为每个请求创建独立的应用实例,也要让状态跟着请求隔离。
概念上是这样:
不要让所有请求共享同一个"用户状态盒子"。
这也是为什么真正生产级项目更推荐 Pinia:它对 SSR、开发工具、热更新、团队约定等场景都有更完整的支持。
八、这一章怎么选
你可以按这张表判断:
| 场景 | 推荐写法 |
|---|---|
| 单组件内部状态 | ref() / reactive() 写在组件里 |
| 多组件共享的小状态 | 模块作用域 reactive() 或组合式 store |
| 可复用但每个组件独立的逻辑 | 状态写在组合式函数内部 |
| SSR 或大型生产项目 | 优先 Pinia |
九、练习
写一个 useThemeStore():
- 全局状态:
theme,可选值是"light"或"dark"。 - 计算属性:
isDark。 - 动作:
toggleTheme()。 - 两个组件都调用
useThemeStore(),一个负责按钮切换,一个负责显示当前主题。
做完后,再把 theme 移到函数内部,观察两个组件是否还共享同一份状态。
这个实验能帮你彻底理解"模块作用域"和"函数内部"的区别。