Skip to content

Pinia

对于 Pinia 其定义一个 Store 是通过 defineStore 的方式去进行的,

defineStore

其提供了两种定义方式

  1. Option 配置的方式
ts
const store = defineStore("storeId", {
  state: () => {
    a: 1
  },
  getters: {},
  actions: {
    increment() {
      this.a++
    },
  },
})

const store = defineStore({
  id: "storeId2",
  state: () => {
    a: 1
  },
  getters: {},
  actions: {
    increment() {
      this.a++
    },
  },
})
  1. setup 配置的方式
ts
const store = defineStore("storeId3", () => {
  const a = ref(1)
  const increment = () => {
    a.value++
  }
  return {
    a,
    increment,
  }
})

那么我们就基于这两种定义方式为入口去看看 defineStore 的核心源码

ts
export function defineStore(
  // TODO: add proper types from above
  idOrOptions: any, // store的 id
  setup?: any,
  setupOptions?: any
): StoreDefinition {
  let id: string
  let options:
    | DefineStoreOptions<string, StateTree, _GettersTree<StateTree>, _ActionsTree>
    | DefineSetupStoreOptions<string, StateTree, _GettersTree<StateTree>, _ActionsTree>
  // 1. 处理入参,判断第二个参数是否为函数类型
  const isSetupStore = typeof setup === "function"
  // 判断第一个参数是否为字符串, 字符串代表就是store的id
  if (typeof idOrOptions === "string") {
    id = idOrOptions
    // the option store setup will contain the actual options in this case
    options = isSetupStore ? setupOptions : setup
  } else {
    // 非字符串类型的那肯定是 Option配置类型的入参
    options = idOrOptions
    id = idOrOptions.id
  }

  // 提供核心的useStore 方法
  function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
    //.....
  }

  useStore.$id = id

  return useStore
}

其主要就两个功能

  1. 处理入参,根据三种入参类型的传入方式去获取入参的 id、 options、setup 和 isSetupStore
  2. 生成 useStore 方法并返回

useStore

这是提供了 Hooks 的方式让用户去使用 store 的所有功能能力,这样就可以在组件或者其他地方通过 const store = useStore();从而获取到当前 store 中的 state、getter、action 和其他方法函数。

核心源码

ts
// 提供核心的useStore 方法
function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
  // 1. 获取全家的 pinia 实例对象
  const hasContext = hasInjectionContext()
  pinia =
    // in test mode, ignore the argument provided as we can always retrieve a
    // pinia instance with getActivePinia()
    (__TEST__ && activePinia && activePinia._testing ? null : pinia) || (hasContext ? inject(piniaSymbol, null) : null)
  if (pinia) setActivePinia(pinia)

  pinia = activePinia!

  // 2. 初次访问的时候初始化 store 实例对象
  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)
    }
  }
  // 获取当前store实例对象
  const store: StoreGeneric = pinia._s.get(id)!
  // StoreGeneric cannot be casted towards Store
  return store as any
}

分析

主要分为三个步骤

  1. 获取当前 app 下的唯一的 pinia 实例对象(非 store 对象)

  2. 在初次访问当前 store 的时候,通过两种方式去初始化 store 实例对象,并将其以 store.id 为 key 存储到 pinia._s

  3. 向外提供 store 实例对象

其核心就是两种初始化 store 的方法

  • createSetupStore : setup 的方式去初始化 store
  • createOptionsStore : option 的方式去初始化 store(其实际上也是组装成 setup 然后通过 setup 的方式去初始化)

createSetupStore

setup 方式创建的 store

例子

ts
const store = defineStore("storeId3", () => {
  const a = ref(1)
  const increment = () => {
    a.value++
  }
  return {
    a,
    increment,
  }
})

其实简单分析其也继承了很多 Vuex 的特点(state、getter、action),移除了 mutations 的概念,例外其不像 Vuex 使用 this.$store.xxx方式去执行对应的方法,而是提供了$patch$reset$subscribe$dispose$onAction等方法。下面我们按照处理这些属性或者方法的功能去分析源码

state

在 state 方面其跟 Vuex 不同的地方主要是,其本身是响应式,所以我们可以直接修改其内容就自动触发对应的更新

image-20230923162032605

从结果可以看出对于 state 其存放在两个地方 : 1. store 对象上 2. store.$state 对象上。 这样我们使用 currentDate 的时候就可以有两种方式

  1. store 对象上

const { currentDate } = store

  1. store.$state 对象上

const { currentDate } = store.$state

下面我们从源码分析

源码

ts
function createSetupStore<
  Id extends string,
  SS extends Record<any, unknown>,
  S extends StateTree,
  G extends Record<string, _Method>,
  A extends _ActionsTree
>(
  $id: Id,
  setup: () => SS,
  options: DefineSetupStoreOptions<Id, S, G, A> | DefineStoreOptions<Id, S, G, A> = {},
  pinia: Pinia,
  hot?: boolean,
  isOptionsStore?: boolean
): Store<Id, S, G, A> {
  let scope!: EffectScope

  // state 1. 获取是否存在初始化的值
  // 主要提供给将store缓存到本地功能
  const initialState = pinia.state.value[$id] as UnwrapRef<S> | undefined

  // avoid setting the state for option stores if it is set
  // by the setup
  // state 2. 如果不存在那就初始化{}
  if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
    /* istanbul ignore if */
    if (isVue2) {
      set(pinia.state.value, $id, {})
    } else {
      pinia.state.value[$id] = {}
    }
  }

  const hotState = ref({} as S)
  // 3. 按照 state actions getters去初始化一个缓存数据的对象
  const _hmrPayload = /*#__PURE__*/ markRaw({
    actions: {} as Record<string, any>,
    getters: {} as Record<string, Ref>,
    state: [] as string[],
    hotState,
  })

  // 4. 响应式 合并生成一个初始化的 store对象
  const store: Store<Id, S, G, A> = reactive(
    __DEV__ || USE_DEVTOOLS
      ? assign(
          {
            _hmrPayload,
            _customProperties: markRaw(new Set<string>()), // devtools custom properties
          },
          partialStore
          // must be added later
          // setupStore
        )
      : partialStore
  ) 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)

  const runWithContext = (pinia._a && pinia._a.runWithContext) || fallbackRunWithContext

  // TODO: idea create skipSerialize that marks properties as non serializable and they are skipped
  // 5. 执行 setup 函数获取 store的结果
  const setupStore = runWithContext(() => pinia._e.run(() => (scope = effectScope()).run(setup)!))!

  // overwrite existing actions to support $onAction
  // 对返回的结果每一个值进行分别处理
  //  1. ref 类型 或者 reactive 类型 => 存放到 store.state.value[key] 上
  //  2. 计算属性类型    => 存放到  _hmrPayload.getters[key]
  //  3. 函数类型   => 存放到 setupStore[key] 和 _hmrPayload.actions[key]
  for (const key in setupStore) {
    const prop = setupStore[key]

    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      // mark it as a piece of state to be serialized
      if (__DEV__ && hot) {
        set(hotState.value, key, toRef(setupStore as any, key))
        // createOptionStore directly sets the state in pinia.state.value so we
        // can just skip that
      } else if (!isOptionsStore) {
        // in setup stores we must hydrate the state and sync pinia state tree with the refs the user just created
        if (initialState && shouldHydrate(prop)) {
          if (isRef(prop)) {
            prop.value = initialState[key]
          } else {
            // probably a reactive object, lets recursively assign
            // @ts-expect-error: prop is unknown
            mergeReactiveObjects(prop, initialState[key])
          }
        }
        // transfer the ref to the pinia state to keep everything in sync
        /* istanbul ignore if */
        if (isVue2) {
          set(pinia.state.value[$id], key, prop)
        } else {
          pinia.state.value[$id][key] = prop
        }
      }

      /* istanbul ignore else */
      if (__DEV__) {
        _hmrPayload.state.push(key)
      }
      // action
    } else if (typeof prop === "function") {
      // @ts-expect-error: we are overriding the function we avoid wrapping if
      const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop)
      // this a hot module replacement store because the hotUpdate method needs
      // to do it with the right context
      /* istanbul ignore if */
      if (isVue2) {
        set(setupStore, key, actionValue)
      } else {
        // @ts-expect-error
        setupStore[key] = actionValue
      }

      /* istanbul ignore else */
      if (__DEV__) {
        _hmrPayload.actions[key] = prop
      }

      // list actions so they can be used in plugins
      // @ts-expect-error
      optionsForPlugin.actions[key] = prop
    } else if (__DEV__) {
      // add getters for devtools
      if (isComputed(prop)) {
        _hmrPayload.getters[key] = isOptionsStore
          ? // @ts-expect-error
            options.getters[key]
          : prop
        if (IS_CLIENT) {
          const getters: string[] =
            (setupStore._getters as string[]) ||
            // @ts-expect-error: same
            ((setupStore._getters = markRaw([])) as string[])
          getters.push(key)
        }
      }
    }
  }

  // add the state, getters, and action properties
  /* istanbul ignore if */
  // 6. 将setup执行和初次处理的结果合并到 store中
  if (isVue2) {
    Object.keys(setupStore).forEach(key => {
      set(store, key, setupStore[key])
    })
  } else {
    assign(store, setupStore)
    // allows retrieving reactive objects with `storeToRefs()`. Must be called after assigning to the reactive object.
    // Make `storeToRefs()` work with `reactive()` #799
    assign(toRaw(store), setupStore)
  }

  // use this instead of a computed with setter to be able to create it anywhere
  // without linking the computed lifespan to wherever the store is first
  // created.
  // 7. 创建store.$state对象
  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
      $patch($state => {
        assign($state, state)
      })
    },
  })

  // only apply hydrate to option stores with an initial state in pinia
  if (initialState && isOptionsStore && (options as DefineStoreOptions<Id, S, G, A>).hydrate) {
    ;(options as DefineStoreOptions<Id, S, G, A>).hydrate!(store.$state, initialState)
  }

  isListening = true
  isSyncListening = true
  return store
}

分析

  1. 初始化一些对象, 如 initialStatehotState_hmrPayloadstore
  2. 执行 store 内容获取结果(执行 setup 的内容获取初始化的值)
  3. 对于 setupResult 进行分类处理
    1. ref 类型 或者 reactive 类型 => 存放到 store.state.value[key]
    1. 计算属性类型 => 存放到 _hmrPayload.getters[key]
    1. 函数类型 => 存放到 setupStore[key]_hmrPayload.actions[key]
  1. 将 setup 执行的结果合并到 store 中
ts
if (isVue2) {
  Object.keys(setupStore).forEach(key => {
    set(store, key, setupStore[key])
  })
} else {
  assign(store, setupStore)
  // allows retrieving reactive objects with `storeToRefs()`. Must be called after assigning to the reactive object.
  // Make `storeToRefs()` work with `reactive()` #799
  assign(toRaw(store), setupStore)
}

这样我们就可以通过 store.currentDate 获取到对应的值了,但是这时候 store.$state 还不存在,那么下一步就是

  1. 创建 store.$state 对象
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
    $patch($state => {
      assign($state, state)
    })
  },
})

对于访问 其代理到了 pinia.state.value[$id] 上 (对于 pinia 其会将所有的 store 的 state 按照 id 存放到 pinia.state.value 上)

对于赋值,很取巧也就是 触发$patch 函数

所以对于 state 的更新其有两种方式

  1. 直接修改 store.xxx 或者 store.$state.xxx
  2. 通过 $patch(() => { state.xxxx = xxx}) 的方式去修改
  3. action 中也可以修改 changeCurrentDate = () => {this.xxx = xxx }

getters

从上面 state 我们其实已经整体了解 pinia 对于关键三元素 state、getter、actions 的存储方式,下面直接简单说明一下

image-20230923164339248

  1. 在处理 setupResult 结果分类的时候对于 计算类型 的会将其存放到 setupStore._getters_hmrPayload.getters[key]

  2. 在 assign 的时候将 setupResult 结果合并到 store 中

ts
if (isVue2) {
  Object.keys(setupStore).forEach(key => {
    set(store, key, setupStore[key])
  })
} else {
  assign(store, setupStore)
  // allows retrieving reactive objects with `storeToRefs()`. Must be called after assigning to the reactive object.
  // Make `storeToRefs()` work with `reactive()` #799
  assign(toRaw(store), setupStore)
}

actions

对于 action 类型的 其不会简单的通过 assgin 合并到 store 上了,而是在处理 setupResult 的时候进行了一个 wrapAction 的包装,其包装的源码为

ts
if (typeof prop === "function") {
  // @ts-expect-error: we are overriding the function we avoid wrapping if
  const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop)
  // this a hot module replacement store because the hotUpdate method needs
  // to do it with the right context
  /* istanbul ignore if */
  if (isVue2) {
    set(setupStore, key, actionValue)
  } else {
    // @ts-expect-error
    // 覆盖原来的action方法
    setupStore[key] = actionValue
  }
}

下面我们详细看一下 wrapAction

wrapAction

对于 action 的执行 pinia 需要在其执行前、执行成功后、执行失败后执行一些订阅的回调函数,目的主要是为了触发用户自定义的 如 store.$onAction 或者插件定义了一个函数

ts
/**
 * Wraps an action to handle subscriptions.
 * actions 执行回调的核心方法
 *
 * @param name - name of the action  action的名称
 * @param action - action to wrap   action的回调函数
 * @returns a wrapped action to handle subscriptions
 */
function wrapAction(name: string, action: _Method) {
  return function (this: any) {
    // 重置pinia实例对象
    setActivePinia(pinia)
    const args = Array.from(arguments)

    // 存放 action执行成功后的回调函数
    const afterCallbackList: Array<(resolvedReturn: any) => any> = []
    // 存放 action执行失败后的回调函数
    const onErrorCallbackList: Array<(error: unknown) => unknown> = []
    // 提供 store.$onAction订阅的对于action修改的监听函数中对于 执行成功的回调钩子
    function after(callback: _ArrayType<typeof afterCallbackList>) {
      afterCallbackList.push(callback)
    }
    // 提供 store.$onAction订阅的对于action修改的监听函数中对于 执行失败的回调钩子
    function onError(callback: _ArrayType<typeof onErrorCallbackList>) {
      onErrorCallbackList.push(callback)
    }

    // @ts-expect-error
    // 触发通过 store.$onAction订阅的对于action修改的监听函数
    triggerSubscriptions(actionSubscriptions, {
      args,
      name,
      store,
      after,
      onError,
    })

    let ret: any
    try {
      // 回调action
      ret = action.apply(this && this.$id === $id ? this : store, args)
      // handle sync errors
    } catch (error) {
      triggerSubscriptions(onErrorCallbackList, error)
      throw error
    }

    if (ret instanceof Promise) {
      return ret
        .then(value => {
          // 触发action执行成功的回调钩子函数
          triggerSubscriptions(afterCallbackList, value)
          return value
        })
        .catch(error => {
          triggerSubscriptions(onErrorCallbackList, error)
          return Promise.reject(error)
        })
    }

    // trigger after callbacks
    // 触发action执行成功的回调钩子函数
    triggerSubscriptions(afterCallbackList, ret)
    return ret
  }
}

可以通过一个简单的例子去分析

ts
const { $onAction, changeCurrentDate } = useStore()

$onAction(({ args, name, store, after, onError }) => {
  // 当changeCurrentDate触发的时候其 name = changeCurrentDate args = xxxx
  after(() => {
    console.log(name + " 执行完成了") // changeCurrentDate 执行完成了
  })
  onError(() => {
    console.log(name + " 执行失败了") // changeCurrentDate 执行失败了
  })
})

执行$onAction 的时候会将这个回到函数存放到 actionSubscriptions 数组中,然后 triggerSubscriptions(actionSubscriptions, { args, name, store, after, onError, }) 这一步会触发这个回调函数,并执行 after 和 onError,这样 after 的回调就会 push 到 afterCallbackList 数组中在函数执行完成后执行

$patch

这是 pinia 提供的修改 state 数据的方法,其主要分为两种方式(跟 setState 很像)

  1. 对象类型入参

$patch({ currentDate : 1})

注意: 这边跟 React.setState 不一样的地方是 这个是和深层遍历合并的方式,所以对于子属性为对象类型的时候不会导致其失去响应式

// 对象类型修改state
// 深度遍历响应式合并
// 元数据 : state = { obj :{ a : 1 , b: 2 } }
// 修改  store.$patch({  obj:{ a : 11 , c : 3 } })
// 结果为: state = { obj :{ a : 11 , b: 2 , c : 3 } }
  1. 回调函数类型 store.$patch((state) => { state.a = 1 }) (推荐的方式)

源码

ts
/**
 * 提供修改store中state方法,两种类型
 * 1. 对象类型 传入的为 state   store.$patch({  a : 1 , b :2  })
 * 2. 函数类型 回调入参为 state   store.$patch((state) => { state.a = 1 })
 * @param partialStateOrMutator
 */
function $patch(partialStateOrMutator: _DeepPartial<UnwrapRef<S>> | ((state: UnwrapRef<S>) => void)): void {
  let subscriptionMutation: SubscriptionCallbackMutation<S>
  isListening = isSyncListening = false
  // reset the debugger events since patches are sync
  /* istanbul ignore else */
  if (__DEV__) {
    debuggerEvents = []
  }
  // 回调函数类型入参
  if (typeof partialStateOrMutator === "function") {
    // 执行回调函数 传入的为整个当前store的state
    partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
    subscriptionMutation = {
      type: MutationType.patchFunction,
      storeId: $id,
      events: debuggerEvents as DebuggerEvent[],
    }
  } else {
    // 对象类型修改state
    // 深度遍历响应式合并
    // 元数据 : state = { obj :{ a : 1 , b: 2 } }
    // 修改  store.$patch({  obj:{ a : 11 , c : 3 } })
    // 结果为: state = { obj :{ a : 11 , b: 2 , c : 3 } }
    mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
    subscriptionMutation = {
      type: MutationType.patchObject,
      payload: partialStateOrMutator,
      storeId: $id,
      events: debuggerEvents as DebuggerEvent[],
    }
  }
  const myListenerId = (activeListener = Symbol())
  nextTick().then(() => {
    if (activeListener === myListenerId) {
      isListening = true
    }
  })
  isSyncListening = true
  // because we paused the watcher, we need to manually call the subscriptions
  // 通知订阅者去更新
  triggerSubscriptions(subscriptions, subscriptionMutation, pinia.state.value[$id] as UnwrapRef<S>)
}

分析

  1. 因为pinia.state.value[$id]是响应式的,所以可以直接修改就可以了

  2. 订阅者更新钩子的触发

triggerSubscriptions(subscriptions, subscriptionMutation, pinia.state.value[$id] as UnwrapRef<S>) 这一步就会触发用户通过 store.$subscribe订阅的数据跟新的钩子函数了

$subscribe

订阅 state 数据跟新的回调方法

源码

ts
// 提供监听state数据修改的回调方法
// options.detached => 布尔值,默认是 false,
//   - 正常情况下,当订阅所在的组件被卸载时,订阅将被停止删除,
//   - 如果设置detached值为 true 时,即使所在组件被卸载,订阅依然在生效
function $subscribe(callback, options = {}) {
  // 将当前回调函数存放到 subscriptions 数组中,并返回移出监听的方法
  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
}

$onAction

订阅 action 执行的回调方法

源码

ts
$onAction: addSubscription.bind(null, actionSubscriptions),

具体可以看 wrapAction , 其核心逻辑也就在 warapAction 中 通过将订阅的回调函数存放到 afterCallbackListonErrorCallbackList 然后在对应的时机执行即可

上次更新: