ToC
Init
在 Pinia 官方文档中的 Getting Started 与 What is Pinia? 一节中,我们可以看到 Pinia 的基本使用方式的代码,比如全局注册:
1import { createApp } from 'vue'2import { createPinia } from 'pinia'3import App from './App.vue'4
5const pinia = createPinia() // 创建一个应用级 pinia 的实例6const app = createApp(App)7
8app.use(pinia) // 将 pinia 实例挂载到 app 上9app.mount('#app')
定义 store
:
1import { defineStore } from 'pinia'2
3export const useCounterStore = defineStore('counter', {4 state: () => {5 return { count: 0 }6 },7 // state: () => ({ count: 0 })8 actions: {9 increment() {10 this.count++3 collapsed lines
11 },12 },13})
使用 store
:
1<script setup>2import { useCounterStore } from '@/stores/counter'3const counter = useCounterStore()4counter.count++5counter.$patch({ count: counter.count + 1 })6counter.increment()7</script>8<template>9 <div>Current Count: {{ counter.count }}</div>10</template>
从基本的使用方法来看,主要的 API 其实就只有两个:createPinia
defineStore
,那么我们就从这两个 API 入手,来看看 Pinia 是如何被实现的。
createPinia
为了降低理解成本,后续的代码都会去除 ts
类型的定义,只保留核心及上下文逻辑,同时因为会增加很多的注释来解释代码,所以在代码中也会增加很多的空行,避免代码过于密集,影响阅读
1/**2 * 必须调用setActivePinia来处理诸如 "fetch"、"setup"、"serverPrefetch" 等函数顶部的SSR3 */4export let activePinia5
6/**7 * 设置或清空 active pinia, 在SSR和内部调用 action 和 getter 时使用8 *9 * @param pinia - Pinia 实例10 */102 collapsed lines
11export const setActivePinia = (pinia) => (activePinia = pinia)12
13// createPinia 实际上是一个工厂函数,每次调用的时候都会返回一个新的 pinia 实例对象14export function createPinia() {15 // effectScope 用于创建一个新的 effect scope, 用于隔离副作用16 // 其本身是一个比较高级的API,可以参考这个 RFC: https://github.com/vuejs/rfcs/blob/master/active-rfcs/0041-reactivity-effect-scope.md17 // 在正常的项目开发中基本用不到,但是在开发一些库或者依赖的时候可以更方便地收集依赖在运行时增加的副作用函数或响应式对象变量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 属性设置为 true40 // 这样即便它被 reactive/ref 包装以后,它也仍然是一个普通对象 👇:41 /**42 * const pinia = reactive(createPinia())43 * console.log(isReactive(pinia)) // false44 */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 = app59
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 = pinia69
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 this96 },97
98 _p, // plugins99 _a: null, // app100 _e: scope, // effectScope101 _s: new Map(), // store map, 方便通过 id 来获取到对应的 store102 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 pinia112}
既然提到了创建,那就顺便看看销毁pinia的 disposePinia
方法吧,虽然这个方法非常少用,大多数只在做测试的时候才会用到,但是了解一下也是好的,万一什么时候就用到了呢?
1/**2 * 通过停止其 effectScope 并删除状态、插件和存储来处理 Pinia 实例。这在测试 pinia 或常规 pinia 的测试中以及在使用多个 pinia 实例的应用中都非常有用。3 *4 * @param pinia - pinia 实例5 */6export function disposePinia(pinia: Pinia) {7 pinia._e.stop() // 停止 effectScope, 销毁收集到的所有副作用8 pinia._s.clear() // 清空 state map9 pinia._p.splice(0) // 清空 plugins10 pinia.state.value = {} // 清空 state2 collapsed lines
11 pinia._a = null // 置空 app12}
defineStore
相比之下,defineStore
的代码会多很多,因为 pinia 要处理对象 state|actions|getters,而且 defineStore
函数不仅支持传入一个对象,还支持传入一个函数,在实现上也会有一些差异,所以我们分开来看,但是为了避免解析和最新版本的代码出现对应不上的问题,所以在查看代码的对照源代码的时候最好是使用指定 commit 的代码,比如这次用的 commit 就是 fix: support webpack minification 。
core
1export function defineStore(2 idOrOptions,3 setup,4 setupOptions5): StoreDefinition {6 let id7 let options8
9 // 判断第二个参数是否是函数类型的 store10 const isSetupStore = typeof setup === 'function'105 collapsed lines
11
12 // 判断第一个参数是不是字符串, 如果是则表示是 id13 if (typeof idOrOptions === 'string') {14 id = idOrOptions15 // 如果第一个参数是字符串, 那么再判断第二个参数是不是函数16 // 如果是函数, 则第三个参数才是真正的 options17 options = isSetupStore ? setupOptions : setup18 } else {19 options = idOrOptions20 id = idOrOptions.id21
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 代码覆盖率的工具, 这里是告诉它忽略检测 else65 /* istanbul ignore else */66 if (__DEV__) {67 useStore._pinia = pinia68 }69 }70
71 // 获取到对应的 store 对象72 const store: StoreGeneric = pinia._s.get(id)!73
74 // 在开发期间注册热更新模块的替换逻辑75 if (__DEV__ && hot) {76 const hotId = '__hot:' + id77 // 注册一个包含已变更数据/状态的 store 去覆盖原有的 store, 这就是热更新的原理78 const newStore = isSetupStore79 ? 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 devtools93 if (94 currentInstance &&95 currentInstance.proxy &&96 // 避免添加专为热模块更换而构建的存储97 !hot98 ) {99 // 获取到当前组件的实例详情100 const vm = currentInstance.proxy101 // 在实例上临时增加 store 的引用, 具体作用不详102 const cache = '_pStores' in vm ? vm._pStores! : (vm._pStores = {})103 cache[id] = store104 }105 }106
107 // StoreGeneric 无法匹配到 Store108 return store as any109 }110
111 // 给闭包函数增加 id, 方便 vue-devtools 溯源112 useStore.$id = id113
114 return useStore115}
createOptionsStore
我们先看一下创建 Options Store
的方法:
1const { assign } = Object2
3function createOptionsStore(4 id,5 options,6 pinia,7 hot8) {9 const { state, actions, getters } = options10
61 collapsed lines
11 // 从当前 pinia 实例中以 id 获取初始状态(可能为空)12 const initialState = pinia.state.value[id]13
14 let store15
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__ && hot30 ? // use ref() to unwrap refs inside state31 // 利用 ref() 内部自动解包(unwrap, 如果是 ref 则跳过, 不是则转换)的机制来绑定响应式32 // TODO: check if this is still necessary33 // TODO: 也许这个判断不是必要的?34 toRefs(ref(state ? state() : {}).value)35 : toRefs(pinia.state.value[id]) // 如果没有热重载则直接进行转换36
37 // 仅做初始状态的处理并转换为 setup 函数, 最终再调用 createSetupStore 函数创建最终的 store38 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 // 获取之前创建的 store53 const store = pinia._s.get(id)!54
55 // 允许交叉使用商店56 /* istanbul ignore if */57 if (isVue2 && !store._r) return58
59 // 改变内部 getter this 指向, 从而指向插件本身60 return getters![name].call(store, store)61 })62 )63 return computedGetters64 }, {})65 )66 }67
68 store = createSetupStore(id, setup, options, pinia, hot, true)69
70 return store71}
createSetupStore
createSetupStore
就是这次的重头戏了, 所有状态创建的核心逻辑都在这里面
1export enum MutationType {2 direct = 'direct',3 patchObject = 'patch object',4 patchFunction = 'patch function',5}6
7// 工具函数, 用于判断是否是一个普通对象, 而非 function/array/map/set 这种值8export function isPlainObject(o) {9 return (10 o &&641 collapsed lines
11 typeof o === 'object' &&12 Object.prototype.toString.call(o) === '[object Object]' &&13 typeof o.toJSON !== 'function'14 )15}16
17// 增加一个订阅函数18export function addSubscription(19 subscriptions,20 callback,21 detached,22 onCleanup23) {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 removeSubscription39}40
41// 触发所有订阅42export function triggerSubscriptions(43 subscriptions,44 ...args45) {46 // 拷贝一份 subscriptions, 防止在执行回调时 subscriptions 发生变化47 subscriptions.slice().forEach((callback) => {48 callback(...args)49 })50}51
52// 工具函数, 合并响应式对象状态53function 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)) continue67 // 新的值68 const subPatch = patchToApply[key]69 // 旧的值70 const targetValue = target[key]71
72 // 如果是对象类型的值且它本身不是响应式对象, 则递归调用 mergeReactiveObjects73 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] = subPatch86 }87 }88
89 return target90}91
92function createSetupStore(93 $id,94 setup,95 options,96 pinia,97 hot,98 isOptionsStore99) {100 // effectScope101 let scope102
103 const optionsForPlugin = assign(104 { actions: {} },105 // 对象型的整个 store,或者是函数型 store 的第三个参数106 options107 )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 $subscribe116 // $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 = event127 // 当 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 state142 let isListening143 let isSyncListening144 let subscriptions = []145 let actionSubscriptions = []146 let debuggerEvents147 const initialState = pinia.state.value[$id]148
149 // 如果是 setup store 则不需要初始化 state150 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 id162 // 避免同一时间触发太多 listener163 // https://github.com/vuejs/pinia/issues/1129164 let activeListener165
166 // $patch 函数的主要是用于覆盖现有store的状态, 在需要批量更新store的时候有奇效167 function $patch(168 // 用来覆盖 store 数据的状态或者是一个函数169 partialStateOrMutator170 ) {171 // 用于保存 patch 触发时的类型与值172 let subscriptionMutation173 isListening = isSyncListening = false174
175 // 由于 $patch 是同步的,所以每次触发的时候重置 debuggerEvents176 /* 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 // 用于标识当前监听函数的id201 const myListenerId = (activeListener = Symbol())202 nextTick().then(() => {203 // 如果当前监听函数不是当前的监听函数则不触发204 if (activeListener === myListenerId) {205 isListening = true206 }207 })208 isSyncListening = true209 // 因为我们暂停了观察者,所以我们需要手动调用订阅210 triggerSubscriptions(211 subscriptions,212 subscriptionMutation,213 pinia.state.value[$id]214 )215 }216
217 // 如果是 options store 则可以调用 $reset 方法还原成初始值218 const $reset = isOptionsStore219 ? function $reset() {220 const { state } = options221 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 : noop235
236 // 用于销毁当前 store237 function $dispose() {238 scope.stop()239 subscriptions = []240 actionSubscriptions = []241 pinia._s.delete($id)242 }243
244 /**245 * 包装 action 以处理订阅, 在值发生变化以后触发订阅246 *247 * @param name - actions 里的 key248 * @param action - actions 里的 value249 * @returns 返回一个包装了触发订阅参数的 action250 */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 ret279 try {280 // 修改 this 指向为 store281 ret = action.apply(this && this.$id === $id ? this : store, args)282 } catch (error) {283 // 如果 action 执行出错, 触发 onError 订阅函数284 triggerSubscriptions(onErrorCallbackList, error)285 throw error286 }287
288 // 如果 action 是异步的, 触发 after 订阅函数289 if (ret instanceof Promise) {290 return ret291 .then((value) => {292 triggerSubscriptions(afterCallbackList, value)293 return value294 })295 .catch((error) => {296 triggerSubscriptions(onErrorCallbackList, error)297 return Promise.reject(error)298 })299 }300
301 // 如果是同步的函数则在执行完成后触发 after 订阅函数302 triggerSubscriptions(afterCallbackList, ret)303 return ret304 }305 }306
307 // 用于 devtools 的 HMR308 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 state343 )344 }345 },346 assign({}, $subscribeOptions, options)347 )348 )!349
350 // 返回一个清理函数, 常用的 API 设计风格了351 return removeSubscription352 },353 $dispose,354 }355
356 /* istanbul ignore if */357 if (isVue2) {358 // start as non ready359 partialStore._r = false360 }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 properties369 },370 partialStore371 )372 // 否则直接将 store 作为响应式对象373 : partialStore374 )375
376 // 现在存储部分存储,以便存储的设置可以在完成之前相互实例化,而不会创建无限循环。377 pinia._s.set($id, store)378
379 const runWithContext =380 (pinia._a && pinia._a.runWithContext) || fallbackRunWithContext381
382 // TODO: idea 创建 skipSerialize 将属性标记为不可序列化并跳过它们383 const setupStore = runWithContext(() =>384 pinia._e.run(() => (scope = effectScope()).run(setup)!)385 )!386
387 // 覆盖现有操作以支持 $onAction388 for (const key in setupStore) {389 const prop = setupStore[key]390
391 // 必须得是 ref/reactive 同时不能是 computed392 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 中设置状态,因此可以跳过, 只需要处理普通的对象 store397 // 在设置存储中,我们必须对状态进行水合,并将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 unknown404 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] = prop413 }414 }415
416 /* istanbul ignore else */417 if (__DEV__) {418 // 把这个key放进热更新状态列表里419 _hmrPayload.state.push(key)420 }421 // action422 } 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] = actionValue431 }432
433 if (__DEV__) {434 // 更新热更新的值435 _hmrPayload.actions[key] = prop436 }437
438 // 列出 actions,以便可以在插件中使用它们439 optionsForPlugin.actions[key] = prop440 } else if (__DEV__) {441 // 添加对 devtools 的支持442 if (isComputed(prop)) {443 _hmrPayload.getters[key] = isOptionsStore444 ? options.getters[key]445 : prop446 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()”一起使用 #799463 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 = true484 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 // 转移 ref496 newStore.$state[stateKey] = oldStateSource497 }498 }499 // 修补直接访问属性以允许 store.stateProperty 用作 store.$state.stateProperty500 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 // 避免开发工具将其记录为 mutation511 isListening = false512 isSyncListening = false513 pinia.state.value[$id] = toRef(newStore._hmrPayload, 'hotState')514 isSyncListening = true515 nextTick().then(() => {516 isListening = true517 })518
519 // 包装 actions520 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 = isOptionsStore530 ? // option store 中的 getters 的特殊处理531 computed(() => {532 setActivePinia(pinia)533 return getter.call(store, store)534 })535 : getter536
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._hmrPayload556 store._getters = newStore._getters557 store._hotUpdating = false558 })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 = true584 }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.hydrate640 ) {641 options.hydrate(642 store.$state,643 initialState644 )645 }646
647 // 表示 store 已经被创建, 内部状态正在监听648 isListening = true649 isSyncListening = true650 return store651}
到这里函数就结束了, 通篇看下来它在开发期间做了很多类型处理与判断, 这也使得在使用pinia进行开发的时候就能发现一些问题, 避免在无意间埋下一些意料之外的隐患. 同时针对开发器间的 HMR 也是做了相当多的处理, 使得在开发期间的体验更加的友好, 所以如果想要自己开发一个 vue 库的话, 也可以参考一下 pinia 的实现, 从中学习到一些开发技巧, 比如如何和 vue-devtools 更好集成, 如何使用 HMR 替换等等.
当然, 除了实现的代码优雅之外, Pinia 内部做的 ts 类型提示也是很完善的, 有空也可以学习一下(挖坑).
学到了什么
defineStore
内部做了些什么, 如果创建了一个pinia store
实例- pinia 的插件在 pinia 实例注册前使用和注册后使用的区别
- 传入函数来创建一个 store 会比使用对象形式来创建 store 更快, 因为对象内部会将对象转换为 setup store, 然后再创建
- $subscribe() 方法其实是通过 Vue 内部的 watch 来监听状态的变化来实现的
- 监听状态变化的回调会有哪些参数
- 如何编写一个兼容Vue2/Vue3的库(使用 vue-demi 对当前应用的环境进行判断)
- pinia 的插件能拿到哪些store的上下文及参数(store, app, pinia, options)
以上.