文章目录
-
- 一、Pinia 相关介绍
-
- 1.1 什么是 Pinia?
- 1.2 为什么要用 Pinia?
- 1.3 Pinia 存什么?
- 二、Pinia 基础使用
-
- 2.1 项目引入:四步初始化
- 2.2 定义仓库:两种写法
-
- 写法一:Option 对象式(类 Vuex 风格)
- 写法二:Setup 函数式(组合式 API 风格)
- 2.3 组件内使用仓库
- 三、待办任务列表实战案例
-
- 3.1 页面搭建(组件化思路)
- 3.2 Pinia 落地流程
- 3.3 功能分步实现
-
- 1)新增待办 —— TodoHeader.vue
- 2)删除待办 —— TodoMain.vue
- 3)单选与全选双向联动
- 4)底部统计与清除已完成 —— TodoFooter.vue
- 5)筛选 + 列表数据切换
- 全文总结
- 核心知识点复盘
一、Pinia 相关介绍
1.1 什么是 Pinia?
Pinia 是专为 Vue 生态打造的状态管理工具,用于实现跨组件通信和多组件共享数据。它是 Vuex 的继任者,由 Vue 核心团队维护,现已成为 Vue3 官方推荐的状态管理方案。
在理解 Pinia 之前,先回顾 Vue 的两大基础特性:
- 数据驱动视图:数据变了,页面自动更新
- 组件化开发:页面拆成一个个独立组件,每个组件各司其职
问题来了:当组件 A、B、C、D 都需要读写同一份公共数据时,靠父子组件传值(props / emit)会非常繁琐且难以维护。Pinia 就是用来解决这个问题的 —— 把公共数据抽到一个独立的"仓库"里,所有组件都能直接访问和修改,并且数据变更全程可追踪。
组件A ←→ Pinia仓库(公共数据) ←→ 组件B
组件C ←→ ←→ 组件D
1.2 为什么要用 Pinia?
使用 Pinia 有两个核心目的:
| 多组件数据同步 | 任意组件修改仓库数据后,所有引用该数据的组件都会自动更新 |
| 数据变更可追踪 | 配合 Vue DevTools 可回溯每一次数据变化,方便调试 |
1.3 Pinia 存什么?
一句话:存储多个组件共用的公共数据。比如:
- 用户登录状态(token、用户信息)
- 购物车商品列表
- 页面主题配置
- 多步骤表单的中间数据
组件私有的、不共享的数据,依然放在组件自身的 ref / reactive 里,不需要放进 Pinia。
二、Pinia 基础使用
2.1 项目引入:四步初始化
任何项目使用 Pinia,都需要走完这固定四步:
// main.js
import { createApp } from 'vue'
import App from './App.vue'
// 第1步:导入 createPinia 方法
import { createPinia } from 'pinia'
// 第2步:创建 Pinia 实例
const pinia = createPinia()
const app = createApp(App)
// 第3步:全局挂载
app.use(pinia)
// 第4步:挂载应用
app.mount('#app')
注意:app.use(pinia) 必须在 app.mount('#app') 之前执行,否则组件内调用仓库时会报错。
2.2 定义仓库:两种写法
引入仓库定义函数,然后创建并导出仓库:
import { defineStore } from 'pinia'
export const useXxxStore = defineStore('仓库唯一id', Setup函数 或 Option对象)
defineStore 有三个注意要点:
写法一:Option 对象式(类 Vuex 风格)
这种方式通过 state、getters、actions 三大核心配置来组织代码,和 Vue2 Options API 风格一致:
// store/stock.js
import { defineStore } from 'pinia'
export const useStockStore = defineStore('stock', {
// state:存放共享原始数据(必须是函数,返回一个对象)
state: () => ({
stock: 100
}),
// getters:基于 state 计算衍生数据(等效组件内的 computed)
getters: {
doubleStock: (state) => state.stock * 2
},
// actions:修改共享数据的函数,支持同步和异步操作
actions: {
addStock() {
// 通过 this 访问 state 和 getters
this.stock++
},
subStock() {
this.stock—
}
}
})
关键点解析:
- state 必须写成箭头函数返回对象的形式 state: () => ({ … }),确保每个组件调用时拿到独立的数据副本
- getters 接收 state 作为第一个参数,可直接通过 state.xxx 访问数据
- actions 内部通过 this 访问 state 和 getters,支持同步和异步逻辑
下表对比三个配置项的功能和对应关系:
| state | 存放原始共享数据 | data() |
| getters | 基于 state 计算衍生值 | computed |
| actions | 定义修改数据的函数 | methods |
写法二:Setup 函数式(组合式 API 风格)
直接复用 Vue3 组合式 API 的写法,更灵活、也更受 Vue3 开发者青睐:
// store/stock.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useStockStore = defineStore('stock', () => {
// 用 ref/reactive 声明响应式数据(等同于 state)
const stock = ref(20)
// 用 computed 计算衍生值(等同于 getters)
const doubleStock = computed(() => {
return stock.value * 2
})
// 普通函数修改数据(等同于 actions)
function addStock() {
stock.value++
}
function subStock() {
stock.value—
}
// 最后 return 暴露出去,组件才能访问
return {
stock,
doubleStock,
addStock,
subStock
}
})
两种写法如何选择? 如果你熟悉 Vue3 组合式 API,推荐 Setup 函数式;如果你从 Vuex 迁移过来或偏好 Options API,选 Option 对象式。两者功能完全等价。
2.3 组件内使用仓库
组件内使用仓库统一分三步:
<!– components/Add.vue –>
<script setup>
// 第1步:导入仓库
import { useStockStore } from '@/store/stock'
// 第2步:调用函数,获取仓库实例
const stockStore = useStockStore()
</script>
<template>
<div class="add">
<h3>Add 组件</h3>
<!– 第3步:通过实例读取数据和调用方法 –>
<p>已知库存数: {{ stockStore.stock }}</p>
<button @click="stockStore.addStock()">库存 +1</button>
</div>
</template>
再来一个减法组件,验证跨组件共享:
<!– components/Sub.vue –>
<script setup>
import { useStockStore } from '@/store/stock'
const stockStore = useStockStore()
</script>
<template>
<div class="sub">
<h3>Sub 组件</h3>
<p>已知库存数: {{ stockStore.stock }}</p>
<button @click="stockStore.subStock()">库存 -1</button>
</div>
</template>
在 App.vue 中汇总:
<script setup>
import Add from './components/Add.vue'
import Sub from './components/Sub.vue'
import { useStockStore } from '@/store/stock'
const stockStore = useStockStore()
</script>
<template>
<h1>根组件</h1>
库存总数:
<input type="number" v-model.number="stockStore.stock" />
<p>stock 的翻倍值 = {{ stockStore.doubleStock }}</p>
<hr />
<Add />
<hr />
<Sub />
</template>
效果验证:在 Add 组件中点击 “+1”,Sub 组件显示的数字会同步更新;在输入框中直接修改库存数,所有组件的显示也会同步。这就是 Pinia 的核心能力 —— 一处修改,处处响应。
三、待办任务列表实战案例
以上是基础语法,接下来通过一个完整的 Todo List(待办事项) 项目,把 Pinia 真正用起来。本章节同时给出 Option 和 Setup 两种写法的代码,方便对比学习。
3.1 页面搭建(组件化思路)
先将页面拆分为 3 个子组件:
| TodoHeader | TodoHeader.vue | 输入框,新增待办事项 |
| TodoMain | TodoMain.vue | 展示待办列表,单选/全选操作 |
| TodoFooter | TodoFooter.vue | 统计未完成数量,筛选切换,清除已完成 |
在 App.vue 中引入组装:
<script setup>
import './assets/style.css'
import TodoHeader from './componnets/TodoHeader.vue'
import TodoMain from './componnets/TodoMain.vue'
import TodoFooter from './componnets/TodoFooter.vue'
</script>
<template>
<div class="todoapp">
<TodoHeader />
<TodoMain />
<TodoFooter />
</div>
</template>
3.2 Pinia 落地流程
按照前面的四步法初始化 Pinia(安装 → 引入 → 创建 → 挂载),然后创建 todo 业务仓库。
Option 对象式仓库
// store/todo.js
import { defineStore } from 'pinia'
// 定义筛选类型常量,避免组件内硬编码字符串
export const TODO_TYPE = {
all: 'all', // 全部
unfinished: 'unfinished', // 未完成
finished: 'finished' // 已完成
}
export const useTodoStore = defineStore('todo', {
state: () => ({
// 当前筛选类型,默认显示全部
type: TODO_TYPE.all,
// 共享的待办列表
todos: [
{ id: 1, text: 'Buy milk', finished: false },
{ id: 2, text: 'Buy eggs', finished: true },
{ id: 3, text: 'Buy bread', finished: false }
]
}),
getters: {
// 判断是否全部选中(用于全选复选框)
isAll: (state) => state.todos.every((item) => item.finished),
// 未完成任务列表(用于底部统计)
unfinishedTodos: (state) => state.todos.filter((item) => !item.finished),
// 根据筛选类型,计算当前需要展示的列表
showTodos: (state) => {
switch (state.type) {
case TODO_TYPE.all:
return state.todos
case TODO_TYPE.unfinished:
return state.todos.filter((item) => !item.finished)
case TODO_TYPE.finished:
return state.todos.filter((item) => item.finished)
}
}
},
actions: {
// 新增待办
addTodo(text) {
this.todos.push({
text,
id: this.todos.length + 1,
finished: false
})
},
// 删除待办
delTodo(i) {
if (window.confirm('确认删除么')) {
this.todos.splice(i, 1)
}
},
// 批量切换完成状态(全选/取消全选)
toggleTodo(flag) {
this.todos.forEach((item) => (item.finished = flag))
},
// 清除所有已完成任务
clearTodo() {
this.todos = this.todos.filter((item) => !item.finished)
},
// 设置筛选类型
setType(type) {
this.type = type
}
}
})
Setup 函数式仓库(对比写法)
// store/todo.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const TODO_TYPE = {
all: 'all',
unfinished: 'unfinished',
finished: 'finished'
}
export const useTodoStore = defineStore('todo', () => {
const todos = ref([
{ id: 1, text: 'Buy milk', finished: false },
{ id: 2, text: 'Buy eggs', finished: true },
{ id: 3, text: 'Buy bread', finished: false }
])
const type = ref(TODO_TYPE.all)
const isAll = computed(() => todos.value.every((item) => item.finished))
const unfinishedTodos = computed(() => todos.value.filter((item) => !item.finished))
const showTodos = computed(() => {
switch (type.value) {
case TODO_TYPE.all:
return todos.value
case TODO_TYPE.unfinished:
return todos.value.filter((item) => !item.finished)
case TODO_TYPE.finished:
return todos.value.filter((item) => item.finished)
}
})
const addTodo = (text) => {
todos.value.push({ text, id: todos.value.length + 1, finished: false })
}
const delTodo = (i) => {
if (window.confirm('确认删除么')) {
todos.value.splice(i, 1)
}
}
const toggleTodo = (flag) => {
todos.value.forEach((item) => (item.finished = flag))
}
const clearTodo = () => {
todos.value = todos.value.filter((item) => !item.finished)
}
const setType = (selectType) => {
type.value = selectType
}
// Setup 写法的关键:最后必须 return 所有需要暴露的变量和方法
return {
todos, type, isAll, unfinishedTodos, showTodos,
addTodo, delTodo, toggleTodo, clearTodo, setType
}
})
两种写法的核心区别:
- Option 写法:state 中数据通过 this.xxx 访问,结构清晰分层
- Setup 写法:ref 数据需要通过 .value 访问,最后 return 暴露;优点是可以用 JS 原生语法组织逻辑(比如把相关功能抽成独立函数)
3.3 功能分步实现
下面以 Option 写法为例,逐一实现各项功能。
1)新增待办 —— TodoHeader.vue
<script setup>
import { useTodoStore } from '@/store/todo'
const todoStore = useTodoStore()
// 监听回车键,触发新增
const onEnter = (e) => {
const text = e.target.value.trim() // 去除首尾空格
if (!text) return alert('任务名称不能为空') // 非空校验
todoStore.addTodo(text) // 调用仓库新增方法
e.target.value = '' // 清空输入框
}
</script>
<template>
<header class="header">
<h1>todos</h1>
<input
@keydown.enter="onEnter"
class="new-todo"
placeholder="What needs to be finished?"
autofocus
/>
</header>
</template>
代码思路:用户在输入框按回车 → 触发 onEnter → 校验输入内容 → 调用仓库的 addTodo 方法 → 仓库往 todos 数组 push 一条新记录 → 所有引用该数组的组件自动更新。
2)删除待办 —— TodoMain.vue
删除逻辑在仓库 actions 中已定义好了,组件只需调用即可:
<button class="destroy" @click="todoStore.delTodo(index)"></button>
这里传入的 index 来自 v-for 循环的索引,仓库中通过 splice(i, 1) 删除对应项。删除前弹出确认框,防止误操作。
3)单选与全选双向联动
这是本案例最核心、最容易出错的功能点,涉及两条数据流:
流程图解:
单选(勾选某一条)
↓ getters.isAll 自动计算
→ 所有项都 finished=true 时 isAll=true
→ 全选复选框自动勾上(get)
全选(点击全选框)
↓ v-model 触发 set
→ 调用 actions.toggleTodo(flag)
→ 遍历所有项,批量赋 finished=flag(set)
单选 ↔ 全选 形成闭环联动
TodoMain.vue 关键代码:
<script setup>
import { computed } from 'vue'
import { useTodoStore } from '@/store/todo'
const todoStore = useTodoStore()
// 带 get/set 的完整计算属性,是实现双向联动的关键
const isAllSelected = computed({
// get:读取时自动触发 → 返回仓库 isAll 的值 → 控制全选框状态
get() {
return todoStore.isAll
},
// set:全选框被点击时触发 → 接收新的勾选状态 → 批量更新所有项
set(flag) {
todoStore.toggleTodo(flag)
}
})
</script>
<template>
<section class="main">
<!– 全选框通过 v-model 绑定带 get/set 的计算属性 –>
<input
v-model="isAllSelected"
id="toggle-all"
class="toggle-all"
type="checkbox"
/>
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list">
<li
v-for="(item, index) in todoStore.showTodos"
:key="item.id"
:class="{ completed: item.finished }"
>
<div class="view">
<!– 单选框直接 v-model 绑定 finished 字段 –>
<input v-model="item.finished" class="toggle" type="checkbox" />
<label>{{ item.text }}</label>
<button class="destroy" @click="todoStore.delTodo(index)"></button>
</div>
</li>
</ul>
</section>
</template>
为什么用带 get/set 的计算属性? 因为全选框需要"读取"仓库的计算结果,又需要"写入"触发批量更新。普通 computed 只有 get,无法响应 v-model 的双向绑定;写成 computed({ get, set }) 才能让全选框和仓库数据形成闭环。
仓库中对应的 getter 和 action:
// getters:判断是否所有项都已勾选
isAll: (state) => state.todos.every((item) => item.finished),
// actions:遍历所有项,统一设置 finished 状态
toggleTodo(flag) {
this.todos.forEach((item) => (item.finished = flag))
}
4)底部统计与清除已完成 —— TodoFooter.vue
<script setup>
import { useTodoStore, TODO_TYPE } from '@/store/todo'
const todoStore = useTodoStore()
</script>
<template>
<footer class="footer">
<!– 统计未完成任务数:直接用 getter 返回的数组取 length –>
<span class="todo-count">
<strong>{{ todoStore.unfinishedTodos.length }}</strong> item left
</span>
<!– 筛选标签:点击切换 type,动态绑定 selected 类名实现高亮 –>
<ul class="filters">
<li>
<a
@click="todoStore.setType(TODO_TYPE.all)"
href="#/"
:class="{ selected: todoStore.type === TODO_TYPE.all }"
>All</a>
</li>
<li>
<a
@click="todoStore.setType(TODO_TYPE.unfinished)"
href="#/active"
:class="{ selected: todoStore.type === TODO_TYPE.unfinished }"
>Active</a>
</li>
<li>
<a
@click="todoStore.setType(TODO_TYPE.finished)"
href="#/completed"
:class="{ selected: todoStore.type === TODO_TYPE.finished }"
>Completed</a>
</li>
</ul>
<!– 一键清除已完成任务 –>
<button class="clear-completed" @click="todoStore.clearTodo()">
Clear completed
</button>
</footer>
</template>
功能解析:
- 统计:unfinishedTodos 是 getters 中 filter 的结果,取 .length 即可得到未完成任务数量。因为是计算属性,数据变化时自动更新。
- 清除已完成:调用 clearTodo(),内部用 filter 过滤掉 finished: true 的项并重新赋值,触发响应式更新。
- 筛选高亮:仓库 state.type 记录当前筛选类型,点击 a 标签时调用 setType() 切换;模板中通过 :class="{ selected: todoStore.type === TODO_TYPE.xxx }" 动态添加高亮样式。
5)筛选 + 列表数据切换
这个功能的核心在于 getters 中的 showTodos:
showTodos: (state) => {
switch (state.type) {
case TODO_TYPE.all:
return state.todos // 全量
case TODO_TYPE.unfinished:
return state.todos.filter((item) => !item.finished) // 未完成
case TODO_TYPE.finished:
return state.todos.filter((item) => item.finished) // 已完成
}
}
工作流程:用户点击筛选标签 → setType() 修改 state.type → showTodos 计算属性自动重新计算 → TodoMain 中 v-for="item in todoStore.showTodos" 拿到新数据 → 列表重渲染。
要点:我们没有真的删除数据或创建新数组,只是基于 type + 原始 todos 计算出一个派生视图。原始数据始终完整保留,筛选只是"换个角度看数据"。这种模式在 Pinia 中非常常见。
全文总结
本文从零开始,系统梳理了 Pinia 的核心知识体系:
核心知识点复盘
| Pinia 定位 | Vue3 官方状态管理工具,替代 Vuex |
| 四步引入 | import → createPinia() → app.use(pinia) → mount |
| defineStore 参数 | 仓库唯一 id + Setup 函数 或 Option 对象 |
| Option 三大件 | state(数据)→ getters(计算)→ actions(修改) |
| Setup 写法 | ref/reactive → computed → 普通函数 → 最后 return 暴露 |
| 组件使用三步骤 | 导入仓库 → 调用获取实例 → 读取数据/调用方法 |
| 全选联动 | 带 get/set 的 computed + getters 计算全选状态 + actions 批量更新 |
| 筛选模式 | state 记录筛选类型,getters 根据类型动态过滤,不修改原始数据 |
本文基于 Vue3 + Pinia 实战项目整理,代码完整可运行,适合作为学习复盘和技术分享的参考资料。

![基于Vue的企业劳务派遣人员管理系统[Vue]-计算机毕业设计源码+LW文档-171主机测评](https://www.171host.com/wp-content/uploads/2026/10/20261030202223-6ae4fc7f2d624-220x150.jpg)