Libon

Pinia 是如何被实现的

31 分钟 #JavaScript#pinia
学习 Vue3 状态管理库 Pinia 的实现原理

ToC

Init

Pinia 官方文档中的 Getting StartedWhat is Pinia? 一节中,我们可以看到 Pinia 的基本使用方式的代码,比如全局注册:

1
import { createApp } from 'vue'
2
import { createPinia } from 'pinia'
3
import App from './App.vue'
4
5
const pinia = createPinia() // 创建一个应用级 pinia 的实例
6
const app = createApp(App)
7
8
app.use(pinia) // 将 pinia 实例挂载到 app 上
9
app.mount('#app')

定义 store

1
import { defineStore } from 'pinia'
2
3
export const useCounterStore = defineStore('counter', {
4
state: () => {
5
return { count: 0 }
6
},
7
// state: () => ({ count: 0 })
8
actions: {
9
increment() {
4 collapsed lines
10
this.count++
11
},
12
},
13
})

使用 store

1
<script setup>
2
import { useCounterStore } from '@/stores/counter'
3
const counter = useCounterStore()
4
counter.count++
5
counter.$patch({ count: counter.count + 1 })
6
counter.increment()
7
</script>
8
<template>
9
<div>Current Count: {{ counter.count }}</div>
1 collapsed line
10
</template>

从基本的使用方法来看,主要的 API 其实就只有两个:createPinia defineStore,那么我们就从这两个 API 入手,来看看 Pinia 是如何被实现的。

createPinia

为了降低理解成本,后续的代码都会去除 ts 类型的定义,只保留核心及上下文逻辑,同时因为会增加很多的注释来解释代码,所以在代码中也会增加很多的空行,避免代码过于密集,影响阅读

1
/**
2
* 必须调用setActivePinia来处理诸如 "fetch"、"setup"、"serverPrefetch" 等函数顶部的SSR
3
*/
4
export let activePinia
5
6
/**
7
* 设置或清空 active pinia, 在SSR和内部调用 action 和 getter 时使用
8
*
9
* @param pinia - Pinia 实例
103 collapsed lines
10
*/
11
export const setActivePinia = (pinia) => (activePinia = pinia)
12
13
// createPinia 实际上是一个工厂函数,每次调用的时候都会返回一个新的 pinia 实例对象
14
export function createPinia() {
15
// effectScope 用于创建一个新的 effect scope, 用于隔离副作用
16
// 其本身是一个比较高级的API,可以参考这个 RFC: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md
17
// 在正常的项目开发中基本用不到,但是在开发一些库或者依赖的时候可以更方便地收集依赖在运行时增加的副作用函数或响应式对象变量
18
// 在实例被销毁的时候,会自动清理掉这些副作用函数和响应式对象
19
// 它只接收一个 `detached` 参数,用于表示是否将这些副作用函数和响应式对象从当前的 effect scope 中独立出来,而不是和当前渲染的实例绑定,这样做的好处是在某个时机可以手动一次性清理掉这些副作用函数和响应式对象
20
const scope = effectScope(true)
21
22
// 在这里,我们可以检查 window object 的状态,如果Vue3 SSR有类似的情况,可以直接设置它
23
const state = scope.run(() => ref({}))
24
25
// 在 pinia 实例注册到 vue app 中的时候需要注册的插件列表
26
let _p = []
27
28
// plugins added before calling app.use(pinia)
29
// 在调用app.use(pinia)之前添加的插件,像这样👇:
30
/**
31
* const app = createApp(App)
32
* const pinia = createPinia()
33
* pinia.use(xxx) // 👈 xxx 就会被放到这里
34
* app.use(pinia)
35
*/
36
let toBeInstalled = []
37
38
// pinia 则是创建出来的 pinia 实例
39
// markRaw 则是将一个对象标记为不可响应的,底层实现就是将对象的 __v_skip 属性设置为 true
40
// 这样即便它被 reactive/ref 包装以后,它也仍然是一个普通对象 👇:
41
/**
42
* const pinia = reactive(createPinia())
43
* console.log(isReactive(pinia)) // false
44
*/
45
const pinia: Pinia = markRaw({
46
// app.use(pinia) 的时候会调用 install 方法
47
install(app: App) {
48
49
// 这允许在安装pinia的插件后,在组件设置之外调用 useStore()
50
setActivePinia(pinia)
51
52
// 如果不是 Vue2 才会执行这里的逻辑
53
// Vue2 是通过全局 mixin 的方式来实现的,文章后面会讲到
54
if (!isVue2) {
55
56
// 将当前 vue app 实例缓存到 pinia._a,以便于能找到正确的上下文
57
// 因为 pinia 像 vue app 一样都是可以创建多个的
58
pinia._a = app
59
60
// 将pinia实例挂载到 app.provide 上,以便于在组件中 defineStore 返回的函数中通过 inject 获取注册的数据
61
app.provide(piniaSymbol, pinia)
62
63
// 注册一个全局属性,以便于 OptionsAPI 中可以通过 this.$pinia 访问到 pinia 实例
64
// 或者直接在模板中通过 $pinia.state.value.xxx 来访问 xxx 模块的数据,示例如下:
65
/**
66
* <div>computer: {{ $pinia.state.value.computer }}</div>
67
*/
68
app.config.globalProperties.$pinia = pinia
69
70
// 如果不是 SSR 环境并且是开发环境则注册 devtools 面板
71
// 关于 vue-devtools 的使用暂且按下不表,有兴趣的话可以后期重新开一篇文章来讲解
72
if (__USE_DEVTOOLS__ && IS_CLIENT) {
73
registerPiniaDevtools(app, pinia)
74
}
75
76
// 把注册前使用的 pinia 插件拷贝到 _p 将要注册的插件数组中
77
toBeInstalled.forEach((plugin) => _p.push(plugin))
78
79
// 清空注册前使用的 pinia 插件数组,避免重复注册以及 AO 内存泄露
80
toBeInstalled = []
81
}
82
},
83
84
// use 方法和 vue app 一样,用于注册 pinia 插件
85
use(plugin) {
86
// 如果还没有 .use(pinia) 则先把插件存储到 toBeInstalled 数组中
87
if (!this._a && !isVue2) {
88
toBeInstalled.push(plugin)
89
} else {
90
// 如果已经 .use(pinia) 则直接注册插件
91
_p.push(plugin)
92
}
93
94
// 返回 this,方便链式调用
95
return this
96
},
97
98
_p, // plugins
99
_a: null, // app
100
_e: scope, // effectScope
101
_s: new Map(), // store map, 方便通过 id 来获取到对应的 store
102
state, // 实际上是一个 Ref<Record<string, Record<string | number | symbol, any>>> 类型, 和 _s 是同一类型的数据
103
})
104
105
// pinia devtools依赖于仅限开发的功能,因此除非使用Vue的开发构建,否则不能强制使用这些功能。避免使用像IE11这样的旧浏览器。
106
if (__USE_DEVTOOLS__ && typeof Proxy !== 'undefined') {
107
pinia.use(devtoolsPlugin)
108
}
109
110
// 返回实例对象,这样就可以 .use(pinia) 了
111
return pinia
112
}

既然提到了创建,那就顺便看看销毁pinia的 disposePinia 方法吧,虽然这个方法非常少用,大多数只在做测试的时候才会用到,但是了解一下也是好的,万一什么时候就用到了呢?

1
/**
2
* 通过停止其 effectScope 并删除状态、插件和存储来处理 Pinia 实例。这在测试 pinia 或常规 pinia 的测试中以及在使用多个 pinia 实例的应用中都非常有用。
3
*
4
* @param pinia - pinia 实例
5
*/
6
export function disposePinia(pinia: Pinia) {
7
pinia._e.stop() // 停止 effectScope, 销毁收集到的所有副作用
8
pinia._s.clear() // 清空 state map
9
pinia._p.splice(0) // 清空 plugins
3 collapsed lines
10
pinia.state.value = {} // 清空 state
11
pinia._a = null // 置空 app
12
}

defineStore

相比之下,defineStore 的代码会多很多,因为 pinia 要处理对象 state|actions|getters,而且 defineStore 函数不仅支持传入一个对象,还支持传入一个函数,在实现上也会有一些差异,所以我们分开来看,但是为了避免解析和最新版本的代码出现对应不上的问题,所以在查看代码的对照源代码的时候最好是使用指定 commit 的代码,比如这次用的 commit 就是 fix: support webpack minification

core

1
export function defineStore(
2
idOrOptions,
3
setup,
4
setupOptions
5
): StoreDefinition {
6
let id
7
let options
8
9
// 判断第二个参数是否是函数类型的 store
106 collapsed lines
10
const isSetupStore = typeof setup === 'function'
11
12
// 判断第一个参数是不是字符串, 如果是则表示是 id
13
if (typeof idOrOptions === 'string') {
14
id = idOrOptions
15
// 如果第一个参数是字符串, 那么再判断第二个参数是不是函数
16
// 如果是函数, 则第三个参数才是真正的 options
17
options = isSetupStore ? setupOptions : setup
18
} else {
19
options = idOrOptions
20
id = idOrOptions.id
21
22
// 如果传递的是一个对象, 但是对象上没有设置 id,
23
// 或者传入了一个函数, 但是函数上没有增加 id 属性则抛出警告
24
if (__DEV__ && typeof id !== 'string') {
25
throw new Error(
26
`[🍍]: "defineStore()" must be passed a store id as its first argument.`
27
)
28
}
29
}
30
31
// 调用 defineStore 以后会返回这个函数,
32
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
33
// hasInjectionContext() 的实现在 【Vue3 project/inject 源码实现】 一文中有过介绍, 感兴趣可自行查阅
34
const hasContext = hasInjectionContext()
35
pinia =
36
// 在测试模式下,忽略提供的参数,因为我们始终可以使用 getActivePinia() 检索 pinia 实例
37
(__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
38
(hasContext ? inject(piniaSymbol, null) : null)
39
40
// 设置成当前激活的 pinia 实例
41
if (pinia) setActivePinia(pinia)
42
43
// 如果没有通过 activePinia 获取到 pinia 实例则表示不是在 setup() 中使用的, 给出警告
44
if (__DEV__ && !activePinia) {
45
throw new Error(
46
`[🍍]: "getActivePinia()" was called but there was no active Pinia. Are you trying to use a store before calling "app.use(pinia)"?\n` +
47
`See https://pinia.vuejs.org/core-concepts/outside-component-usage.html for help.\n` +
48
`This will fail in production.`
49
)
50
}
51
52
pinia = activePinia!
53
54
// 如果没有定义过这个 id 的模块就定义它
55
if (!pinia._s.has(id)) {
56
// 在注册了对应的 store 的时候将其保存到 _s 中
57
// 两个创建的函数放在下文解析
58
if (isSetupStore) {
59
createSetupStore(id, setup, options, pinia)
60
} else {
61
createOptionsStore(id, options as any, pinia)
62
}
63
64
// istanbul 是一个检查 JS 代码覆盖率的工具, 这里是告诉它忽略检测 else
65
/* istanbul ignore else */
66
if (__DEV__) {
67
useStore._pinia = pinia
68
}
69
}
70
71
// 获取到对应的 store 对象
72
const store: StoreGeneric = pinia._s.get(id)!
73
74
// 在开发期间注册热更新模块的替换逻辑
75
if (__DEV__ && hot) {
76
const hotId = '__hot:' + id
77
// 注册一个包含已变更数据/状态的 store 去覆盖原有的 store, 这就是热更新的原理
78
const newStore = isSetupStore
79
? createSetupStore(hotId, setup, options, pinia, true)
80
: createOptionsStore(hotId, assign({}, options) as any, pinia, true)
81
82
// 应用热更新的数据
83
hot._hotUpdate(newStore)
84
85
// 从缓存中清除状态属性和存储
86
delete pinia.state.value[hotId]
87
pinia._s.delete(hotId)
88
}
89
90
if (__DEV__ && IS_CLIENT) {
91
const currentInstance = getCurrentInstance()
92
// save stores in instances to access them devtools
93
if (
94
currentInstance &&
95
currentInstance.proxy &&
96
// 避免添加专为热模块更换而构建的存储
97
!hot
98
) {
99
// 获取到当前组件的实例详情
100
const vm = currentInstance.proxy
101
// 在实例上临时增加 store 的引用, 具体作用不详
102
const cache = '_pStores' in vm ? vm._pStores! : (vm._pStores = {})
103
cache[id] = store
104
}
105
}
106
107
// StoreGeneric 无法匹配到 Store
108
return store as any
109
}
110
111
// 给闭包函数增加 id, 方便 vue-devtools 溯源
112
useStore.$id = id
113
114
return useStore
115
}

createOptionsStore

我们先看一下创建 Options Store 的方法:

1
const { assign } = Object
2
3
function createOptionsStore(
4
id,
5
options,
6
pinia,
7
hot
8
) {
9
const { state, actions, getters } = options
62 collapsed lines
10
11
// 从当前 pinia 实例中以 id 获取初始状态(可能为空)
12
const initialState = pinia.state.value[id]
13
14
let store
15
16
function setup() {
17
if (!initialState && (!__DEV__ || !hot)) {
18
/* istanbul ignore if */
19
if (isVue2) {
20
set(pinia.state.value, id, state ? state() : {})
21
} else {
22
pinia.state.value[id] = state ? state() : {}
23
}
24
}
25
26
// 避免在pinia.state.value中创建状态
27
const localState =
28
// 如果是开发环境并且启用了热重载
29
__DEV__ && hot
30
? // use ref() to unwrap refs inside state
31
// 利用 ref() 内部自动解包(unwrap, 如果是 ref 则跳过, 不是则转换)的机制来绑定响应式
32
// TODO: check if this is still necessary
33
// TODO: 也许这个判断不是必要的?
34
toRefs(ref(state ? state() : {}).value)
35
: toRefs(pinia.state.value[id]) // 如果没有热重载则直接进行转换
36
37
// 仅做初始状态的处理并转换为 setup 函数, 最终再调用 createSetupStore 函数创建最终的 store
38
return assign(
39
localState,
40
actions,
41
Object.keys(getters || {}).reduce((computedGetters, name) => {
42
// 如果 getter 函数和 state 的键值同名则抛出警告
43
if (__DEV__ && name in localState) {
44
console.warn(
45
`[🍍]: A getter cannot have the same name as another state property. Rename one of them. Found with "${name}" in store "${id}".`
46
)
47
}
48
49
computedGetters[name] = markRaw(
50
computed(() => {
51
setActivePinia(pinia)
52
// 获取之前创建的 store
53
const store = pinia._s.get(id)!
54
55
// 允许交叉使用商店
56
/* istanbul ignore if */
57
if (isVue2 && !store._r) return
58
59
// 改变内部 getter this 指向, 从而指向插件本身
60
return getters![name].call(store, store)
61
})
62
)
63
return computedGetters
64
}, {})
65
)
66
}
67
68
store = createSetupStore(id, setup, options, pinia, hot, true)
69
70
return store
71
}

createSetupStore

createSetupStore 就是这次的重头戏了, 所有状态创建的核心逻辑都在这里面

1
export enum MutationType {
2
direct = 'direct',
3
patchObject = 'patch object',
4
patchFunction = 'patch function',
5
}
6
7
// 工具函数, 用于判断是否是一个普通对象, 而非 function/array/map/set 这种值
8
export function isPlainObject(o) {
9
return (
642 collapsed lines
10
o &&
11
typeof o === 'object' &&
12
Object.prototype.toString.call(o) === '[object Object]' &&
13
typeof o.toJSON !== 'function'
14
)
15
}
16
17
// 增加一个订阅函数
18
export function addSubscription(
19
subscriptions,
20
callback,
21
detached,
22
onCleanup
23
) {
24
subscriptions.push(callback)
25
26
const removeSubscription = () => {
27
const idx = subscriptions.indexOf(callback)
28
if (idx > -1) {
29
subscriptions.splice(idx, 1)
30
onCleanup()
31
}
32
}
33
34
if (!detached && getCurrentScope()) {
35
onScopeDispose(removeSubscription)
36
}
37
38
return removeSubscription
39
}
40
41
// 触发所有订阅
42
export function triggerSubscriptions(
43
subscriptions,
44
...args
45
) {
46
// 拷贝一份 subscriptions, 防止在执行回调时 subscriptions 发生变化
47
subscriptions.slice().forEach((callback) => {
48
callback(...args)
49
})
50
}
51
52
// 工具函数, 合并响应式对象状态
53
function mergeReactiveObjects(target, patchToApply) {
54
// 更新 Map 实例对象
55
if (target instanceof Map && patchToApply instanceof Map) {
56
patchToApply.forEach((value, key) => target.set(key, value))
57
}
58
// 更新 Set 实例对象
59
if (target instanceof Set && patchToApply instanceof Set) {
60
patchToApply.forEach(target.add, target)
61
}
62
63
// 无需遍历 Symbol,因为它们无论如何都无法序列化
64
for (const key in patchToApply) {
65
// 如果是原型属性, 则跳过
66
if (!patchToApply.hasOwnProperty(key)) continue
67
// 新的值
68
const subPatch = patchToApply[key]
69
// 旧的值
70
const targetValue = target[key]
71
72
// 如果是对象类型的值且它本身不是响应式对象, 则递归调用 mergeReactiveObjects
73
if (
74
isPlainObject(targetValue) &&
75
isPlainObject(subPatch) &&
76
target.hasOwnProperty(key) &&
77
// isRef/isReactive 是 vue 内部用于判断是否是响应式对象的方法
78
!isRef(subPatch) &&
79
!isReactive(subPatch)
80
) {
81
// NOTE: 在这里,我想警告不一致的类型,但这是不可能的,因为在设置存储中,人们可能会将属性的值启动为某种类型,例如 一个 Map,然后出于某种原因,在 SSR 期间,将其更改为 "undefined"。 当尝试水合时,我们想用 "undefined" 覆盖 Map。
82
target[key] = mergeReactiveObjects(targetValue, subPatch)
83
} else {
84
// 否则直接赋值覆盖
85
target[key] = subPatch
86
}
87
}
88
89
return target
90
}
91
92
function createSetupStore(
93
$id,
94
setup,
95
options,
96
pinia,
97
hot,
98
isOptionsStore
99
) {
100
// effectScope
101
let scope
102
103
const optionsForPlugin = assign(
104
{ actions: {} },
105
// 对象型的整个 store,或者是函数型 store 的第三个参数
106
options
107
)
108
109
/* istanbul ignore if */
110
// 开发环境如果 effectScope 的 active 为 false 则表示 pinia 被提前销毁了
111
if (__DEV__ && !pinia._e.active) {
112
throw new Error('Pinia destroyed')
113
}
114
115
// watcher options for $subscribe
116
// $subscribe 功能的实现其实就是添加了一个 watch 函数来监听变量的变化, 这个对象就是 watch 的第三个参数
117
const $subscribeOptions = {
118
deep: true,
119
// flush: 'post',
120
}
121
122
if (__DEV__ && !isVue2) {
123
// vue3 watch 支持 onTrigger 参数, 这个参数可以让底层框架做一些其他的事, 而不是将用户传入的参数包装一层再调用
124
$subscribeOptions.onTrigger = (event) => {
125
if (isListening) {
126
debuggerEvents = event
127
// 当 store 正在创建中并且 pinia 正在更新中则不触发这个事件
128
} else if (isListening == false && !store._hotUpdating) {
129
// 收集所有事件然后一起发送
130
if (Array.isArray(debuggerEvents)) {
131
debuggerEvents.push(event)
132
} else {
133
console.error(
134
'🍍 debuggerEvents should be an array. This is most likely an internal Pinia bug.'
135
)
136
}
137
}
138
}
139
}
140
141
// internal state
142
let isListening
143
let isSyncListening
144
let subscriptions = []
145
let actionSubscriptions = []
146
let debuggerEvents
147
const initialState = pinia.state.value[$id]
148
149
// 如果是 setup store 则不需要初始化 state
150
if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
151
/* istanbul ignore if */
152
if (isVue2) {
153
set(pinia.state.value, $id, {})
154
} else {
155
pinia.state.value[$id] = {}
156
}
157
}
158
159
const hotState = ref({})
160
161
// 当前触发的 listener id
162
// 避免同一时间触发太多 listener
163
// https://github.com/vuejs/pinia/issues/1129
164
let activeListener
165
166
// $patch 函数的主要是用于覆盖现有store的状态, 在需要批量更新store的时候有奇效
167
function $patch(
168
// 用来覆盖 store 数据的状态或者是一个函数
169
partialStateOrMutator
170
) {
171
// 用于保存 patch 触发时的类型与值
172
let subscriptionMutation
173
isListening = isSyncListening = false
174
175
// 由于 $patch 是同步的,所以每次触发的时候重置 debuggerEvents
176
/* istanbul ignore else */
177
if (__DEV__) {
178
debuggerEvents = []
179
}
180
// 如果传入的
181
if (typeof partialStateOrMutator === 'function') {
182
// 如果是函数则执行函数, 并把 store 作为参数传入
183
partialStateOrMutator(pinia.state.value[$id])
184
// 设置更新值得 mutation 参数, 用于告诉通知 subscriptions 时的变更类型
185
subscriptionMutation = {
186
type: MutationType.patchFunction,
187
storeId: $id,
188
events: debuggerEvents,
189
}
190
} else {
191
// 如果是对象则合并对象
192
mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
193
subscriptionMutation = {
194
type: MutationType.patchObject,
195
payload: partialStateOrMutator,
196
storeId: $id,
197
events: debuggerEvents,
198
}
199
}
200
// 用于标识当前监听函数的id
201
const myListenerId = (activeListener = Symbol())
202
nextTick().then(() => {
203
// 如果当前监听函数不是当前的监听函数则不触发
204
if (activeListener === myListenerId) {
205
isListening = true
206
}
207
})
208
isSyncListening = true
209
// 因为我们暂停了观察者,所以我们需要手动调用订阅
210
triggerSubscriptions(
211
subscriptions,
212
subscriptionMutation,
213
pinia.state.value[$id]
214
)
215
}
216
217
// 如果是 options store 则可以调用 $reset 方法还原成初始值
218
const $reset = isOptionsStore
219
? function $reset() {
220
const { state } = options
221
const newState = state ? state() : {}
222
// 因为这个 $reset 最终会被放到一个单独的对象上,所以 this.$patch 其实就是上面的 $patch 函数,只是内部覆盖值的操作被简化成了直接覆盖整个对象
223
this.$patch(($state) => {
224
assign($state, newState)
225
})
226
}
227
: /* istanbul ignore next */
228
__DEV__
229
? () => {
230
throw new Error(
231
`🍍: Store "${$id}" is built using the setup syntax and does not implement $reset().`
232
)
233
}
234
: noop
235
236
// 用于销毁当前 store
237
function $dispose() {
238
scope.stop()
239
subscriptions = []
240
actionSubscriptions = []
241
pinia._s.delete($id)
242
}
243
244
/**
245
* 包装 action 以处理订阅, 在值发生变化以后触发订阅
246
*
247
* @param name - actions 里的 key
248
* @param action - actions 里的 value
249
* @returns 返回一个包装了触发订阅参数的 action
250
*/
251
function wrapAction(name, action) {
252
return function () {
253
// 在每次操作前更新一次 pinia,确保实例的正确
254
setActivePinia(pinia)
255
const args = Array.from(arguments)
256
257
// 函数执行后的订阅函数(正常的订阅函数)
258
const afterCallbackList = []
259
// 函数触发错误后的订阅函数
260
const onErrorCallbackList = []
261
function after(callback) {
262
afterCallbackList.push(callback)
263
}
264
function onError(callback) {
265
onErrorCallbackList.push(callback)
266
}
267
268
// 触发所有添加的订阅函数
269
triggerSubscriptions(actionSubscriptions, {
270
args,
271
name,
272
store,
273
after,
274
onError,
275
})
276
277
// 用户获取判断 action 是否是异步的(返回一个 promise)
278
let ret
279
try {
280
// 修改 this 指向为 store
281
ret = action.apply(this && this.$id === $id ? this : store, args)
282
} catch (error) {
283
// 如果 action 执行出错, 触发 onError 订阅函数
284
triggerSubscriptions(onErrorCallbackList, error)
285
throw error
286
}
287
288
// 如果 action 是异步的, 触发 after 订阅函数
289
if (ret instanceof Promise) {
290
return ret
291
.then((value) => {
292
triggerSubscriptions(afterCallbackList, value)
293
return value
294
})
295
.catch((error) => {
296
triggerSubscriptions(onErrorCallbackList, error)
297
return Promise.reject(error)
298
})
299
}
300
301
// 如果是同步的函数则在执行完成后触发 after 订阅函数
302
triggerSubscriptions(afterCallbackList, ret)
303
return ret
304
}
305
}
306
307
// 用于 devtools 的 HMR
308
const _hmrPayload = markRaw({
309
actions: {},
310
getters: {},
311
state: [],
312
hotState,
313
})
314
315
const partialStore = {
316
_p: pinia,
317
// _s: scope,
318
$id,
319
$onAction: addSubscription.bind(null, actionSubscriptions),
320
$patch,
321
$reset,
322
$subscribe(callback, options = {}) {
323
const removeSubscription = addSubscription(
324
subscriptions,
325
callback,
326
options.detached,
327
328
// 清理函数, 用于取消 scopeEffect 收集的副作用
329
() => stopWatcher()
330
)
331
const stopWatcher = scope.run(() =>
332
watch(
333
() => pinia.state.value[$id],
334
(state) => {
335
if (options.flush === 'sync' ? isSyncListening : isListening) {
336
callback(
337
{
338
storeId: $id,
339
type: MutationType.direct,
340
events: debuggerEvents as DebuggerEvent,
341
},
342
state
343
)
344
}
345
},
346
assign({}, $subscribeOptions, options)
347
)
348
)!
349
350
// 返回一个清理函数, 常用的 API 设计风格了
351
return removeSubscription
352
},
353
$dispose,
354
}
355
356
/* istanbul ignore if */
357
if (isVue2) {
358
// start as non ready
359
partialStore._r = false
360
}
361
362
const store = reactive(
363
__DEV__ || (__USE_DEVTOOLS__ && IS_CLIENT)
364
// 如果是开发环境且开启了 devtools, 则添加 hmr 对象同时将 store 作为响应式对象
365
? assign(
366
{
367
_hmrPayload,
368
_customProperties: markRaw(new Set()), // devtools custom properties
369
},
370
partialStore
371
)
372
// 否则直接将 store 作为响应式对象
373
: partialStore
374
)
375
376
// 现在存储部分存储,以便存储的设置可以在完成之前相互实例化,而不会创建无限循环。
377
pinia._s.set($id, store)
378
379
const runWithContext =
380
(pinia._a && pinia._a.runWithContext) || fallbackRunWithContext
381
382
// TODO: idea 创建 skipSerialize 将属性标记为不可序列化并跳过它们
383
const setupStore = runWithContext(() =>
384
pinia._e.run(() => (scope = effectScope()).run(setup)!)
385
)!
386
387
// 覆盖现有操作以支持 $onAction
388
for (const key in setupStore) {
389
const prop = setupStore[key]
390
391
// 必须得是 ref/reactive 同时不能是 computed
392
if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
393
// 将其标记为要序列化的状态
394
if (__DEV__ && hot) {
395
set(hotState.value, key, toRef(setupStore as any, key))
396
} else if (!isOptionsStore) { // createOptionStore 直接在 pinia.state.value 中设置状态,因此可以跳过, 只需要处理普通的对象 store
397
// 在设置存储中,我们必须对状态进行水合,并将pinia状态树与用户刚刚创建的 refs 同步
398
if (initialState && shouldHydrate(prop)) {
399
if (isRef(prop)) {
400
prop.value = initialState[key]
401
} else {
402
// 可能是一个反应对象, 递归合并
403
// @ts-expect-error: prop is unknown
404
mergeReactiveObjects(prop, initialState[key])
405
}
406
}
407
// 将 ref 转移到 pinia 内部状态以保持一切同步处理
408
/* istanbul ignore if */
409
if (isVue2) {
410
set(pinia.state.value[$id], key, prop)
411
} else {
412
pinia.state.value[$id][key] = prop
413
}
414
}
415
416
/* istanbul ignore else */
417
if (__DEV__) {
418
// 把这个key放进热更新状态列表里
419
_hmrPayload.state.push(key)
420
}
421
// action
422
} else if (typeof prop === 'function') {
423
// 这是一个热模块替换 store,因为 hotUpdate 方法需要在正确的上下文中执行此操作
424
// 所以如果是开发环境并且启用了热更新则不要包装它
425
const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop)
426
427
if (isVue2) {
428
set(setupStore, key, actionValue)
429
} else {
430
setupStore[key] = actionValue
431
}
432
433
if (__DEV__) {
434
// 更新热更新的值
435
_hmrPayload.actions[key] = prop
436
}
437
438
// 列出 actions,以便可以在插件中使用它们
439
optionsForPlugin.actions[key] = prop
440
} else if (__DEV__) {
441
// 添加对 devtools 的支持
442
if (isComputed(prop)) {
443
_hmrPayload.getters[key] = isOptionsStore
444
? options.getters[key]
445
: prop
446
if (IS_CLIENT) {
447
const getters = (setupStore._getters) || ((setupStore._getters = markRaw([])))
448
getters.push(key)
449
}
450
}
451
}
452
}
453
454
// 添加 state、getters 和 actions 属性
455
/* istanbul ignore if */
456
if (isVue2) {
457
Object.keys(setupStore).forEach((key) => {
458
set(store, key, setupStore[key])
459
})
460
} else {
461
assign(store, setupStore)
462
// 允许使用“storeToRefs()”检索反应对象。 必须在分配给反应对象后调用。 让“storeToRefs()”与“reactive()”一起使用 #799
463
assign(toRaw(store), setupStore)
464
}
465
466
// 使用它而不是使用 setter 计算,以便能够在任何地方创建它,而无需将计算的生命周期链接到首次创建存储的位置。
467
Object.defineProperty(store, '$state', {
468
get: () => (__DEV__ && hot ? hotState.value : pinia.state.value[$id]),
469
set: (state) => {
470
if (__DEV__ && hot) {
471
throw new Error('cannot set hotState')
472
}
473
$patch(($state) => {
474
assign($state, state)
475
})
476
},
477
})
478
479
// 在插件之前添加 hotUpdate 以允许它们覆盖它
480
if (__DEV__) {
481
// 执行热更新时触发的操作
482
store._hotUpdate = markRaw((newStore) => {
483
store._hotUpdating = true
484
newStore._hmrPayload.state.forEach((stateKey) => {
485
if (stateKey in store.$state) {
486
const newStateTarget = newStore.$state[stateKey]
487
const oldStateSource = store.$state[stateKey]
488
if (
489
typeof newStateTarget === 'object' &&
490
isPlainObject(newStateTarget) &&
491
isPlainObject(oldStateSource)
492
) {
493
patchObject(newStateTarget, oldStateSource)
494
} else {
495
// 转移 ref
496
newStore.$state[stateKey] = oldStateSource
497
}
498
}
499
// 修补直接访问属性以允许 store.stateProperty 用作 store.$state.stateProperty
500
set(store, stateKey, toRef(newStore.$state, stateKey))
501
})
502
503
// 删除已删除的状态属性
504
Object.keys(store.$state).forEach((stateKey) => {
505
if (!(stateKey in newStore.$state)) {
506
del(store, stateKey)
507
}
508
})
509
510
// 避免开发工具将其记录为 mutation
511
isListening = false
512
isSyncListening = false
513
pinia.state.value[$id] = toRef(newStore._hmrPayload, 'hotState')
514
isSyncListening = true
515
nextTick().then(() => {
516
isListening = true
517
})
518
519
// 包装 actions
520
for (const actionName in newStore._hmrPayload.actions) {
521
const action = newStore[actionName]
522
523
set(store, actionName, wrapAction(actionName, action))
524
}
525
526
// TODO: 不确定这在 setup store 和 option store 中是否都有效
527
for (const getterName in newStore._hmrPayload.getters) {
528
const getter = newStore._hmrPayload.getters[getterName]
529
const getterValue = isOptionsStore
530
? // option store 中的 getters 的特殊处理
531
computed(() => {
532
setActivePinia(pinia)
533
return getter.call(store, store)
534
})
535
: getter
536
537
set(store, getterName, getterValue)
538
}
539
540
// 删除已删除的 getters 属性
541
Object.keys(store._hmrPayload.getters).forEach((key) => {
542
if (!(key in newStore._hmrPayload.getters)) {
543
del(store, key)
544
}
545
})
546
547
// 删除已删除的 actions 属性
548
Object.keys(store._hmrPayload.actions).forEach((key) => {
549
if (!(key in newStore._hmrPayload.actions)) {
550
del(store, key)
551
}
552
})
553
554
// 更新 devtools 中使用的值并允许稍后删除新属性
555
store._hmrPayload = newStore._hmrPayload
556
store._getters = newStore._getters
557
store._hotUpdating = false
558
})
559
}
560
561
if (__USE_DEVTOOLS__ && IS_CLIENT) {
562
const nonEnumerable = {
563
writable: true,
564
configurable: true,
565
// 避免在开发工具尝试显示此属性时发出警告
566
enumerable: false,
567
}
568
569
// 避免在开发工具中列出内部属性
570
;(['_p', '_hmrPayload', '_getters', '_customProperties'] as const).forEach(
571
(p) => {
572
Object.defineProperty(
573
store,
574
p,
575
assign({ value: store[p] }, nonEnumerable)
576
)
577
}
578
)
579
}
580
581
if (isVue2) {
582
// 在插件之前将 store 标记为就绪
583
store._r = true
584
}
585
586
// 应用所有的插件
587
pinia._p.forEach((extender) => {
588
if (__USE_DEVTOOLS__ && IS_CLIENT) {
589
// 插件内部可能会为 store 添加新的属性, 这些属性可能是响应式的, 所以要将他们收集起来
590
const extensions = scope.run(() =>
591
extender({
592
store: store,
593
app: pinia._a,
594
pinia,
595
options: optionsForPlugin,
596
})
597
)!
598
Object.keys(extensions || {}).forEach((key) =>
599
// 把新增的属性添加到 store._customProperties 中, 以便在 devtools 中显示
600
store._customProperties.add(key)
601
)
602
assign(store, extensions)
603
} else {
604
// 执行同样的操作, 将插件返回的值和 store 合并
605
assign(
606
store,
607
scope.run(() =>
608
extender({
609
store: store,
610
app: pinia._a,
611
pinia,
612
options: optionsForPlugin,
613
})
614
)!
615
)
616
}
617
})
618
619
// 对传入的 state 进行格式判断, 不允许是一个自定义类或内置类, 只能是传统对象, 同样只在开发器间做判断
620
if (
621
__DEV__ &&
622
store.$state &&
623
typeof store.$state === 'object' &&
624
typeof store.$state.constructor === 'function' &&
625
!store.$state.constructor.toString().includes('[native code]')
626
) {
627
console.warn(
628
`[🍍]: The "state" must be a plain object. It cannot be\n` +
629
`\tstate: () => new MyClass()\n` +
630
`Found in store "${store.$id}".`
631
)
632
}
633
634
// 只把初始状态下的 pinia store 用作 SSR 时的初始状态
635
if (
636
initialState &&
637
isOptionsStore &&
638
// 可以自定义 hydrate 方法, 用于在 SSR 时, 将初始状态同步到 store 中
639
options.hydrate
640
) {
641
options.hydrate(
642
store.$state,
643
initialState
644
)
645
}
646
647
// 表示 store 已经被创建, 内部状态正在监听
648
isListening = true
649
isSyncListening = true
650
return store
651
}

到这里函数就结束了, 通篇看下来它在开发期间做了很多类型处理与判断, 这也使得在使用pinia进行开发的时候就能发现一些问题, 避免在无意间埋下一些意料之外的隐患. 同时针对开发器间的 HMR 也是做了相当多的处理, 使得在开发期间的体验更加的友好, 所以如果想要自己开发一个 vue 库的话, 也可以参考一下 pinia 的实现, 从中学习到一些开发技巧, 比如如何和 vue-devtools 更好集成, 如何使用 HMR 替换等等.

当然, 除了实现的代码优雅之外, Pinia 内部做的 ts 类型提示也是很完善的, 有空也可以学习一下(挖坑).

学到了什么

  1. defineStore 内部做了些什么, 如果创建了一个 pinia store 实例
  2. pinia 的插件在 pinia 实例注册前使用和注册后使用的区别
  3. 传入函数来创建一个 store 会比使用对象形式来创建 store 更快, 因为对象内部会将对象转换为 setup store, 然后再创建
  4. $subscribe() 方法其实是通过 Vue 内部的 watch 来监听状态的变化来实现的
  5. 监听状态变化的回调会有哪些参数
  6. 如何编写一个兼容Vue2/Vue3的库(使用 vue-demi 对当前应用的环境进行判断)
  7. pinia 的插件能拿到哪些store的上下文及参数(store, app, pinia, options)

以上.


CD ..