pinia核心笔记

2022-05-13 18:49:06 浏览数 (1)

pinia 核心源码

记录pinia核心源码阅读笔记,这里跳过hmr(热更新), mapHelpers(class 工具)等工具源码。 剔除的部分vue2.0兼容代码。 当前pinia版本2.0.13

执行流程概述

  1. 创建pinia实例,挂载到vue
  2. 定义state
  3. 创建组件
  4. 调用useState
  5. 生成并缓存pinia
  6. 注销组件
  7. 注销监听

pinia.png

rootStore.js

这里主要提供 activePinia(当前可用pinia实例)缓存对象。 并提供两个操作方法,

setActivePinia 更新 activePinia

代码语言:javascript复制
export const setActivePinia = (pinia: Pinia | undefined) =>
  (activePinia = pinia)

getActivePinia 获取 activePinia

代码语言:javascript复制
export const getActivePinia = () =>
  // 这里优先返回全局注册的pinia实例
  (getCurrentInstance() && inject(piniaSymbol)) || activePinia

subscriptions.ts

响应事件相关, 提供两个方法

addSubscription

代码语言:javascript复制
// 向当前state事件队列中注册事件回调
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
}

triggerSubscriptions

代码语言:javascript复制
// 执行事件队列
export function triggerSubscriptions<T extends _Method>(
  subscriptions: T[],
  ...args: Parameters<T>
) {
  subscriptions.slice().forEach((callback) => {
    callback(...args)
  })
}

createPinia.ts

创建pinia实例

代码语言:javascript复制
export function createPinia(): Pinia {
  
  // 创建响应式空间,空值pinia相关的响应对象的有效性
  const scope = effectScope(true)
  
  // state缓存空间, 生成的store将缓存到该队列中
  // 当使用useState是,将通过注册的id,从stateTrue
  // 中查询对应的store,保证不同组件使用相同的store
  const state = scope.run<Ref<Record<string, StateTree>>>(() =>
    ref<Record<string, StateTree>>({})
  )!

  // 插件队列
  let _p: Pinia['_p'] = []
  let toBeInstalled: PiniaPlugin[] = []

  // 创建pinia实例
  // markRaw 保证pinia不会被代理
  const pinia: Pinia = markRaw({

    // 将pinia实例注册到Vue实例中
    install(app: App) {

      // 激活当前pinia实例, 
      setActivePinia(pinia)
      if (!isVue2) {

        // 设置vue实例
        pinia._a = app
        // 通过依赖注入设置全局默认pinia实例
        // 后面useState会用到
        app.provide(piniaSymbol, pinia)
        // 挂载全局pinia实例
        app.config.globalProperties.$pinia = pinia

        if (__DEV__ && IS_CLIENT) {
          registerPiniaDevtools(app, pinia)
        }
        // 添加插件
        toBeInstalled.forEach((plugin) => _p.push(plugin))
        toBeInstalled = []
      }
      
    },

    // 注册插件
    use(plugin) {
      if (!this._a && !isVue2) {
        toBeInstalled.push(plugin)
      } else {
        _p.push(plugin)
      }
      return this
    },

    // 插件集合
    _p,
    // 应用实例
    _a: null,
    // 响应空间
    _e: scope,
    // store 队列
    _s: new Map<string, StoreGeneric>(),
    // state配置队列, 用于重置state
    state,
  })

  return pinia
}

这里主要创建pinia实例,如果pinia实例被注册要vue应用实例时,将执行一些初始值设置,依赖注册pinia实例,以供useState使用

store.ts

pinia状态, 主要包括三个核心

  1. defineStore 定义状态
  2. createOptionsStore 对象型状态生成函数 defineStore(id, {state, getter, action})
  3. createSetupStore 函数型状态生成函数 defineStore(id, () => { setup(){} })

defineStore 定义store

defineStore 只做了两件事

  1. 参数处理
  2. 构建useState函数 这里主要看useState做了什么
代码语言:javascript复制
// 通过配置类型判断配置类型 
const isSetupStore = typeof setup === 'function'
...

// useState 可接收一个pinia实例作为参数
// 如果设置参数pinia,将通过依赖注入获取全局默认pinia实例
pinia =
   (__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
   (currentInstance && inject(piniaSymbol))

// 激活当前pinia实例
if (pinia) setActivePinia(pinia)

// 通过 id查询对应的store是否已经创建
if (!pinia._s.has(id)) {
      
      // 如果未在当前pinia上查到对应store, 将根据参数类型创建store
      if (isSetupStore) {
        // 函数型
        createSetupStore(id, setup, options, pinia)
      } else {
        // 对象型
        createOptionsStore(id, options as any, pinia)
      }
     ...
    }

// 如果store存在返回该实例
const store: StoreGeneric = pinia._s.get(id)!
...
return store as any


// 其他
// 在创建useStore函数后
// 将当前id挂载useStore.$id属性上
useStore.$id = id

createOptionsStore 对象型store生成

这个函数其实是createSetupStore的包装函数, 将对象型的定义转为函数型 再交由createOptionsStore生成store

store生成
代码语言:javascript复制
// 这里会先将options转为setup函数
// 通过createSetupStore生成store实例
store = createSetupStore(id, setup, options, pinia, hot)

// 绑定重置函数
store.$reset = function $reset() {
 // state 这里是闭包
 const newState = state ? state() : {}
 // this指向store
 // this.$patch 是state更新函数, 
 this.$patch(($state) => {
   // 将原state与现有state合并,将state部分属性值重置
   assign($state, newState)
 })
}
setup函数

这里看setup函数做了什么

代码语言:javascript复制
function setup() {
  ...

// 初始将state缓存到当前pinia.state中
pinia.state.value[id] = state ? state() : {}

// 将state转未ref
const localState = toRefs(pinia.state.value[id])

// 返回响应对象
  return assign(
   localState, // state => Refs(state)
   actions, //   actions => actions
    // 遍历getters, 将属性包裹一层computed
   Object.keys(getters || {}).reduce((computedGetters, name) => {
     // markRow 防止对象被重复代理
     computedGetters[name] = markRaw(
       computed(() => {

         // pinia 处于闭包
         setActivePinia(pinia)
         // it was created just before
         const store = pinia._s.get(id)!

         // 将执行函数绑定在store上下文中,支持 {getters: { fn(){ this.count   } }} 模式
         // 所以当使用箭头函数时不能使用this获取state
         // 函数接收state作为参数, 支持{gtters: { f(state){state.count   } }}
         // 返回getter执行结果
         return getters![name].call(store, store)
       })
     )
     return computedGetters}
    {}
  )
}

所以setup主要作用是 1.将getter包裹computed, 2.返回新的store定义,通过getter的包装过程,知道了为什么箭头函数不能使用this模式,主要应为箭头函数的this原定义上下文绑定,后期无法通过call函数绑定到state上。

createSetupStore 函数型store生成

生成并挂载store实例

公共变量
代码语言:javascript复制
let isListening: boolean // 监听函数执行时机标识
let isSyncListening: boolean // 监听函数执行时机标识
// state 更新响应队列,缓存¥subscribe挂载的任务
let subscriptions: SubscriptionCallback<S>[] = markRaw([])
// actions 响应事件队列, 缓存$onAction挂载的任务
let actionSubscriptions: StoreOnActionListener<Id, S, G, A>[] = markRaw([])
// debugger 事件队列
let debuggerEvents: DebuggerEvent[] | DebuggerEvent
// 初始缓存state
const initialState = pinia.state.value[$id] as UnwrapRef<S> | undefined
store 实例

因为createSetupStore的主要功能就是生成store实例,所以这里先看生成的store 主要步骤

代码语言:javascript复制
// 如果state不存在,设置默认值
 if (!buildState && !initialState && (!__DEV__ || !hot)) {
   pinia.state.value[$id] = {}
 }

// store基础方法属性
// 这里主要定义store实力的操作API
const partialStore = {
  _p: pinia,
 // action 响应事件注册函数
 $onAction: addSubscription.bind(null, actionSubscriptions),
 // state 更新函数
 $patch,
 // 重置store
 $reset,
// 注册响应修改监听
 $subscribe(callback, options = {}) {...},
 // 注销store
  $dispose,
}

// 转为响应对象
const store: Store<Id, S, G, A> = reactive(
  assign({}, partialStore)
)

// 缓存store, useState通过当前激活的pinia获取到store
pinia._s.set($id, store)

// 合并store
// setupStore为setup()执行处理后配置对象
// 主要是对action的包装以及部分属性的合并
assign(store, setupStore)
// 这里为了 storeToRefs, 将响应属性合并到store原对象上
// storeToRefs 将先取得toRaw(store)再说Refs处理
assign(toRaw(store), setupStore)

// 绑定$state属性
Object.defineProperty(store, '$state', {
 get: () => pinia.state.value[$id],
 set: (state) => {
   $patch(($state) => {
     assign($state, state)
   })
 },
})

这里剔除的具体的方法定义,和周期函数的调用,主要看store的基础生成。

$patch state更新
代码语言:javascript复制
 function $patch(
    partialStateOrMutator:
      | _DeepPartial<UnwrapRef<S>>
      | ((state: UnwrapRef<S>) => void)
  ): void {
    
    let subscriptionMutation: SubscriptionCallbackMutation<S>
    
    // 阻止$subscribe监听事件执行
    // 防止重复触发
    // 保证$subscribe在完整合并后再执行
    isListening = isSyncListening = false
    
    if (__DEV__) {
      debuggerEvents = []
    }

    // 如果状态修改器为函数,执行并生成修改类型
    if (typeof partialStateOrMutator === 'function') {
      // 例如 $reset()
      partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)

      // 函数更新类型
      subscriptionMutation = {
        type: MutationType.patchFunction,
        storeId: $id,
        events: debuggerEvents as DebuggerEvent[],
      }
    } else {
      // 如果状态修改器为对象, 合并到新state中
      // mergeReactiveObjects将递归合并对象内的属性
      mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
      
      // 对象更新类型
      subscriptionMutation = {
        type: MutationType.patchObject,
        payload: partialStateOrMutator,
        storeId: $id,
        events: debuggerEvents as DebuggerEvent[],
      }
    }

    // 开启监听锁
    nextTick().then(() => {
      isListening = true
    })
    isSyncListening = true

    // 应为之前关闭了watch监听, 所以这里需要手动执行一次监听队列
    triggerSubscriptions(
      subscriptions,
      subscriptionMutation,
      pinia.state.value[$id] as UnwrapRef<S>
    )
  }

subscribe绑定的事件将在state更新后被执行一次

$subscribe 更新监听
代码语言:javascript复制
 $subscribe(callback, options = {}) {

   // 向任务队列中添加任务, 并返回移除函数
   const removeSubscription = addSubscription(
     subscriptions,
     callback,
     options.detached,
     // 这里有个问题 stopWatcher 先于定义,const应该存在假死区 
     () => stopWatcher()
   )
   // 挂载更新监听 
   const stopWatcher = scope.run(() =>
     watch(
       () => pinia.state.value[$id] as UnwrapRef<S>,
       (state) => {
         // 更新锁, patch时禁用更新监听
         if (options.flush === 'sync' ? isSyncListening : isListening) {
           callback(
             {
               storeId: $id,
               type: MutationType.direct,
               events: debuggerEvents as DebuggerEvent,
             },
             state
           )
         }
       },
       assign({}, $subscribeOptions, options)
     )
   )!

   return removeSubscription
 }
wrapAction

action 包装函数,主要为了提供 $onAction 监听钩子, 该函数在setupStore生成时被调用

代码语言:javascript复制
 function wrapAction(name: string, action: _Method) {
    return function (this: any) {
      setActivePinia(pinia)
      const args = Array.from(arguments)

      // action执行后回调队列
      const afterCallbackList: Array<(resolvedReturn: any) => any> = []
      // 错误回调队列
      const onErrorCallbackList: Array<(error: unknown) => unknown> = []
      
      // action执行后回调添加函数
      function after(callback: _ArrayType<typeof afterCallbackList>) {
        afterCallbackList.push(callback)
      }
      // 错误回调添加函数
      function onError(callback: _ArrayType<typeof onErrorCallbackList>) {
        onErrorCallbackList.push(callback)
      }
      
      // 执行action任务队列
      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
      }
      
      // 异步函数处理
      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
    }
  }

执行流程 $onAction监听队列 -> action -> after任务队列 or error任务队列 应为onAction本身可以看作 beforeCallbackList, action的前置监听队列

其他钩子

plugins

代码语言:javascript复制
// 生成store后将执行插件函数
pinia._p.forEach((extender) => {...}

hydrate

代码语言:javascript复制
// 执行plugins后执行合并函数
(options as DefineStoreOptions<Id, S, G, A>).hydrate!(
  store.$state,
  initialState
)

总结

pinia核心代码并不多,主要功能放在了store生成,钩子包装。 值得注意的是:

  1. pinia实例的调用
  2. scope 空值响应作用空间
  3. 钩子的调度
  4. 兼容支持

疑问

$subscribe 监听中 stopWatcher 变量先于定义

代码语言:javascript复制
 const removeSubscription = addSubscription(
   ....
  () => stopWatcher()
)
const stopWatcher = scope.run(() =>{...})

部分属性遍历上是否可以用其他的方法

代码语言:javascript复制
// 使用了 for in 遍历,将获取到原型上方法
for (const key in patchToApply) {
 if (!patchToApply.hasOwnProperty(key)) continue
 const subPatch = patchToApply[key]
 const targetValue = target[key]

0 人点赞