The article based on Pinia version is: 2.0.3

A brief introduction to Pinia — Why vuE3 recommends using Pinia

Pinia source repository address

1. Entrance createPinia

As you can see from the project root, Pinia is packaged based on rollup. Find rollup.config.js and find the entry SRC /index.ts

In business testing an instance of Pinia is created through createPinia, and in vue, the Pinia is loaded using app.use(Pinia)

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App).use(pinia) 
app.mount('#app')
Copy the code

Open the packages/pinia/SRC/index. The ts

Find the createPinia method

Path (packages/pinia/SRC/createPinia ts)

/** * Creates a Pinia instance to be used by the application */
export function createPinia() :Pinia {
  const scope = effectScope(true)
  // NOTE: here we could check the window object for a state and directly set it
  // if there is anything like it with Vue 3 SSR
  const state = scope.run(() = > ref<Record<string, StateTree>>({}))!

  let _p: Pinia['_p'] = []
  // plugins added before calling app.use(pinia)
  let toBeInstalled: PiniaPlugin[] = []
    
  // The current pinia instance
  const pinia: Pinia = markRaw({
    install(app: App) { // This is vue's plug-in mechanism, exposing the install method
      // this allows calling useStore() outside of a component setup after
      // installing pinia's plugin
      setActivePinia(pinia) // Set the currently active pinia
      if(! isVue2) { pinia._a = app app.provide(piniaSymbol, pinia)// Pass pinia instances through provide for subsequent use
        app.config.globalProperties.$pinia = pinia // Set the global attribute $pinia
        /* istanbul ignore else */
        if (__DEV__ && IS_CLIENT) {
          // @ts-expect-error: weird type in devtools api
          registerPiniaDevtools(app, pinia)
        }
        toBeInstalled.forEach((plugin) = > _p.push(plugin)) // // Load the Pinia plug-in
        toBeInstalled = []
      }
    },

    use(plugin) {  // Pinia exposed plugin usage
      if (!this._a && ! isVue2) { toBeInstalled.push(plugin)// Save the plug-in to toBeInstalled for initialization
      } else {
        _p.push(plugin)
      }
      return this
    },

    _p,
    // it's actually undefined here
    // @ts-expect-error
    _a: null._e: scope,
    _s: new Map<string, StoreGeneric>(),
    state, // All states
  })

  // pinia devtools rely on dev only features so they cannot be forced unless
  // the dev build of Vue is used
  if (__DEV__ && IS_CLIENT) {
    // Integrate vue devTools
    pinia.use(devtoolsPlugin)
  }

  return pinia
}
Copy the code

Critical source analysis is indicated above

2. Define defineStore

Business test usage

import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
    state: () = > { 
        return { count: 0}},// could also be defined as 
    // state: () => ({ count: 0 }) 
    actions: { 
        increment() { 
            this.count++ 
        }, 
    }, 
})

Copy the code

The source code to achieve

Path (packages/pinia/SRC/store ts)

You can see defineStore finally returns useStore and marks a unique $ID

Get the ID and options based on the parameter format

 if (typeof idOrOptions === 'string') {
    id = idOrOptions
    // the option store setup will contain the actual options in this case
    options = isSetupStore ? setupOptions : setup
  } else {
    options = idOrOptions
    id = idOrOptions.id
  }
Copy the code

useStore

Step by step analysis of useStore code

    const currentInstance = getCurrentInstance()
    pinia =
      // in test mode, ignore the argument provided as we can always retrieve a
      // pinia instance with getActivePinia()
      (__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
      (currentInstance && inject(piniaSymbol))
Copy the code

Get the current vue instance via getCurrentInstance of vue and determine if Pinia exists. If not, Obtain by inject(piniaSymbol) (App.provide (piniaSymbol, Pinia) provided with install)

if (pinia) setActivePinia(pinia)
Copy the code

Set the currently active Pinia instance. When there are multiple Pinia instances, it is convenient to obtain the currently active Pinia instance

SetActivePinia code path (packages/pinia/SRC/rootStore ts)

export let activePinia: Pinia | undefined

export const setActivePinia = (pinia: Pinia | undefined) =>
  (activePinia = pinia)
Copy the code
  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.`)}Copy the code

ActivePinia does not exist, error message entry use(pinia)

if (! pinia._s.has(id)) { // creating the store registers it in `pinia._s` if (isSetupStore) { createSetupStore(id, setup, options, pinia) } else { createOptionsStore(id, options as any, pinia) } /* istanbul ignore else */ if (__DEV__) { // @ts-expect-error: not the right inferred type useStore._pinia = pinia } }Copy the code

At first, pinia._s.has(id) has no value, so enter the logic inside, where the format of the parameter is changed

defineStore('counter', {
    state: () = > { 
        return { count: 0}},// could also be defined as 
    // state: () => ({ count: 0 }) 
    actions: { 
        increment() { 
            this.count++ 
        }, 
    }, 
})
Copy the code

For example, so else logic

else {
    createOptionsStore(id, options as any, pinia)
}
Copy the code

What does createOptionsStore(ID, options as any, pinia) do

function createOptionsStore<
  Id extends string.S extends StateTree.G extends _GettersTree<S>,
  A extends _ActionsTree> (id: Id, options: DefineStoreOptions
       
        , pinia: Pinia, hot? : boolean
       ,>) :Store<Id.S.G.A> {
  // Initialize the data according to the parameters passed
  const { state, actions, getters } = options

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

  let store: Store<Id, S, G, A>

  function setup() {
    if(! initialState && (! __DEV__ || ! hot)) {/* istanbul ignore if */
      if (isVue2) {
        set(pinia.state.value, id, state ? state() : {})
      } else {
        pinia.state.value[id] = state ? state() : {}
      }
    }

    // avoid creating a state in pinia.state.value
    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])

    return assign(
      localState,
      actions,
      Object.keys(getters || {}).reduce((computedGetters, name) = > {
        computedGetters[name] = markRaw(
          computed(() = > {
            setActivePinia(pinia)
            // it was created just before
            const store = pinia._s.get(id)!

            // allow cross using stores
            /* istanbul ignore next */
            if(isVue2 && ! store._r)return

            // @ts-expect-error
            // return getters! [name].call(context, context)
            // TODO: avoid reading the getter while assigning with a global variable
            returngetters! [name].call(store, store) }) )return computedGetters
      }, {} as Record<string, ComputedRef>)
    )
  }

  store = createSetupStore(id, setup, options, pinia, hot)

  store.$reset = function $reset() {
    const newState = state ? state() : {}
    // we use a patch to group all changes into one single subscription
    this.$patch(($state) = > {
      assign($state, newState)
    })
  }

  return store as any
}
Copy the code

Initializes the data according to the passed parameters. The setup function merges state and getters into responsive data, and merges actions into return

if(! initialState && (! __DEV__ || ! hot)) {/* istanbul ignore if */
      if (isVue2) {
        set(pinia.state.value, id, state ? state() : {})
      } else {
        pinia.state.value[id] = state ? state() : {}
      }
    }

    // avoid creating a state in pinia.state.value
    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])
Copy the code

LocalState aggregates the incoming state from the business test into a response

    Object.keys(getters || {}).reduce((computedGetters, name) = > {
        computedGetters[name] = markRaw(
          computed(() = > {
            setActivePinia(pinia)
            // it was created just before
            const store = pinia._s.get(id)!

            // allow cross using stores
            /* istanbul ignore next */
            if(isVue2 && ! store._r)return

            // @ts-expect-error
            // return getters! [name].call(context, context)
            // TODO: avoid reading the getter while assigning with a global variable
            returngetters! [name].call(store, store) }) )return computedGetters
      }, {} as Record<string, ComputedRef>)
Copy the code

The above code converts the value of getters from an ordinary function to a calculated property. The return callback function has a store parameter, so the business test can obtain the state as follows:

  getters: {
    double: (state) = > state.n * 2,},Copy the code

Call store = createSetupStore(ID, setup, options, pinia, hot)

Analyze the core code in the createSetupStore function

$patch method

  function $patch(
    partialStateOrMutator:
      | DeepPartial<UnwrapRef<S>>
      | ((state: UnwrapRef<S>) => void)
  ) :void {
    let subscriptionMutation: SubscriptionCallbackMutation<S>
    isListening = false
    // reset the debugger events since patches are sync
    /* istanbul ignore else */
    if (__DEV__) {
      debuggerEvents = []
    }
    if (typeof partialStateOrMutator === 'function') {
      partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
      subscriptionMutation = {
        type: MutationType.patchFunction,
        storeId: $id,
        events: debuggerEvents as DebuggerEvent[],
      }
    } else {
      /* ** Merge partialStateOrMutator into state. $patch({count: counter. Count + 1}), ** {count: counter
      mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
      subscriptionMutation = {
        type: MutationType.patchObject,
        payload: partialStateOrMutator,
        storeId: $id,
        events: debuggerEvents as DebuggerEvent[],
      }
    }
    isListening = true
    console.log('---subscriptions', subscriptions)
    // because we paused the watcher, we need to manually call the subscriptions
    triggerSubscriptions(
      subscriptions,
      subscriptionMutation,
      pinia.state.value[$id] as UnwrapRef<S>
    )
  }
Copy the code

First take a look at the service test usage

$patch is a way to update the store
counter.$patch({ count: counter.count + 1 })
Copy the code

Based on the type of the partialStateOrMutator parameter passed in, the corresponding logic is taken. Take the example above:

Follow the following logic

else {
     /* ** Merge partialStateOrMutator into state. $patch({count: counter. Count + 1}), ** {count: counter
     mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
     ...
   }
Copy the code

The mergeReactiveObjects function merges the partialStateOrMutator into the state

Such as:

counter.$patch({ count: counter.count + 1}),// {count: counter.count + 1} updates to state
Copy the code

Then you define $dispose, which handles the cleanup logic

 function $dispose() {
    scope.stop()
    subscriptions = []
    actionSubscriptions = []
    pinia._s.delete($id)
  }
Copy the code

Next, integrate the partialStore

PartialStore merge _p, $ID, $onAction, $patch, $SUBSCRIBE (callback, options = {}), $dispose, later merge to useStore()

Pinia._s. set($id, store)

const store: Store<Id, S, G, A> = reactive(
    assign(
      __DEV__ && IS_CLIENT
        ? // devtools custom properties
          {
            _customProperties: markRaw(new Set<string>()),
            _hmrPayload,
          }
        : {},
      partialStore
      // must be added later
      // setupStore))as unknown as Store<Id, S, G, A>

  // store the partial store now so the setup of stores can instantiate each other before they are finished without
  // creating infinite loops.
  pinia._s.set($id, store)
Copy the code

Then call assign(Store, setupStore) and merge the setupStore values (state,getters, Actions, etc.).

Finally, the useStore function returns the following

const store: StoreGeneric = pinia._s.get(id)!
return store as any
Copy the code

The value pinia._s.get(id) is set above by pinia._s.set($id, store)

3. Summary

Pinia YYDS