Pinia(皮尼亚)源码分析

3,174 阅读3分钟

文章基于Pinia版本是:2.0.3

看源码之前,先了解下Pinia的用法浅谈Pinia(皮尼亚)--为什么vue3推荐使用Pinia

Pinia源码仓库地址

1. 入口 createPinia

从项目根目录可以看到,pinia是基于rollup打包的,找到rollup.config.js, 发现入口是src/index.ts

截屏2021-11-17 下午2.24.08.png

在业务测是通过createPinia创建的Pinia实例,在vue中,使用app.use(pinia)加载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')

打开packages/pinia/src/index.ts

找到createPinia方法

截屏2021-11-17 下午2.37.41.png

路径(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[] = []
    
  // 当前的pinia实例 
  const pinia: Pinia = markRaw({
    install(app: App) { // 这是vue的插件机制,对外暴露install方法
      // this allows calling useStore() outside of a component setup after
      // installing pinia's plugin
      setActivePinia(pinia) // 设置当前活跃的 pinia
      if (!isVue2) {
        pinia._a = app
        app.provide(piniaSymbol, pinia) // 通过provide传递pinia实例,提供给后续使用
        app.config.globalProperties.$pinia = pinia // 设置全局属性 $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)) // // 加载pinia插件
        toBeInstalled = []
      }
    },

    use(plugin) {  // pinia对外暴露的插件用法
      if (!this._a && !isVue2) {
        toBeInstalled.push(plugin)  // 将插件存入toBeInstalled,待初始化的时候使用
      } 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, // 所有状态
  })

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

  return pinia
}

关键源码分析已在上面标明

2. 定义 defineStore

业务测用法

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++ 
        }, 
    }, 
})

源码实现

截屏2021-11-17 下午2.57.43.png

路径(packages/pinia/src/store.ts)

截屏2021-11-17 下午2.59.59.png

可以看到defineStore最终返回useStore,并标记唯一$id

根据传参格式,获取id和options

 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
  }

useStore

useStore代码逐步分析

    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))

通过vuegetCurrentInstance拿到当前的vue实例,接着判断pinia是否存在,不存在的话,通过inject(piniaSymbol)获取(install时提供的app.provide(piniaSymbol, pinia)

if (pinia) setActivePinia(pinia)

设置当前活跃的pinia实例,有多个pinia实例时,方便获取当前活跃的pinia实例

setActivePinia代码路径(packages/pinia/src/rootStore.ts)

export let activePinia: Pinia | undefined

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

activePinia 不存在时,报错提示入口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
      }
    }

一开始,pinia._s.has(id)没值,所以进入里面的逻辑, 这里传参格式已

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

为例,所以走else的逻辑

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

接着看createOptionsStore(id, options as any, pinia)函数做了什么

function createOptionsStore<
  Id extends string,
  S extends StateTree,
  G extends _GettersTree<S>,
  A extends _ActionsTree
>(
  id: Id,
  options: DefineStoreOptions<Id, S, G, A>,
  pinia: Pinia,
  hot?: boolean
): Store<Id, S, G, A> {
  // 根据传参,初始化数据
  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
            return getters![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
}

根据传参,初始化数据。setup函数整合state、getters响应式数据,再整合actions合并返回

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])

localState把业务测传入的state整成响应式的

    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
            return getters![name].call(store, store)
          })
        )
        return computedGetters
      }, {} as Record<string, ComputedRef>)

上面代码把getters的value值由原本的普通函数转成计算属性,返回回调函数带有store参数,所以业务测可以获取到state,如下:

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

接着调用 store = createSetupStore(id, setup, options, pinia, hot)

分析下createSetupStore函数里的核心代码

$patch方法

  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 {
      /*
       ** 合并partialStateOrMutator 到 state中。
       ** 比如:counter.$patch({ count: counter.count + 1 }),
       ** { count: counter.count + 1 } 更新到 state中
       */
      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>
    )
  }

先看下业务测使用方式

// $patch是更新store的一种方式
counter.$patch({ count: counter.count + 1 })

根据传入的partialStateOrMutator参数类型,走相应的逻辑。以上面例子为例:

走以下逻辑

else {
     /*
      ** 合并partialStateOrMutator 到 state中。
      ** 比如:counter.$patch({ count: counter.count + 1 }),
      ** { count: counter.count + 1 } 更新到 state中
      */
     mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
     ...
   }

mergeReactiveObjects函数作用:合并partialStateOrMutator 到 state中

比如:

counter.$patch({ count: counter.count + 1}),
// { count: counter.count + 1 } 更新到 state中

接着定义$dispose,处理清除逻辑

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

接着整合partialStore

截屏2021-11-17 下午4.29.45.png

partialStore整合_p、$id、$onAction、$patch、$subscribe(callback, options = {})、$dispose,后续会合并到useStore()

接着整合store,挂载到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)

接着调用assign(store, setupStore),把setupStore值(state,getters,actions等)值合并进来(因为store是引用类型,所以会改变)

最后,useStore函数返回如下

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

pinia._s.get(id)值就是上面pinia._s.set($id, store)设置的

3.总结

Pinia YYDS