一、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