【pinia原始碼】二、defineStore原始碼解析

語言: CN / TW / HK

前言

【pinia原始碼】系列文章主要分析pinia的實現原理。該系列文章原始碼參考pinia v2.0.14

原始碼地址:https://github.com/vuejs/pinia

官方文件:https://pinia.vuejs.org

本篇文章將分析defineStore的實現。

使用

通過defineStore定義一個store

```ts const useUserStore = defineStore('counter', { state: () => ({ count: 0 }), actions: { increment() { this.count++ } } })

// or const useUserStore = defineStore({ id: 'counter', state: () => ({ count: 0 }), actions: { increment() { this.count++ } } })

// or const useUserStore = defineStore('counter', () => { const count = ref(0)

function increment() { count.value++ } return { count, increment } }) ```

defineStore

```ts export function defineStore( idOrOptions: any, setup?: any, setupOptions?: any ): StoreDefinition { let id: string let options: | DefineStoreOptions< string, StateTree, _GettersTree, _ActionsTree > | DefineSetupStoreOptions< string, StateTree, _GettersTree, _ActionsTree >

const isSetupStore = typeof setup === 'function' if (typeof idOrOptions === 'string') { id = idOrOptions options = isSetupStore ? setupOptions : setup } else { options = idOrOptions id = idOrOptions.id }

function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric { // ... }

useStore.$id = id

return useStore } ```

defineStore函式可以接收三個引數:idOrOptionssetupsetOptions,後兩個引數為可選引數。下面是三個defineStore的函式型別定義。

```ts export function defineStore< Id extends string, S extends StateTree = {}, G extends _GettersTree = {}, A / extends ActionsTree / = {}

( id: Id, options: Omit, 'id'> ): StoreDefinition

export function defineStore< Id extends string, S extends StateTree = {}, G extends _GettersTree = {}, A / extends ActionsTree / = {}

(options: DefineStoreOptions): StoreDefinition

export function defineStore( id: Id, storeSetup: () => SS, options?: DefineSetupStoreOptions< Id, _ExtractStateFromSetupStore, _ExtractGettersFromSetupStore, _ExtractActionsFromSetupStore > ): StoreDefinition< Id, _ExtractStateFromSetupStore, _ExtractGettersFromSetupStore, _ExtractActionsFromSetupStore

```

首先在defineStore中聲明瞭三個變數:idoptionsisSetupStore,其中id為定義的store的唯一idoptions為定義store時的optionsisSetupStore代表傳入的setup是不是個函式。

然後根據傳入的idOrOptions的型別,為idotions賦值。緊接著聲明瞭一個useStore函式,並將id賦給它,然後將其return。截止到此,我們知道defineStore會返回一個函式,那麼這個函式具體是做什麼的呢?我們繼續看useStore的實現。

useStore

```ts function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric { // 獲取當前例項 const currentInstance = getCurrentInstance() // 測試環境下,忽略提供的引數,因為總是能使用getActivePinia()獲取pinia例項 // 非測試環境下,如果未傳入pinia,則會從元件中使用inject獲取pinia pinia = (TEST && activePinia && activePinia._testing ? null : pinia) || (currentInstance && inject(piniaSymbol)) // 設定啟用的pinia if (pinia) setActivePinia(pinia)

// 如果沒有activePinia,那麼可能沒有install pinia,開發環境下進行提示 if (DEV && !activePinia) { throw new Error( [🍍]: getActivePinia was called with no active Pinia. Did you forget to install pinia?\n + \tconst pinia = createPinia()\n + \tapp.use(pinia)\n + This will fail in production. ) }

// 設定pinia為啟用的pinia pinia = activePinia!

// 從pina._s中查詢id否註冊過,如果沒有被註冊,建立一個store並註冊在pinia._s中 if (!pinia._s.has(id)) { if (isSetupStore) { createSetupStore(id, setup, options, pinia) } else { createOptionsStore(id, options as any, pinia) }

if (__DEV__) {
  useStore._pinia = pinia
}

}

// 從pinia._s中獲取id對應的store const store: StoreGeneric = pinia._s.get(id)!

if (DEV && hot) { const hotId = '__hot:' + id const newStore = isSetupStore ? createSetupStore(hotId, setup, options, pinia, true) : createOptionsStore(hotId, assign({}, options) as any, pinia, true)

hot._hotUpdate(newStore)

// cleanup the state properties and the store from the cache
delete pinia.state.value[hotId]
pinia._s.delete(hotId)

}

if ( DEV && IS_CLIENT && currentInstance && currentInstance.proxy && !hot ) { const vm = currentInstance.proxy const cache = '_pStores' in vm ? vm._pStores! : (vm._pStores = {}) cache[id] = store }

// 返回store return store as any } ```

useStore接收兩個可選引數:piniahotpinia是個Pinia的例項,而hot只在開發環境下有用,它與模組的熱更新有關。

useStore中會首先獲取當前元件例項,如果存在元件例項,使用inject(piniaSymbol)獲取pinia(在install中會進行provide),並將其設定為activePinia,然後在activePinia._s中查詢是否有被註冊為idstore,如果沒有則建立store,將其註冊到activePinia._s中。最後返回activePinia._sid對應的store

現在我們知道useStore函式,最終會返回一個store。那麼這個store是什麼呢?它是如何建立的呢?在useStore中根據不同情況中有兩中方式來建立store,分別是:createSetupStorecreateOptionsStore。這兩個方式的使用條件是:如果defineStore第二個引數是個function呼叫createSetupStore,相反呼叫createOptionsStore

createSetupStore

createSetupStore函式程式碼過長,這裡就不貼完整程式碼了。createSetupStore可接收引數如下:

| 引數 | 說明 | | | ------------------ | -------------------------------- | ------ | | $id | 定義storeid | | | setup | 一個可以返回state的函式 | | | options | defineStoreoptions | | | pinia | Pinia例項 | | | hot | 是否啟用熱更新 | 可選 | | isOptionsStore | 是否使用options宣告的store | 可選 |

createSetupStore程式碼有500多行,如果從頭開始看的話,不容易理解。我們可以根據createSetupStore的用途,從其核心開始看。因為createSetupStore是需要建立store,並將store註冊到pinia._s中,所以createSetupStore中可能需要建立store,我們找到建立store的地方。

```ts const partialStore = { _p: pinia, // _s: scope, $id, $onAction: addSubscription.bind(null, actionSubscriptions), $patch, $reset, $subscribe(callback, options = {}) { const removeSubscription = addSubscription( subscriptions, callback, options.detached, () => stopWatcher() ) const stopWatcher = scope.run(() => watch( () => pinia.state.value[$id] as UnwrapRef, (state) => { if (options.flush === 'sync' ? isSyncListening : isListening) { callback( { storeId: $id, type: MutationType.direct, events: debuggerEvents as DebuggerEvent, }, state ) } }, assign({}, $subscribeOptions, options) ) )!

return removeSubscription

}, $dispose, } as _StoreWithState

if (isVue2) { partialStore._r = false }

const store: Store = reactive( assign( DEV && IS_CLIENT ? // devtools custom properties { _customProperties: markRaw(new Set()), _hmrPayload, } : {}, partialStore ) ) as unknown as Store

pinia._s.set($id, store) ```

store是用reactive包裝的一個響應式物件,reactive所包裝的物件是由partialStore通過Object.assign進行復制的。partialStore中定義了很多方法,這些方法都是暴露給使用者操作store的一些介面,如$onAction可設定actions的回撥、$patch可更新store中的state$dispose可銷燬store

在呼叫完pinia._s.set($id, store)之後,會執行setup,獲取所有的資料。setup的執行會在建立pinia例項時建立的effectScope中執行,而且會再單獨建立一個effectScope,用來單獨執行setup.

ts const setupStore = pinia._e.run(() => { scope = effectScope() return scope.run(() => setup()) })!

然後遍歷setupStore的屬性:如果propkey對應的值)為ref(不為computed)或reactive,則將keyprop同步到pina.state.value[$id]中;如果propfunction,則會使用wrapAction包裝prop,並將包裝後的方法賦值給setupStore[key],以覆蓋之前的值,同時將包裝後的方法存入optionsForPlugin.actions中。

```ts for (const key in setupStore) { const prop = setupStore[key]

// 如果prop是ref(但不是computed)或reactive if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) { if (DEV && hot) { set(hotState.value, key, toRef(setupStore as any, key)) } else if (!isOptionsStore) { if (initialState && shouldHydrate(prop)) { if (isRef(prop)) { prop.value = initialState[key] } else { mergeReactiveObjects(prop, initialState[key]) } }

  // 將對應屬性同步至pinia.state中
  if (isVue2) {
    set(pinia.state.value[$id], key, prop)
  } else {
    pinia.state.value[$id][key] = prop
  }
}

if (__DEV__) {
  _hmrPayload.state.push(key)
}

} else if (typeof prop === 'function') { // 如果prop是function // 使用wrapAction包裝prop,在wrapAction會處理afeterCallback、errorCallback const actionValue = DEV && hot ? prop : wrapAction(key, prop)

// 將actionsValue新增到setupStore中,覆蓋原來的function
if (isVue2) {
  set(setupStore, key, actionValue)
} else {
  setupStore[key] = actionValue
}

if (__DEV__) {
  _hmrPayload.actions[key] = prop
}

// 將function型別的prop存入optionsForPlugin.actions中
optionsForPlugin.actions[key] = prop

} else if (DEV) { if (isComputed(prop)) { _hmrPayload.getters[key] = isOptionsStore ? // @ts-expect-error options.getters[key] : prop if (IS_CLIENT) { const getters: string[] = setupStore._getters || (setupStore._getters = markRaw([])) getters.push(key) } } } } ```

接下來我們看下wrapAction是如何進行包裝function型別上的prop

```ts function wrapAction(name: string, action: _Method) { return function (this: any) { setActivePinia(pinia) const args = Array.from(arguments)

const afterCallbackList: Array<(resolvedReturn: any) => any> = []
const onErrorCallbackList: Array<(error: unknown) => unknown> = []
function after(callback: _ArrayType<typeof afterCallbackList>) {
  afterCallbackList.push(callback)
}
function onError(callback: _ArrayType<typeof onErrorCallbackList>) {
  onErrorCallbackList.push(callback)
}

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
}

// 如果結果是promise,在promise中觸發afterCallbackList及onErrorCallbackList
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

} } ```

wrapAction首先返回一個函式,在這個函式中,首先將pinia設定為activePinia,觸發actionSubscriptions中的函式,然後執行action函式,如果執行過程中出錯,會執行onErrorCallbackList中的errorCallback,如果沒有出錯的話,執行afterCallbackList中的afterCallback,最後將action的返回結果return

wrapAction中的actionSubscriptions是個什麼呢?

其實actionSubscriptions中的callback就是是通過store.$onAction新增的回撥函式;在執行actionSubscriptions中的callback過程中,會將對應callback新增到afterCallbackListonErrorCallbackList中。例如:

```ts store.$onAction(({ after, onError, name, store }) => { after((value) => { console.log(value) })

onError((error) => { console.log(error) }) }) ```

遍歷完setupStore之後,會將setupStore合併至storestore的原始對物件中,以方便使用storeToRefs()檢索響應式物件。

ts if (isVue2) { Object.keys(setupStore).forEach((key) => { set( store, key, setupStore[key] ) }) } else { assign(store, setupStore) assign(toRaw(store), setupStore) }

緊接著攔截store.$stategetset方法:當呼叫store.$state時,能夠從pinia.state.value找到對應的state;當使用store.$state = xxx去修改值時,則呼叫$patch方法修改值。

ts Object.defineProperty(store, '$state', { get: () => (__DEV__ && hot ? hotState.value : pinia.state.value[$id]), set: (state) => { /* istanbul ignore if */ if (__DEV__ && hot) { throw new Error('cannot set hotState') } $patch(($state) => { assign($state, state) }) }, })

截止到此,store就準備完畢。如果在Vue2環境下,會將store._r設定為true。

ts if (isVue2) { store._r = true }

接下來就需要呼叫使用use方法註冊的plugins

ts pinia._p.forEach((extender) => { if (__DEV__ && IS_CLIENT) { const extensions = scope.run(() => extender({ store, app: pinia._a, pinia, options: optionsForPlugin, }) )! Object.keys(extensions || {}).forEach((key) => store._customProperties.add(key) ) assign(store, extensions) } else { // 將plugin的結果合併到store中 assign( store, scope.run(() => extender({ store, app: pinia._a, pinia, options: optionsForPlugin, }) )! ) } })

最後返回store

```ts if ( initialState && isOptionsStore && (options as DefineStoreOptions).hydrate ) { ;(options as DefineStoreOptions).hydrate!( store.$state, initialState ) }

isListening = true isSyncListening = true return store ```

接下來看下store中的幾個方法:

$onAction

在每個action中添加回調函式。回撥接收一個物件引數:該物件包含nameactionkey值)、store(當前store)、after(新增action執行完之後的回撥)、onError(新增action執行過程中的錯誤回撥)、argsaction的引數)屬性。

示例:

```ts // 統計add action的呼叫次數 let count = 0, successCount = 0, failCount = 0 store.$onAction(({ name, after, onError }) => { if (name === 'add') { count++ after((resolveValue) => { successCount++ console.log(resolveValue) })

onError((error) => {
  failCount++
  console.log(error)
})

} }) ```

$onAction內部通過釋出訂閱模式實現。在pinia中有個專門的訂閱模組subscriptions.ts,其中包含兩個主要方法:addSubscription(新增訂閱)、triggerSubscriptions(觸發訂閱)。

addSubscription可接收四個引數:subscriptions(訂閱列表)、callback(新增的訂閱函式)、detached(遊離的訂閱,如果為false在元件解除安裝後,自動移除訂閱;如果為true,不會自動移除訂閱)、onCleanup(訂閱被移除時的回撥)

triggerSubscriptions接收兩個引數:subscriptions(訂閱列表)、argsaction的引數列表)

```ts export function addSubscription( 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 }

export function triggerSubscriptions( subscriptions: T[], ...args: Parameters ) { subscriptions.slice().forEach((callback) => { callback(...args) }) } ```

$onAction通過addSubscription.bind(null, actionSubscriptions)實現。

如何觸發訂閱?

首先在store的初始化過程中,會將action使用wrapAction函式進行包裝,wrapAction返回一個函式,在這個函式中會先觸發actionSubscriptions,這個觸發過程中會將afterCallbackonErrorCallback新增到對應列表。然後呼叫action,如果呼叫過程中出錯,則觸發onErrorCallbackList,否則觸發afterCallbackList。如果action的結果是Promise的話,則在then中觸發onErrorCallbackList,在catch中觸發onErrorCallbackList。然後會將包裝後的action覆蓋原始action,這樣每次呼叫action時就是呼叫的包裝後的action

$patch

使用$patch可以更新state的值,可進行批量更新。$patch接收一個partialStateOrMutator引數,它可以是個物件也可以是個方法。

示例:

ts store.$patch((state) => { state.name = 'xxx' state.age = 14 }) // or store.$patch({ name: 'xxx', age: 14 })

$patch原始碼:

ts function $patch( partialStateOrMutator: | _DeepPartial<UnwrapRef<S>> | ((state: UnwrapRef<S>) => void) ): void { // 合併的相關資訊 let subscriptionMutation: SubscriptionCallbackMutation<S> // 是否觸發狀態修改後的回撥,isListening代表非同步觸發,isSyncListening代表同步觸發 // 此處先關閉回撥的觸發,防止修改state的過程中頻繁觸發回撥 isListening = isSyncListening = false if (__DEV__) { debuggerEvents = [] } // 如果partialStateOrMutator是個function,執行方法,傳入當前的store if (typeof partialStateOrMutator === 'function') { partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>) subscriptionMutation = { type: MutationType.patchFunction, storeId: $id, events: debuggerEvents as DebuggerEvent[], } } else { // 如果不是function,則呼叫mergeReactiveObjects合併state mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator) subscriptionMutation = { type: MutationType.patchObject, payload: partialStateOrMutator, storeId: $id, events: debuggerEvents as DebuggerEvent[], } } // 當合並完之後,將isListening、isSyncListening設定為true,意味著可以觸發狀態改變後的回撥函數了 const myListenerId = (activeListener = Symbol()) nextTick().then(() => { if (activeListener === myListenerId) { isListening = true } }) isSyncListening = true // 因為在修改pinia.state.value[$id]的過程中關閉(isSyncListening與isListening)了監聽,所以需要手動觸發訂閱列表 triggerSubscriptions( subscriptions, subscriptionMutation, pinia.state.value[$id] as UnwrapRef<S> ) }

$reset

通過構建一個新的state objectstate重置為初始狀態。只在options配置下生效。如果是setup配置,開發環境下報錯。

ts store.$reset = function $reset() { // 重新執行state,獲取一個新的state const newState = state ? state() : {} // 通過$patch,使用assign將newState合併到$state中 this.$patch(($state) => { assign($state, newState) }) }

$subscribe

設定state改變後的回撥,返回一個移除回撥的函式。可接受兩個引數:callback(新增的回撥函式)、options:{detached, flush, ...watchOptions}detachedaddSubscription中的detachedflush代表是否同步觸發回撥,可取值:sync)。

示例: ts store.$subribe((mutation: {storeId, type, events}, state) => { console.log(storeId) console.log(type) console.log(state) }, { detached: true, flush: 'sync' })

$subscribe原始碼:

```ts function $subscribe(callback, options = {}) { // 將callback新增到subscriptions中,以便使用$patch更新狀態時,觸發回撥 // 當使用removeSubscription移除callback時,停止對pinia.state.value[$id]監聽 const removeSubscription = addSubscription( subscriptions, callback, options.detached, () => stopWatcher() ) const stopWatcher = scope.run(() => // 監聽pinia.state.value[$id],以觸發callback,當使用$patch更新state時,不會進入觸發這裡的callback watch( () => pinia.state.value[$id] as UnwrapRef, (state) => { if (options.flush === 'sync' ? isSyncListening : isListening) { callback( { storeId: $id, type: MutationType.direct, events: debuggerEvents as DebuggerEvent, }, state ) } }, assign({}, $subscribeOptions, options) ) )!

return removeSubscription } ```

callback中的第一個引數中有個type屬性,表示是通過什麼方式更新的state,它有三個值:

  1. MutationType.direct:通過state.name='xxx'/store.$state.name='xxx'等方式修改
  2. MutationType.patchObject:通過store.$patch({ name: 'xxx' })方式修改
  3. MutationType.patchFunction:通過store.$patch((state) => state.name='xxx')方式修改

$dispose

銷燬store

ts function $dispose() { // 停止監聽 scope.stop() // 清空subscriptions及actionSubscriptions subscriptions = [] actionSubscriptions = [] // 從pinia._s中刪除store pinia._s.delete($id) }

createOptionsStore

createOptionsStore可接收引數如下:

| 引數 | 說明 | | | ----------------- | -------------------------------- | ------ | | id | 定義storeid | | | options | defineStoreoptions | | | pinia | Pinia例項 | | | hot | 是否啟用熱更新 | 可選 |

```ts function createOptionsStore< Id extends string, S extends StateTree, G extends _GettersTree, A extends _ActionsTree

( id: Id, options: DefineStoreOptions, pinia: Pinia, hot?: boolean ): Store { const { state, actions, getters } = options

const initialState: StateTree | undefined = pinia.state.value[id]

let store: Store

function setup() { // 如果pinia.state.value[id]不存在,進行初始化 if (!initialState && (!DEV || !hot)) { if (isVue2) { set(pinia.state.value, id, state ? state() : {}) } else { pinia.state.value[id] = state ? state() : {} } }

// 將pinia.state.value[id]各屬性值轉為響應式物件
const localState =
  __DEV__ && hot
    ? // use ref() to unwrap refs inside state TODO: check if this is still necessary
      toRefs(ref(state ? state() : {}).value)
    : toRefs(pinia.state.value[id])

// 處理getters,並將處理後的getters和actions合併到localState中
return assign(
  localState,
  actions,
  Object.keys(getters || {}).reduce((computedGetters, name) => {
    computedGetters[name] = markRaw(
      computed(() => {
        setActivePinia(pinia)
        const store = pinia._s.get(id)!

        if (isVue2 && !store._r) return

        return getters![name].call(store, store)
      })
    )
    return computedGetters
  }, {} as Record<string, ComputedRef>)
)

}

// 利用createSetupStore建立store store = createSetupStore(id, setup, options, pinia, hot, true)

// 重寫store.$reset store.$reset = function $reset() { const newState = state ? state() : {} this.$patch(($state) => { assign($state, newState) }) }

return store as any } ```

createOptionsStore中會根據傳入引數構造一個setup函式,然後通過createSetupStore建立一個store,並重寫store.$reset方法,最後返回store

這個setup函式中會將state()的返回值賦值給pinia.state.value[id],然後將pinia.state.value[id]進行toRefs,得到localState,最後將處理後的gettersactions都合併到localState中,將其返回。對於getters的處理:將每個getter函式都轉成一個計算屬性。

總結

defineStore返回一個useStore函式,通過執行useStore可以獲取對應的store。呼叫useStore時我們並沒有傳入id,為什麼能準確獲取store呢?這是因為useStore是個閉包,在執行useStore執行過程中會自動獲取id

獲取store的過程:

  1. 首先獲取元件例項
  2. 使用inject(piniaSymbol)獲取pinia例項
  3. 判斷pinia._s中是否有對應id的鍵,如果有直接取對應的值作為store,如果沒有則建立store

store建立流程分兩種:setup方式與options方式

setup方式: 1. 首先在pinia.state.value中新增鍵為$id的空物件,以便後續賦值 2. 使用reactive宣告一個響應式物件store 3. 將store存至pinia._s中 4. 執行setup獲取返回值setupStore 5. 遍歷setupStore的鍵值,如果值是ref(不是computed)或reactive,將鍵值新增到pinia.state.value[$id]中;如果值時function,首先將值使用wrapAction包裝,然後用包裝後的function替換setupStore中對應的值 6. 將setupStore合併到store中 7. 攔截store.$state,使get操作可以正確獲取pinia.state.value[$id]set操作使用this.$patch更新 8. 呼叫pinia._p中的擴充套件函式,擴充套件store

options方式: 1. 從options中提取stategetteractions 2. 構建setup函式,在setup函式中會將getter處理成計算屬性 3. 使用setup方式建立store 4. 重寫store.$reset