前言

【pinia源码】系列文章次要剖析pinia的实现原理。该系列文章源码参考pinia v2.0.14

源码地址:https://github.com/vuejs/pinia

官网文档:https://pinia.vuejs.org

本篇文章将剖析defineStore的实现。

应用

通过defineStore定义一个store

const useUserStore = defineStore('counter', {  state: () => ({    count: 0  }),  actions: {    increment() {      this.count++    }  }})// orconst useUserStore = defineStore({  id: 'counter',  state: () => ({    count: 0  }),  actions: {    increment() {      this.count++    }  }})// orconst useUserStore = defineStore('counter', () => {  const count = ref(0)    function increment() {    count.value++  }  return { count, increment }})

defineStore

export function defineStore(  idOrOptions: any,  setup?: any,  setupOptions?: any): StoreDefinition {  let id: string  let options:    | DefineStoreOptions<        string,        StateTree,        _GettersTree<StateTree>,        _ActionsTree      >    | DefineSetupStoreOptions<        string,        StateTree,        _GettersTree<StateTree>,        _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的函数类型定义。

export function defineStore<  Id extends string,  S extends StateTree = {},  G extends _GettersTree<S> = {},  A /* extends ActionsTree */ = {}>(  id: Id,  options: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>): StoreDefinition<Id, S, G, A>export function defineStore<  Id extends string,  S extends StateTree = {},  G extends _GettersTree<S> = {},  A /* extends ActionsTree */ = {}  >(options: DefineStoreOptions<Id, S, G, A>): StoreDefinition<Id, S, G, A>export function defineStore<Id extends string, SS>(  id: Id,  storeSetup: () => SS,  options?: DefineSetupStoreOptions<    Id,    _ExtractStateFromSetupStore<SS>,    _ExtractGettersFromSetupStore<SS>,    _ExtractActionsFromSetupStore<SS>    >): StoreDefinition<  Id,  _ExtractStateFromSetupStore<SS>,  _ExtractGettersFromSetupStore<SS>,  _ExtractActionsFromSetupStore<SS>  >

首先在defineStore中申明了三个变量:idoptionsisSetupStore,其中id为定义的store的惟一idoptions为定义store时的optionsisSetupStore代表传入的setup是不是个函数。

而后依据传入的idOrOptions的类型,为idotions赋值。紧接着申明了一个useStore函数,并将id赋给它,而后将其return。截止到此,咱们晓得defineStore会返回一个函数,那么这个函数具体是做什么的呢?咱们持续看useStore的实现。

useStore

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的函数
optionsdefineStoreoptions
piniaPinia实例
hot是否启用热更新可选
isOptionsStore是否应用options申明的store可选

createSetupStore代码有500多行,如果从头开始看的话,不容易了解。咱们能够依据createSetupStore的用处,从其外围开始看。因为createSetupStore是须要创立store,并将store注册到pinia._s中,所以createSetupStore中可能须要创立store,咱们找到创立store的中央。

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<S>,        (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<Id, S, G, A>if (isVue2) {  partialStore._r = false}const store: Store<Id, S, G, A> = reactive(  assign(    __DEV__ && IS_CLIENT      ? // devtools custom properties        {          _customProperties: markRaw(new Set<string>()),          _hmrPayload,        }      : {},    partialStore  )) as unknown as Store<Id, S, G, A>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.

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中。

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

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中。例如:

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

遍历完setupStore之后,会将setupStore合并至storestore的原始对对象中,以方便使用storeToRefs()检索响应式对象。

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办法批改值。

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。

if (isVue2) {  store._r = true}

接下来就须要调用应用use办法注册的plugins

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

if (  initialState &&  isOptionsStore &&  (options as DefineStoreOptions<Id, S, G, A>).hydrate) {  ;(options as DefineStoreOptions<Id, S, G, A>).hydrate!(    store.$state,    initialState  )}isListening = trueisSyncListening = truereturn store

接下来看下store中的几个办法:

$onAction

在每个action中增加回调函数。回调接管一个对象参数:该对象蕴含nameactionkey值)、store(以后store)、after(增加action执行完之后的回调)、onError(增加action执行过程中的谬误回调)、argsaction的参数)属性。

示例:

// 统计add action的调用次数let count = 0, successCount = 0, failCount = 0store.$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的参数列表)

export function addSubscription<T extends _Method>(  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<T extends _Method>(  subscriptions: T[],  ...args: Parameters<T>) {  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参数,它能够是个对象也能够是个办法。

示例:

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

$patch源码:

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配置,开发环境下报错。

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

示例:

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

$subscribe源码:

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<S>,      (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

function $dispose() {  // 进行监听  scope.stop()  // 清空subscriptions及actionSubscriptions  subscriptions = []  actionSubscriptions = []  // 从pinia._s中删除store  pinia._s.delete($id)}

createOptionsStore

createOptionsStore可接管参数如下:

参数阐明
id定义storeid
optionsdefineStoreoptions
piniaPinia实例
hot是否启用热更新可选
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() {    // 如果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