在前面的文章中,我们学习了 Vue Router 与响应式系统的集成。今天,我们将探索 Pinia,这是 Vue 官方推荐的状态管理库。Pinia 充分利用 Vue3 的响应式系统,提供了简单、类型安全的状态管理方案。理解它的实现原理,将帮助我们更好地组织应用状态,写出更可维护的代码。
前言:状态管理的演进
随着应用规模增长,组件间共享状态变得越来越复杂:
在 Vue2 开发中,使用
Vuex 状态管理来解决组件共享状态问题;而 Vue3 则采用了 Pinia 的方式,为什么会有这层变化呢?
传统 Vuex 的问题
- 繁琐的 mutations:必须通过
mutations修改状态 - 类型支持差:TypeScript 体验不佳
- 模块化复杂:
namespaced概念增加心智负担 - 体积较大:包含大量模板代码
Pinia的优势:
- 直接修改状态:无需
mutations - 完美的类型推导:原生 TypeScript 支持
- 扁平化结构:没有嵌套模块
- 轻量高效:核心逻辑精简
Pinia 的设计理念与架构
Pinia 的本质
Pinia 本质上是一个 基于 Vue 3 响应式系统 + effectScope 的全局可控副作用容器 。它的核心目标是以最简洁的方式管理全局状态,同时保持类型安全和开发体验。
整体架构分层
Pinia 的源码架构可以清晰地分为三层:
这种分层设计使得
Pinia 既保持了上层 API 的简洁性,又能够充分利用 Vue 3 底层响应式系统的能力。
Pinia 如何利用 Vue 3 响应式系统
响应式核心:reactive 与 ref
Pinia 的状态管理完全建立在 Vue 3 的响应式 API 之上。当我们在 Pinia 中定义状态时,实际上是在创建 Vue 的响应式对象 :
// Pinia 内部的核心实现
import { reactive, ref } from 'vue'
// 选项式 Store 的 state 会被转换为 reactive
const state = reactive({
count: 0,
user: null
})
// 组合式 Store 直接使用 ref/reactive
const count = ref(0)
const user = ref(null)
Pinia 并不会重新发明一套响应式系统,而是直接复用 Vue 的响应式能力,这意味着:
- 状态变化自动触发视图更新:当
state变化时,所有依赖它的组件会自动重新渲染 - 依赖自动收集:
getters中访问state时,Vue 会自动收集依赖关系
effectScope:全局副作用管理
Pinia 的一个重要创新是使用 Vue 3 的 effectScope API 来管理所有 store 的副作用 :
// createPinia 源码简化
export function createPinia() {
// 创建全局 effectScope
const scope = effectScope(true)
// 全局 state 容器
const state = scope.run(() => ref({}))!
const pinia = markRaw({
_e: scope, // 全局 scope
_s: new Map(), // store 注册表
state, // 全局 state
install(app) {
app.provide(piniaSymbol, pinia)
}
})
return pinia
}
这种设计有以下优势:
- 统一管理:所有
store的computed、watch、effect都挂载在全局scope下 - 一键清理:调用
pinia._e.stop()即可销毁所有store的副作用 - 每个 store 独立 scope:每个
store还有自己的scope,支持独立销毁(store.$dispose())
Store 的创建与类型推导
defineStore 的核心逻辑
defineStore 是用户定义 store 的入口,它返回一个 useStore 函数 :
// defineStore 源码简化
export function defineStore(id, setupOrOptions) {
return function useStore() {
// 获取当前活跃的 pinia 实例
const pinia = getActivePinia()
// 单例模式:同一 id 的 store 只创建一次
if (!pinia._s.has(id)) {
createStore(id, setupOrOptions, pinia)
}
return pinia._s.get(id)
}
}
两种 Store 定义方式的实现
Pinia 支持两种定义 store 的方式:选项式 Store 和 组合式 Store,它们的底层实现略有不同 :
选项式 Store(Options Store)
// 用户定义
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0, name: 'Pinia' }),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})
// 内部处理逻辑
function createOptionsStore(id, options, pinia) {
const { state, getters, actions } = options
// 1. 初始化 state
pinia.state.value[id] = state ? state() : {}
// 2. 创建 store 实例
const store = reactive({})
// 3. 将 state 转换为 refs 挂载到 store
for (const key in pinia.state.value[id]) {
store[key] = toRef(pinia.state.value[id], key)
}
// 4. 处理 getters -> 转换为 computed
for (const key in getters) {
store[key] = computed(() => {
setActivePinia(pinia)
return getters[key].call(store, store)
})
}
// 5. 处理 actions -> 绑定 this
for (const key in actions) {
store[key] = wrapAction(key, actions[key])
}
return store
}
组合式 Store(Setup Store)
// 用户定义
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const name = ref('Pinia')
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, name, doubleCount, increment }
})
// 内部处理逻辑
function createSetupStore(id, setup, pinia) {
const scope = effectScope()
// 运行 setup 函数,创建响应式状态
const setupResult = scope.run(() => setup())
// 创建 store 实例(reactive 包裹整个 store)
const store = reactive({})
// 将 setup 返回的属性挂载到 store
for (const key in setupResult) {
const prop = setupResult[key]
store[key] = prop
}
pinia._s.set(id, store)
return store
}
类型推导的实现
Pinia 的类型推导之所以强大,是因为它充分利用了 TypeScript 的 类型推断 和 条件类型 :
// 简化的类型定义
export function defineStore<Id, S, G, A>(
id: Id,
options: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>
): StoreDefinition<Id, S, G, A>
// 使用时的类型推导
const store = useCounterStore()
// TypeScript 自动推导出:
// store.count: number
// store.doubleCount: number
// store.increment: () => void
Actions 的实现原理
Action 的本质
Pinia 中的 actions 就是普通的函数,但它们的 this 被自动绑定到了 store 实例上 :
// 源码中的 action 包装
function wrapAction(name, action) {
return function(this: any) {
// 绑定 this 为当前 store
return action.apply(this, arguments)
}
}
同步与异步 Action
Pinia 的 actions 天然支持同步和异步操作,无需任何特殊处理 :
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
loading: false
}),
actions: {
// 同步 action
setUser(user) {
this.user = user
},
// 异步 action
async fetchUser(id) {
this.loading = true
try {
const response = await fetch(`/api/users/${id}`)
this.user = await response.json()
} finally {
this.loading = false
}
}
}
})
Actions 的订阅机制
Pinia 提供了 $onAction 方法来订阅 actions 的执行 :
// 源码简化
store.$onAction(({ name, store, args, after, onError }) => {
console.log(`Action ${name} 开始执行`)
after((result) => {
console.log(`Action ${name} 执行完成`, result)
})
onError((error) => {
console.error(`Action ${name} 执行失败`, error)
})
})
Getters 的实现原理
Getter 的本质是 computed
Pinia 的 getters 底层就是 Vue 的 computed 属性:
// 源码中的 getter 处理
for (const key in getters) {
store[key] = computed(() => {
// 确保当前 pinia 实例活跃
setActivePinia(pinia)
// 调用 getter 函数,绑定 this 为 store
return getters[key].call(store, store)
})
}
这意味着 getters 具备 computed 的所有特性:
- 缓存性:只有依赖变化时才重新计算
- 懒计算:只有在被访问时才执行
- 响应式依赖收集:自动追踪依赖的
state
Getter 的互相调用
getters 之间可以相互调用,就像计算属性可以组合一样 :
getters: {
doubleCount: (state) => state.count * 2,
// 通过 this 访问其他 getter
quadrupleCount(): number {
return this.doubleCount * 2
}
}
Pinia vs Vuex:核心差异对比
设计理念对比
| 维度 | Pinia | Vuex |
|---|---|---|
| API 设计 | 简洁直观,无 mutations | 严格区分 state/getters/mutations/actions |
| TypeScript 支持 | 原生支持 | 需要手动声明类型,支持有限 |
| 模块化 | 多 store 自然拆分 | 单一 store + 模块嵌套 |
| 响应式系统 | 直接使用 reactive/computed | 内部实现响应式 |
| 代码体积 | 轻量(约1KB) | 相对较大 |
核心差异详解
Mutations 的废除
Pinia 最大的改变是移除了 mutations。在 Vuex 中,修改状态必须通过 mutations(同步)和 actions(异步):
Vuex 方式:
mutations: {
add(state) {
state.count++
}
},
actions: {
increment({ commit }) {
commit('add')
}
}
而在 Pinia 中,actions 可以直接修改状态:
actions: {
increment() {
this.count++ // 直接修改
}
}
模块化设计
- Vuex:单一
store,通过modules拆分,需要处理命名空间 - Pinia:每个
store独立,按需引入,天然支持代码分割
TypeScript 支持
Pinia 在设计之初就充分考虑 TypeScript,几乎所有 API 都支持类型推导。
源码简析:Pinia 的核心逻辑
createPinia:全局容器创建
// 源码简化自 pinia/src/createPinia.ts
export function createPinia() {
const scope = effectScope(true)
// 全局状态容器
const state = scope.run(() => ref({}))
const pinia = markRaw({
// 唯一标识
__pinia: true,
// 全局 effectScope
_e: scope,
// store 注册表
_s: new Map(),
// 全局状态
state,
// 插件数组
_p: [],
// Vue 插件安装方法
install(app) {
// 设置为当前活跃 pinia
setActivePinia(pinia)
// 通过 provide 注入
app.provide(piniaSymbol, pinia)
// 挂载 $pinia 到全局属性
app.config.globalProperties.$pinia = pinia
// 使用效果域来管理响应式
pinia._e.run(() => {
app.runWithContext(() => {
// 初始化
})
})
}
})
return pinia
}
响应式 store 的创建过程
// 源码简化自 pinia/src/store.ts
function createStore(id, options, pinia) {
// 创建 store 的作用域
const scope = effectScope()
// 创建 store 实例(整个 store 是 reactive 的)
const store = reactive({})
// 初始化 state
pinia.state.value[id] = options.state ? options.state() : {}
// 将 state 转换为 ref 并挂载
for (const key in pinia.state.value[id]) {
store[key] = toRef(pinia.state.value[id], key)
}
// 处理 getters(转换为 computed)
if (options.getters) {
for (const key in options.getters) {
store[key] = computed(() => {
setActivePinia(pinia)
return options.getters[key].call(store, store)
})
}
}
// 处理 actions(绑定 this)
if (options.actions) {
for (const key in options.actions) {
store[key] = function(...args) {
return options.actions[key].apply(store, args)
}
}
}
// 缓存 store 实例
pinia._s.set(id, store)
return store
}
storeToRefs 的实现原理
为什么直接从 store 解构会失去响应式?因为 store 本身是一个 reactive 对象,解构会得到原始值 。storeToRefs 的源码揭示了解决方案:
// 源码简化自 pinia/src/storeToRefs.ts
export function storeToRefs(store) {
// 将 store 转换为原始对象,避免重复代理
store = toRaw(store)
const refs = {}
for (const key in store) {
const value = store[key]
// 只转换响应式数据(state 和 getters)
if (isRef(value) || isReactive(value)) {
// 使用 toRef 保持响应式连接
refs[key] = toRef(store, key)
}
}
return refs
}
这个实现的核心在于:
- toRaw(store):脱掉
store的Proxy外壳,获取原始对象 - 只转换响应式数据:过滤掉
actions等非响应式属性 - toRef 包装:创建
ref引用,保持与原始数据的响应式连接
结语
Pinia 的成功告诉我们,优秀的状态管理库不一定要复杂,而是要在保持简洁的同时,充分利用框架底层的能力。理解 Pinia 的响应式原理,不仅有助于我们更好地使用它,也为我们在实际项目中设计和封装自己的组合式函数提供了思路和借鉴。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!