从源码视角来看Pinia!

一、Pinia 概览

Pinia 本质是:「基于 Vue3 响应式系统 + effectScope 的"全局可控副作用容器"」

Pinia 核心代码集中在:

Plain 复制代码
packages/pinia/src/
├── createPinia.ts
├── rootStore.ts
├── store.ts
├── subscriptions.ts
├── types.ts
Plain 复制代码
┌─────────────────────┐
│   用户 API 层        │  defineStore / storeToRefs
├─────────────────────┤
│   Store 实现层       │  setupStore / optionsStore
├─────────────────────┤
│   响应式 & 调度层     │  reactive / effectScope / watch
└─────────────────────┘

二、createPinia 全局容器

2.1 createPinia 核心

TypeScript 复制代码
export function createPinia() {
  const scope = effectScope(true)
  const state = scope.run(() => ref({}))!

  const pinia = markRaw({
    _e: scope,
    _s: new Map(), // store 注册
    state,
    install(app) {
      setActivePinia(pinia)
      app.provide(piniaSymbol, pinia)
    }
  })

  return pinia
}
① 全局 effectScope
TypeScript 复制代码
const scope = effectScope(true)

所有 store 的 effect / computed / watch,全部挂在这个全局 scope(作用域) 下

这意味着 pinia._e.stop() 就能一次性销毁所有 store 副作用

② 全局 state 容器
TypeScript 复制代码
const state = ref({})

Pinia 并不是每个 store 自己维护 root state。

而是 pinia 统一管理,然后由 storeId 区分 state 属于哪个 store:

TypeScript 复制代码
pinia.state.value[storeId] = storeState // storeId 就是我们通过 defineStore 创建 store 的第一个参数

三、defineStore 定义 store

TypeScript 复制代码
export function defineStore(id, setupOrOptions) {
  return function useStore() {
    const pinia = getActivePinia()
    if (!pinia._s.has(id)) {
      createStore(id, setupOrOptions, pinia)
    }
    return pinia._s.get(id)
  }
}

Pinia 内部其实有 ​两种 store​:

3.1 setupStore 核心流程

TypeScript 复制代码
function setupStore(id, setup, pinia) {
  const scope = effectScope()
  const store = reactive({})

  const setupResult = scope.run(() =>
    setup({ action })
  )

  for (const key in setupResult) {
    const prop = setupResult[key]
    store[key] = prop
  }

  pinia._s.set(id, store)
}
① 每个 store 自己也有一个 effectScope
TypeScript 复制代码
const scope = effectScope()

层级关系:

Plain 复制代码
Pinia Root Scope
 └── Store Scope
      ├── computed
      ├── watch
      └── effect

所以,store.$dispose() ⇒ stop 当前 store 的所有副作用,不影响其他 store

② store 是 reactive 包裹的对象
TypeScript 复制代码
const store = reactive({})

​注意:​ Pinia ​不包装 state,​ Pinia 包装的是整个 store

所以:

TypeScript 复制代码
store.count
store.double
store.increment

全部是同一个 reactive proxy。

如果通过 Setup Store 创建:

TypeScript 复制代码
const count = ref(0)
return { count }
TypeScript 复制代码
store.count === count // true // 本质上 Pinia 直接复用 Vue 原生响应式对象

如果是 options store :

TypeScript 复制代码
state: () => ({ count: 0 })
TypeScript 复制代码
pinia.state.value[id] = reactive(state())

然后:

TypeScript 复制代码
store.count -> toRef(pinia.state.value[id], 'count')

所以:

TypeScript 复制代码
store.count++ // 实际修改的是 pinia.state

四、getters 的底层原理(computed)

Options Store 的 getter:

TypeScript 复制代码
getters: {
  double: (state) => state.count * 2
}

源码实现本质:

TypeScript 复制代码
computed(() => {
  setActivePinia(pinia)
  return getter.call(store, store)
})

本质结论:

Pinia getter == Vue computed

  • 依赖收集由 Vue 完成
  • 脏检查、缓存完全交给 computed

五、actions

TypeScript 复制代码
actions: {
  increment() {
    this.count++
  }
}

源码:

TypeScript 复制代码
function wrapAction(name, action) {
  return function () {
    return action.apply(store, arguments)
  }
}

六、其他 API

6.1 $patch:为什么比直接改 state 更好?

TypeScript 复制代码
store.$patch({
  count: store.count + 1
})

源码:

TypeScript 复制代码
pauseTracking()
applyPatch() // 批量更新,暂停追踪依赖
resumeTracking()
triggerSubscriptions() // 统一触发

6.2 $subscribe / watch 的关系(副作用系统)

TypeScript 复制代码
store.$subscribe((mutation, state) => {})

源码:

TypeScript 复制代码
watch(
  () => pinia.state.value[id],
  (state) => callback(),
  { deep: true }
)

Pinia 的 subscribe 就是一个封装过的 watch

但多做了:mutation 类型、时间戳、devtools hook

6.3 storeToRefs:为什么不会丢响应式?

TypeScript 复制代码
const { count } = storeToRefs(store)

源码:

TypeScript 复制代码
if (isRef(value) || isReactive(value)) {
  refs[key] = toRef(store, key)
}

storeToRefs = 批量 toRef

七、Pinia vs Vuex(源码级简单对比)

相关推荐
Csvn29 分钟前
`??` 和 `||` 搞混,线上用户头像全挂了
前端
kyriewen1 小时前
白宫前脚下了限制令,OpenAI 后脚就把 GPT-5.6 发了
前端·gpt·openai
用户40269244819082 小时前
CRMEB Pro 新增后台接口全链路:路由、权限、验证器、返回格式一次讲清
前端·后端
泉城老铁2 小时前
springboot+vue+ ffmpeg 实现视频的拉流播放
前端
逸铭2 小时前
Day 5:三栏布局——左账号 / 中聊天 / 右工具
vue.js·electron
PedroQue993 小时前
uni-router v1.8.0新增冷启动守卫补执行
前端·uni-app
xiaok3 小时前
部署之后,本地浏览器还在读取旧缓存导致页面一直显示loading中
前端
用户059540174463 小时前
Redis缓存一致性踩坑实录:线上故障排查6小时,我用pytest+内存快照把它永久关进了笼子
前端·css
星栈3 小时前
我用 Rust + Dioxus 做了个全栈跨平台笔记应用:第一版先把列表和详情跑通
前端·rust·前端框架
用户1733598075373 小时前
Vue 3 SPA 首屏优化:从 3s 到 1.2s 的 5 个实践
前端·vue.js