pinia核心笔记

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实例)缓存对象。
并提供两个操作方法,

  1. setActivePinia 更新 activePinia
export const setActivePinia = (pinia: Pinia | undefined) =>
  (activePinia = pinia)
  1. getActivePinia 获取 activePinia
export const getActivePinia = () =>
  // 这里优先返回全局注册的pinia实例
  (getCurrentInstance() && inject(piniaSymbol)) || activePinia

subscriptions.ts

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

  1. addSubscription
// 向当前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
}

  1. triggerSubscriptions
// 执行事件队列
export function triggerSubscriptions<T extends _Method>(
  subscriptions: T[],
  ...args: Parameters<T>
) {
  subscriptions.slice().forEach((callback) => {
    callback(...args)
  })
}

createPinia.ts

创建pinia实例

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做了什么
// 通过配置类型判断配置类型 
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生成

// 这里会先将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函数做了什么

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实例

公共变量

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
主要步骤


// 如果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更新


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

patch用来更新state值, 并出发更新监听,subscribe绑定的事件将在state更新后被执行一次

$subscribe 更新监听

 $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生成时被调用

 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的前置监听队列

其他钩子

  1. plugins
// 生成store后将执行插件函数
pinia._p.forEach((extender) => {...}
  1. hydrate
// 执行plugins后执行合并函数
(options as DefineStoreOptions<Id, S, G, A>).hydrate!(
  store.$state,
  initialState
)

总结

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

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

疑问

  1. $subscribe 监听中 stopWatcher 变量先于定义
 const removeSubscription = addSubscription(
   ....
  () => stopWatcher()
)
const stopWatcher = scope.run(() =>{...})

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342