pinia 核心源码
记录pinia核心源码阅读笔记,这里跳过hmr(热更新), mapHelpers(class 工具)等工具源码。 剔除的部分vue2.0兼容代码。 当前pinia版本2.0.13
执行流程概述
- 创建pinia实例,挂载到vue
- 定义state
- 创建组件
- 调用useState
- 生成并缓存pinia
- 注销组件
- 注销监听
pinia.png
rootStore.js
这里主要提供 activePinia(当前可用pinia实例)缓存对象。 并提供两个操作方法,
setActivePinia 更新 activePinia
代码语言:javascript复制export const setActivePinia = (pinia: Pinia | undefined) =>
(activePinia = pinia)
getActivePinia 获取 activePinia
代码语言:javascript复制export const getActivePinia = () =>
// 这里优先返回全局注册的pinia实例
(getCurrentInstance() && inject(piniaSymbol)) || activePinia
subscriptions.ts
响应事件相关, 提供两个方法
addSubscription
代码语言:javascript复制// 向当前state事件队列中注册事件回调
export function addSubscription<T extends _Method>(
subscriptions: T[],
callback: T,
detached?: boolean,
onCleanup: () => void = noop
) {
subscriptions.push(callback)
const removeSubscription = () => {
const idx = subscriptions.indexOf(callback)
if (idx > -1) {
subscriptions.splice(idx, 1)
onCleanup()
}
}
// 默认组件注销时,清理事件回调
if (!detached && getCurrentInstance()) {
onUnmounted(removeSubscription)
}
return removeSubscription
}
triggerSubscriptions
代码语言:javascript复制// 执行事件队列
export function triggerSubscriptions<T extends _Method>(
subscriptions: T[],
...args: Parameters<T>
) {
subscriptions.slice().forEach((callback) => {
callback(...args)
})
}
createPinia.ts
创建pinia实例
代码语言:javascript复制export function createPinia(): Pinia {
// 创建响应式空间,空值pinia相关的响应对象的有效性
const scope = effectScope(true)
// state缓存空间, 生成的store将缓存到该队列中
// 当使用useState是,将通过注册的id,从stateTrue
// 中查询对应的store,保证不同组件使用相同的store
const state = scope.run<Ref<Record<string, StateTree>>>(() =>
ref<Record<string, StateTree>>({})
)!
// 插件队列
let _p: Pinia['_p'] = []
let toBeInstalled: PiniaPlugin[] = []
// 创建pinia实例
// markRaw 保证pinia不会被代理
const pinia: Pinia = markRaw({
// 将pinia实例注册到Vue实例中
install(app: App) {
// 激活当前pinia实例,
setActivePinia(pinia)
if (!isVue2) {
// 设置vue实例
pinia._a = app
// 通过依赖注入设置全局默认pinia实例
// 后面useState会用到
app.provide(piniaSymbol, pinia)
// 挂载全局pinia实例
app.config.globalProperties.$pinia = pinia
if (__DEV__ && IS_CLIENT) {
registerPiniaDevtools(app, pinia)
}
// 添加插件
toBeInstalled.forEach((plugin) => _p.push(plugin))
toBeInstalled = []
}
},
// 注册插件
use(plugin) {
if (!this._a && !isVue2) {
toBeInstalled.push(plugin)
} else {
_p.push(plugin)
}
return this
},
// 插件集合
_p,
// 应用实例
_a: null,
// 响应空间
_e: scope,
// store 队列
_s: new Map<string, StoreGeneric>(),
// state配置队列, 用于重置state
state,
})
return pinia
}
这里主要创建pinia实例,如果pinia实例被注册要vue应用实例时,将执行一些初始值设置,依赖注册pinia实例,以供useState使用
store.ts
pinia状态, 主要包括三个核心
- defineStore 定义状态
- createOptionsStore 对象型状态生成函数
defineStore(id, {state, getter, action})
- createSetupStore 函数型状态生成函数
defineStore(id, () => { setup(){} })
defineStore 定义store
defineStore 只做了两件事
- 参数处理
- 构建useState函数 这里主要看useState做了什么
// 通过配置类型判断配置类型
const isSetupStore = typeof setup === 'function'
...
// useState 可接收一个pinia实例作为参数
// 如果设置参数pinia,将通过依赖注入获取全局默认pinia实例
pinia =
(__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
(currentInstance && inject(piniaSymbol))
// 激活当前pinia实例
if (pinia) setActivePinia(pinia)
// 通过 id查询对应的store是否已经创建
if (!pinia._s.has(id)) {
// 如果未在当前pinia上查到对应store, 将根据参数类型创建store
if (isSetupStore) {
// 函数型
createSetupStore(id, setup, options, pinia)
} else {
// 对象型
createOptionsStore(id, options as any, pinia)
}
...
}
// 如果store存在返回该实例
const store: StoreGeneric = pinia._s.get(id)!
...
return store as any
// 其他
// 在创建useStore函数后
// 将当前id挂载useStore.$id属性上
useStore.$id = id
createOptionsStore 对象型store生成
这个函数其实是createSetupStore的包装函数, 将对象型的定义转为函数型 再交由createOptionsStore生成store
store生成
代码语言:javascript复制// 这里会先将options转为setup函数
// 通过createSetupStore生成store实例
store = createSetupStore(id, setup, options, pinia, hot)
// 绑定重置函数
store.$reset = function $reset() {
// state 这里是闭包
const newState = state ? state() : {}
// this指向store
// this.$patch 是state更新函数,
this.$patch(($state) => {
// 将原state与现有state合并,将state部分属性值重置
assign($state, newState)
})
}
setup函数
这里看setup函数做了什么
代码语言:javascript复制function setup() {
...
// 初始将state缓存到当前pinia.state中
pinia.state.value[id] = state ? state() : {}
// 将state转未ref
const localState = toRefs(pinia.state.value[id])
// 返回响应对象
return assign(
localState, // state => Refs(state)
actions, // actions => actions
// 遍历getters, 将属性包裹一层computed
Object.keys(getters || {}).reduce((computedGetters, name) => {
// markRow 防止对象被重复代理
computedGetters[name] = markRaw(
computed(() => {
// pinia 处于闭包
setActivePinia(pinia)
// it was created just before
const store = pinia._s.get(id)!
// 将执行函数绑定在store上下文中,支持 {getters: { fn(){ this.count } }} 模式
// 所以当使用箭头函数时不能使用this获取state
// 函数接收state作为参数, 支持{gtters: { f(state){state.count } }}
// 返回getter执行结果
return getters![name].call(store, store)
})
)
return computedGetters}
{}
)
}
所以setup主要作用是 1.将getter包裹computed, 2.返回新的store定义,通过getter的包装过程,知道了为什么箭头函数不能使用this模式,主要应为箭头函数的this原定义上下文绑定,后期无法通过call函数绑定到state上。
createSetupStore 函数型store生成
生成并挂载store实例
公共变量
代码语言:javascript复制let isListening: boolean // 监听函数执行时机标识
let isSyncListening: boolean // 监听函数执行时机标识
// state 更新响应队列,缓存¥subscribe挂载的任务
let subscriptions: SubscriptionCallback<S>[] = markRaw([])
// actions 响应事件队列, 缓存$onAction挂载的任务
let actionSubscriptions: StoreOnActionListener<Id, S, G, A>[] = markRaw([])
// debugger 事件队列
let debuggerEvents: DebuggerEvent[] | DebuggerEvent
// 初始缓存state
const initialState = pinia.state.value[$id] as UnwrapRef<S> | undefined
store 实例
因为createSetupStore的主要功能就是生成store实例,所以这里先看生成的store 主要步骤
代码语言:javascript复制// 如果state不存在,设置默认值
if (!buildState && !initialState && (!__DEV__ || !hot)) {
pinia.state.value[$id] = {}
}
// store基础方法属性
// 这里主要定义store实力的操作API
const partialStore = {
_p: pinia,
// action 响应事件注册函数
$onAction: addSubscription.bind(null, actionSubscriptions),
// state 更新函数
$patch,
// 重置store
$reset,
// 注册响应修改监听
$subscribe(callback, options = {}) {...},
// 注销store
$dispose,
}
// 转为响应对象
const store: Store<Id, S, G, A> = reactive(
assign({}, partialStore)
)
// 缓存store, useState通过当前激活的pinia获取到store
pinia._s.set($id, store)
// 合并store
// setupStore为setup()执行处理后配置对象
// 主要是对action的包装以及部分属性的合并
assign(store, setupStore)
// 这里为了 storeToRefs, 将响应属性合并到store原对象上
// storeToRefs 将先取得toRaw(store)再说Refs处理
assign(toRaw(store), setupStore)
// 绑定$state属性
Object.defineProperty(store, '$state', {
get: () => pinia.state.value[$id],
set: (state) => {
$patch(($state) => {
assign($state, state)
})
},
})
这里剔除的具体的方法定义,和周期函数的调用,主要看store的基础生成。
$patch state更新
代码语言:javascript复制 function $patch(
partialStateOrMutator:
| _DeepPartial<UnwrapRef<S>>
| ((state: UnwrapRef<S>) => void)
): void {
let subscriptionMutation: SubscriptionCallbackMutation<S>
// 阻止$subscribe监听事件执行
// 防止重复触发
// 保证$subscribe在完整合并后再执行
isListening = isSyncListening = false
if (__DEV__) {
debuggerEvents = []
}
// 如果状态修改器为函数,执行并生成修改类型
if (typeof partialStateOrMutator === 'function') {
// 例如 $reset()
partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
// 函数更新类型
subscriptionMutation = {
type: MutationType.patchFunction,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
} else {
// 如果状态修改器为对象, 合并到新state中
// mergeReactiveObjects将递归合并对象内的属性
mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
// 对象更新类型
subscriptionMutation = {
type: MutationType.patchObject,
payload: partialStateOrMutator,
storeId: $id,
events: debuggerEvents as DebuggerEvent[],
}
}
// 开启监听锁
nextTick().then(() => {
isListening = true
})
isSyncListening = true
// 应为之前关闭了watch监听, 所以这里需要手动执行一次监听队列
triggerSubscriptions(
subscriptions,
subscriptionMutation,
pinia.state.value[$id] as UnwrapRef<S>
)
}
subscribe绑定的事件将在state更新后被执行一次
$subscribe 更新监听
代码语言:javascript复制 $subscribe(callback, options = {}) {
// 向任务队列中添加任务, 并返回移除函数
const removeSubscription = addSubscription(
subscriptions,
callback,
options.detached,
// 这里有个问题 stopWatcher 先于定义,const应该存在假死区
() => stopWatcher()
)
// 挂载更新监听
const stopWatcher = scope.run(() =>
watch(
() => pinia.state.value[$id] as UnwrapRef<S>,
(state) => {
// 更新锁, patch时禁用更新监听
if (options.flush === 'sync' ? isSyncListening : isListening) {
callback(
{
storeId: $id,
type: MutationType.direct,
events: debuggerEvents as DebuggerEvent,
},
state
)
}
},
assign({}, $subscribeOptions, options)
)
)!
return removeSubscription
}
wrapAction
action 包装函数,主要为了提供 $onAction 监听钩子, 该函数在setupStore生成时被调用
代码语言:javascript复制 function wrapAction(name: string, action: _Method) {
return function (this: any) {
setActivePinia(pinia)
const args = Array.from(arguments)
// action执行后回调队列
const afterCallbackList: Array<(resolvedReturn: any) => any> = []
// 错误回调队列
const onErrorCallbackList: Array<(error: unknown) => unknown> = []
// action执行后回调添加函数
function after(callback: _ArrayType<typeof afterCallbackList>) {
afterCallbackList.push(callback)
}
// 错误回调添加函数
function onError(callback: _ArrayType<typeof onErrorCallbackList>) {
onErrorCallbackList.push(callback)
}
// 执行action任务队列
triggerSubscriptions(actionSubscriptions, {
args,
name,
store,
after,
onError,
})
let ret: any
try {
ret = action.apply(this && this.$id === $id ? this : store, args)
} catch (error) {
triggerSubscriptions(onErrorCallbackList, error)
throw error
}
// 异步函数处理
if (ret instanceof Promise) {
return ret
.then((value) => {
triggerSubscriptions(afterCallbackList, value)
return value
})
.catch((error) => {
triggerSubscriptions(onErrorCallbackList, error)
return Promise.reject(error)
})
}
triggerSubscriptions(afterCallbackList, ret)
return ret
}
}
执行流程 $onAction监听队列 -> action -> after任务队列 or error任务队列 应为onAction本身可以看作 beforeCallbackList, action的前置监听队列
其他钩子
plugins
代码语言:javascript复制// 生成store后将执行插件函数
pinia._p.forEach((extender) => {...}
hydrate
代码语言:javascript复制// 执行plugins后执行合并函数
(options as DefineStoreOptions<Id, S, G, A>).hydrate!(
store.$state,
initialState
)
总结
pinia核心代码并不多,主要功能放在了store生成,钩子包装。 值得注意的是:
- pinia实例的调用
- scope 空值响应作用空间
- 钩子的调度
- 兼容支持
疑问
$subscribe 监听中 stopWatcher 变量先于定义
代码语言:javascript复制 const removeSubscription = addSubscription(
....
() => stopWatcher()
)
const stopWatcher = scope.run(() =>{...})
部分属性遍历上是否可以用其他的方法
代码语言:javascript复制// 使用了 for in 遍历,将获取到原型上方法
for (const key in patchToApply) {
if (!patchToApply.hasOwnProperty(key)) continue
const subPatch = patchToApply[key]
const targetValue = target[key]