likes
comments
collection
share

不得不说,这个状态管理库是真🍍!—Pinia源码透析

作者站长头像
站长
· 阅读数 49

如果你正在开发大型Vue 3 项目,那么Pinia将是你最好的拍档。它能够轻松地管理和组织应用程序的状态,让你的开发变得更加简单和高效。在这篇文章中,我们将介绍一个菠萝🍍 状态管理库——Pinia!相比于传统的状态管理库,Pinia 更加优秀,更加强大。它基于 Vue 3 和 Composition API 构建,采用响应式的数据结构,并具备插件机制、丰富的 API 和 TypeScript 支持等诸多优点。

在接下来的篇章中,我们将深入剖析 Pinia 的核心原理,探讨如何使用 Pinia 优化应用程序性能,并分享一些 Pinia 的最佳实践经验。废话不多说,让我们开始吧!

(PS:以下文章仅对 Vue3.* 版本做说明,不会过多地说明在 Vue 2.* 版本中的兼容实现)

createPinia

不得不说,这个状态管理库是真🍍!—Pinia源码透析

createPinia 的作用是创建 Pinia 实例(如上图),它是 Pinia 的核心构造函数。它接收一个可选的初始状态,并返回一个 Pinia 实例。

Pinia 实例是一个包含以下属性的对象:

  • state: ref 响应式数据,用于存储所有的 store 实例状态
  • _a: vue实例,用于挂载 Pinia 插件
  • _e: effectScope 对象,用于管理所有的副作用
  • _s: Map 对象,用于存储所有的 store 实例
  • _p: 用于存储所有待安装的 Pinia 插件

createPinia 使用 effectScope 创建一个新的作用域,并在该作用域中初始化状态。它还创建一个包含 install 和 use 方法的对象,并将该对象标记为一个原始值(不可响应式的对象)。该对象包含 install 方法,用于将 Pinia 实例安装到一个 Vue 应用程序中,并包含 use 方法,用于安装 Pinia 插件。

Pinia 实例通过 provide 方法提供给应用程序,从而可以在任何组件中使用该实例。Pinia 实例还将自身挂载到应用程序的全局属性 $pinia 上,以便在应用程序中的任何位置都可以访问该实例。 toBeInstalled 数组用于存储待安装的 Pinia 插件,以便在应用程序启动时安装这些插件。_p 数组用于存储已安装的插件,以便可以在需要时访问这些插件。 以下是相关源码的简单版实现,为了更好的理解,内部变量名称会与源码大致一致。

packages\pinia\core\createPinia.js

// 引入必要的 Vue 3 模块
import { effectScope, markRaw, ref } from 'vue'
// 引入 Pinia 实例标识
import { piniaSymbol } from './rootStore'
// 引入 assign 工具函数
import { assign } from './utils'

// 创建 Pinia 实例的函数
export function createPinia() {
  // 创建作用域实例,该实例用于管理 Pinia 实例的副作用
  const scope = effectScope(true)
  // 使用作用域实例创建响应式状态对象
  const state = scope.run(() => ref({}))
  // 创建用于存储插件的数组
  const _p = []
  // 创建用于存储未安装插件的数组
  let toBeInstalled = []

  // 创建 Pinia 实例对象
  const pinia = markRaw({
    // Pinia 实例的安装方法
    install(app) {
      console.log('pinia installed', app, pinia)
      // 将 Vue 实例赋值给 Pinia 实例的私有属性
      pinia._a = app
      // 在 Vue 实例中注入 Pinia 实例
      app.provide(piniaSymbol, pinia)
      // 在 Vue 实例内部的全局属性中添加 Pinia 实例
      app.config.globalProperties.$pinia = pinia

      // 如果有插件需要安装,将插件添加到插件数组中
      toBeInstalled.forEach(plug => _p.push(plug))
      // 清空待安装插件数组
      toBeInstalled = []
    },
    // Pinia 实例的插件安装方法
    use(plugin) {
      // 将插件添加到待安装插件数组中
      toBeInstalled.push(plugin)
      return this
    }
  })
  
  console.trace('[createPinia]', pinia)
  // 将创建的 Pinia 实例对象合并到 state 对象中,并返回合并后的对象
  return assign(pinia, {
    state,
    _a: null,
    _e: scope,
    _s: new Map,
    _p
  })
}

definedStore

defineStore 的作用就是定义独立的 store ,在 Pinia 中,每个 store 对象都是一个独立的实例,由它自身定义的 state、getter、action 组成,而 definedStore 就是一个用于创建 store 的工厂方法,它会根据传入的参数来动态创建 store 实例,并返回一个可供外部调用的 useStore 函数。

defineStore简单的流程如下:

  • 根据形参的类型进行不同的默认值初始化
  • 定义工厂方法 useStore 用于创建 store,假如已创建过则会直接从 pinia 实例中取出,避免重复的创建
  • 内部会判断 setup 参数是否为函数,如果是则走 createSetupStore 进行 store 的初始化和创建,防止则走 createOptionsStore (createOptionsStore 内部最终也会走 createSetupStore 流程,中间会替我们包装一个 setup 函数,下文具体讨论)

definedStore 简单的理解就是以高阶函数的方式来惰性创建 store ,下面是简单的一个源码实现流程。

src\libs\pinia\core\store.js

import { getCurrentInstance, inject } from "vue"
import { activePinia, piniaSymbol, setActivePinia } from "./rootStore"
import createSetupStore from "./craeteSetupStore"
import createOptionsStore from "./createOptionsStore"
import { isFun } from "./utils"

/**
 * 定义状态仓库 
 * @param {string | object} idOrOptions store 标识符
 * @param {object | function} setup store创建器
 * @param {object} setupOptions store配置
 */
export function defineStore(idOrOptions, setup, setupOptions) {
  let id, options

  // 判断 setup 是否是一个函数
  const isSetupStore = isFun(setup)

  // 入参兼容适配
  if (typeof idOrOptions == 'string') {
    id = idOrOptions
    // 如果存在创建器函数则取 setupOptions 作为配置
    options = isSetupStore ? setupOptions : setup
  } else {
    options = setupOptions
    id = idOrOptions.id
  }

  /**
   * useStore 函数用于在组件中使用状态仓库
   * @param {object} pinia Pinia 实例
   * @returns {object} 状态仓库
   */
  function useStore(pinia) {
    // 获取当前组件实例
    const currentInstance = getCurrentInstance()

    // 如果当前组件实例存在,则从该实例中注入 pinia 实例
    if (currentInstance) pinia = inject(piniaSymbol)

    // 设置正在应用的实例
    if (pinia) setActivePinia(pinia)

    // 获取正在使用的 pinia 实例
    pinia = activePinia

    // 判断当前状态库是否定义过,走缓存策略
    if (!pinia._s.has(id)) {
      // 创建状态库
      // 两种策略,如果 setup 传入类型是函数则走 createSetupStore,否则走 createOptionsStore
      if (isSetupStore) {
        createSetupStore(id, setup, options, pinia)
      } else {
        createOptionsStore(id, options, pinia)
      }
    }

    // 采用单例模式,如果之前创建过则从 Map 取出来再次返回即可
    const store = pinia._s.get(id)

    return store
  }

  return useStore
}

为了便于源码的理解,我将 createOptionsStore和 createSetupStore 单独提取出来了,下面分开来讨论下这俩玩意的实现吧~

createOptionsStore

import { computed, markRaw, toRefs } from "vue"
import { assign, isFun } from "./utils"
import { setActivePinia } from "./rootStore"
import craeteSetupStore from "./craeteSetupStore"

export default function createOptionsStore($id, options, pinia) {
  const { state, actions, getters } = options

  // 获取初始化状态
  const initState = pinia.state.value[$id]

  // 包装一个 setup 函数
  const setup = () => {
    // 初始化状态值
    if (!initState) {
      pinia.state.value[$id] = isFun(state) ? state() : {}
    }

    const localStore = toRefs(pinia.state.value[$id])
    // debugger
    // 处理 getters
    const computedGetters = Object.entries(getters || {}).reduce((memoGetters, [name, getter]) => {
      memoGetters[name] = markRaw(computed(() => {
        setActivePinia(pinia)
        // 获取 状态仓库
        const store = pinia._s.get($id)
        return getter.call(store, store)
      }))
      return memoGetters
    }, {})
    // 将 state,actions 和 getters 拍平
    return assign(localStore, actions, computedGetters)
  }

  return craeteSetupStore($id, setup, options, pinia, true)
}

createSetupStore

import { effectScope, markRaw, nextTick, reactive, watch, isReactive, toRaw, isRef } from "vue"
import { addSubscription, triggerSubscriptions } from "./subscriptions"
import { assign, isComputed, isFun, mergeRectiveObjects } from "./utils"
import { setActivePinia } from "./rootStore"

export default function craeteSetupStore($id, setup, options, pinia, isOptionsStore) {
  let scope
  const optionForPlugin = {
    actions: {},
    ...options
  }
  // 设置订阅相关的配置
  const $subscribeOptions = {
    deep: true
  }

  // 定义内部的一些状态
  let isListening = false
  let isSyncListening = false
  let subscribes = markRaw([])
  // 获取状态
  const initState = pinia.state.value[$id]
  const actionSubscriptions = []

  // 初始化状态
  // 判断是否是 createOptionsStore 进来的,并且已经初始化状态了则不需要再次初始化
  if (!isOptionsStore && !initState) {
    pinia.value[$id] = {}
  }

  let activeListener

  // 批量更新状态
  function $patch(partialStateOrMutator) {
    let subscribeMutation
    isListening = isSyncListening = false

    if (isFun(partialStateOrMutator)) {
      // 将当前状态传入自定义的函数中
      partialStateOrMutator(pinia.state.value[$id])
    } else {
      // 深度合并对象
      mergeRectiveObjects(pinia.state.value[$id], partialStateOrMutator)
    }

    const MyListenerId = activeListener = Symbol()
    nextTick().then(() => {

      if (MyListenerId === activeListener) {
        isListening = true
      }
    })
    isSyncListening = true

    // 触发订阅函数
    triggerSubscriptions(subscribes, subscribeMutation, pinia.state.value[$id])
  }

  // 重置状态
  function $reset() {
    const { state } = options
    const newState = state ? state() : {}
    this.$patch($state => assign($state, newState))
  }

  // 订阅 action
  function $onAction(...args) {
    return addSubscription(actionSubscriptions, ...args)
  }

  // 注销 store
  function $dispose() {
    scope.stop()
    subscribes = []
    actionSubscriptions = []
    pinia._s.deleta($id)
  }

  // 将每一个 action 动作封装为一个订阅函数
  function wrapAction(name, action) {
    return function (...args) {
      setActivePinia(pinia)
      const afterCallbackList = []
      const onErrorCallbackList = []
      // 注册切片函数
      function after(fn) {
        afterCallbackList.push(fn)
      }

      function onError(fn) {
        onErrorCallbackList.push(fn)
      }

      // 触发订阅函数
      triggerSubscriptions(actionSubscriptions, {
        args,
        name,
        store,
        after,
        onError
      })

      let ret
      try {
        ret = action.apply(this && this.id === $id ? this : store, args)
      } catch (err) {
        triggerSubscriptions(onErrorCallbackList, err)
      }

      if (ret instanceof Promise) {
        ret.then(value => {
          triggerSubscriptions(afterCallbackList, value)
          return value
        }).catch(err => {
          triggerSubscriptions(onErrorCallbackList, err)
          return Promise.reject(err)
        })
      }

      triggerSubscriptions(afterCallbackList, ret)

      return ret
    }
  }

  function $subscript(cb) {
    const removeSubscript = addSubscript(
      subscribes,
      cb,
      options.detached,
      () => stopWatcher()
    )
    
    const stopWatcher = scope.run(() => {
      watch(() => pinia.state.value[$id], state => {
        if (options.flush ? isSyncListening : isListening) {
          cb({
            storeId: $id,
            type: MutationType.direct
          },
            state)
        }
      }, {
        ...$subscribeOptions,
        ...options
      })
    })

    return removeSubscript
  }

  const partialStore = {
    _p: pinia,
    $id,
    $patch,
    $reset,
    $dispose,
    $subscript,
    $onAction
  }
  // 将响应式状态储存起来,后续复用避免重复创建
  const store = reactive(partialStore)
  pinia._s.set($id, store)

  const setupStore = pinia._e.run(() => {
    scope = effectScope()
    return scope.run(() => setup())
  })
  // 扁平化 state,getters,actions
  for (const key in setupStore) {
    const prop = setupStore[key]
    // 判断是否是响应式数据
    if (isRef(prop) && isComputed(prop) || isReactive(prop)) {
      // createOptionsStore 中的数据已经经过响应式处理了,这里可以跳过
      if (!isOptionsStore) {
        if (initState && isRef(prop)) {
          prop.value = initState[key]
        } else {
          mergeRectiveObjects(prop, initState[key])
        }
        // 更新 pinia 状态
        pinia.state.value[$id][key] = prop
      }
    } else if (isFun(prop)) {
      // 进入到这里的都是 action 动作函数
      // 对 action 进行包装
      const activeValue = wrapAction(key, prop)
      setupStore[key] = activeValue
    }
  }

  // 合并仓库
  assign(store, setupStore)
  assign(toRaw(store), setupStore)

  // 对 store 进行代理操作
  Object.defineProperty(store, '$state', {
    get: () => pinia.state.value[$id],
    set(state) {
      // 合并新旧状态
      $patch($state => assign($state, state))
    }
  })

  // 执行 pinia 插件
  pinia._p.forEach(extender => {
    // 获取插件返回的仓库变更结果
    const extendStore = scope.run(() => extender({
      app: pinia._a,
      options: optionForPlugin,
      store,
      pinia
    }))
    // 进行合并操作
    assign(store, extendStore)
  })

  isListening = true
  isSyncListening = true

  return store
}

mapHelpers

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: {
    double: (state) => state.count * 2,
  },
  actions: {
    increment() {
      this.count++
    },
  },
})

export const useCartStore= defineStore('cart', {
  state: () => ({})
})

我们就拿上面定义的 store 来作为例子讨论以下辅助工具的使用和实现吧~

mapStores

Example

pinia.vuejs.org/zh/api/modu…

export default {
  computed: {
    // 其他计算属性
    ...mapStores(useUserStore, useCartStore)
  },

  created() {
    this.userStore // id 为 "user" 的 store
    this.cartStore // id 为 "cart" 的 store
  }
}

源码实现

export const mapStores = function (...stores) {
  // 参数兼容处理
  stores = Array.isArray(stores[0]) ? stores[0] : stores
  const storeMap = stores.reduce(function (memoStoreMap, useStore) {
    // PS: 在 createPinia 中已经注入 *.$pinia 的全局属性了
    const store = useStore(this.$pinia)
    // 会基于当前的 store 标识拼接上 mapStoreSuffix(默认 Store)
    // 可以使用 setMapStoreSuffix 方法来调整后缀名称
    memoStoreMap[`${store.$id}${mapStoreSuffix}`] = store
    return memoStoreMap
  }, {})
  return storeMap
}

mapState

Example

pinia.vuejs.org/zh/api/modu…

export default {
  computed: {
    // 其他计算属性
    // useCounterStore 拥有一个名为 `count` 的 state 属性和一个名为 `double` 的 getter
    ...mapState(useCounterStore, {
      n: 'count',
      triple: store => store.n * 3,
      // 如果想使用 `this`,就不能使用箭头函数
      custom(store) {
        return this.someComponentValue + store.n
      },
      doubleN: 'double'
    })
  },

  created() {
    this.n // 2
    this.doubleN // 4
  }
}

源码实现

export const mapState = function (useStore, keyMapper) {
  if (Array.isArray(keyMapper)) {
    return keyMapper.reduce((reduce, name) => {
      reduce[name] = function () {
        const store = useStore(this.$pinia)
        return store[name]
      }
      return reduce
    }, {})
  } else {
    /**
     * keyMapper ==>
     * {
     *  renameA: 'a',
     *  b: (store) => store.b
     * }
     */
    return Object.entries(keyMapper || {}).reduce((reduce, [name, key]) => {
      reduce[name] = function () {
        const store = useStore(this.$pinia)
        if (isFun(key)) return key.call(this, store)
        if (isStr) return store[key]
      }
      return reduce
    }, {})
  }
}

mapGetters

Example

pinia.vuejs.org/zh/api/modu…

export default {
  computed: {
    // 其他计算属性
    // useCounterStore 有一个名为 `count` 的 state 属性以及一个名为 `double` 的 getter
    ...mapState(useCounterStore, {
      n: 'count',
      triple: store => store.n * 3,
      // 注意如果你想要使用 `this`,那你不能使用箭头函数
      custom(store) {
        return this.someComponentValue + store.n
      },
      doubleN: 'double'
    })
  },

  created() {
    this.n // 2
    this.doubleN // 4
  }
}

源码实现与 mapState 一致

mapActions

Example

pinia.vuejs.org/zh/api/modu…

export default {
  methods: {
    // 其他方法属性
    // useCounterStore 有两个 action,分别是 `increment` 与 `setCount`。
    ...mapActions(useCounterStore, { moar: 'increment', setIt: 'setCount' })
  },

  created() {
    this.moar()
    this.setIt(2)
  }
}

源码实现

export const mapActions = function (useStore, keyMapper) {
  const isArray = Array.isArray(keyMapper)

  if (isArray) {
    return keyMapper.reduce((reduce, name) => {
      reduce[name] = function (...args) {
        const store = useStore(this.$pinia)
        return store[name].apply(store, args)
      }
      return reduce
    }, {})
  } else {
    return Object.entries(keyMapper).reduce((reduce, [rename, name]) => {
      reduce[rename] = function (...args) {
        const store = useStore(this.$pinia)
        return store[name].apply(store, args)
      }
      return reduce
    }, {})
  }
}

mapWritableState

Example(可读可写的高阶版本 mapState)

<script>
import {mapWritableState} from '@/libs/pinia'
import { useCounterStore } from '@/stores/counter'

export default {
  computed: {
    // 其他计算属性
    // useCounterStore 拥有一个名为 `count` 的 state 属性和一个名为 `double` 的 getter
    ...mapWritableState(useCounterStore, {
      n: 'count',
      triple: store => {
        return store.count * 3
      }
    })
  },
  methods: {
    handleChangeCount () {
      this.n++
    }
  }
}
</script>
<template>
  <!-- 直接从 store 中访问 state -->
  <div>n: {{ n }}</div>
  <div>triple: {{triple}}</div>
  <button @click="handleChangeCount">add n</button>
</template>
源码实现
export const mapWritableState = function (useStore, keyMapper) {
  const isArray = Array.isArray(keyMapper)
  if (isArray) {
    return keyMapper.reduce((reduce, name) => {
      reduce[name] = {
        get() {
          return useStore(this.$pinia)[name]
        },
        set(val) {
          return useStore(this.$pinia)[name] = val
        }
      }
      return reduce
    }, {})
  } else {
    return Object.entries(keyMapper).reduce((reduce, [rename, name]) => {
      reduce[rename] = {
        get() {
          // 兼容函数的获取
          let store = useStore(this.$pinia)
          if (isFun(name)) return name(store)
          return useStore(store)[name]
        },
        set(val) {
          return useStore(this.$pinia)[name] = val
        }
      }
      return reduce
    }, {})
  }
}

其他

storeToRefs

创建一个引用对象,包含 store 的所有 state、 getter 和 plugin 添加的 state 属性。

Example

<script setup>
import {storeToRefs} from '@/libs/pinia'
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
const {count} = storeToRefs(counter)
setInterval(() => {
  counter.count++
}, 3000)
</script>
<template>
  <div>Current Count: {{ count }}</div>
</template>
源码实现
// 创建一个引用对象,包含 store 的所有 state、 getter 和 plugin 添加的 state 属性。
// https://pinia.vuejs.org/zh/api/modules/pinia.html#storetorefs
import { isReactive, isRef, toRaw, toRef } from "vue";

export function storeToRefs(store) {
  store = toRaw(store)
  return Object.keys(store).reduce((refs, key) => {
    const val = store[key]
    if (isRef(val) || isReactive(val)) {
      refs[key] = toRef(store, key)
    }
    return refs
  }, {})
}

subscriptions

基于发布订阅的模式添加和触发回调,应用于 onAction,onAction ,onActionsubscript及状态变更后的监听回调派发(插件应用)。

import { getCurrentScope, onScopeDispose } from "vue"
/**
 * 添加订阅函数
 * @param {array} subscriptions 指定订阅队列
 * @param {function} callback 订阅函数
 * @param {boolean} detached 组件上下文分离(组件卸载不随着注销)
 * @param {function} onCleaned 
 * @returns 
 */
export function addSubscription(subscriptions, callback, detached, onCleaned) {
  subscriptions.push(callback)
  // 封装注销函数
  const removeSubscription = () => {
    const pos = subscriptions.indexOf(callback)
    if (pos != -1) {
      subscriptions.splice(pos, 1)
      onCleaned?.()
    }
  }

  if (!detached && getCurrentScope()) {
    onScopeDispose(removeSubscription)
  }

  return removeSubscription
}

// 触发订阅函数
export function triggerSubscriptions(subscriptions, ...args) {
  [...subscriptions].forEach(callback => callback(...args))
}

总结

通过以上的demo演示及源码学习中,我们可以发现到 Pinia 的一些优点,主要如下:

  1. 简单易用:提供了一个简单、直观和易于使用的 API,可以轻松地创建和管理 Store。
  2. 响应式状态管理:依赖 Vue 的响应式状态,这意味着当状态发生变化时,相关的组件会自动重新渲染。
  3. 插件化:提供了插件化的架构,可以很容易地扩展其功能,并与其他库进行集成。
  4. 性能优化:针对性能进行了优化,采用了一些优化策略,例如惰性加载(useStore)、批处理($patch)等。
  5. TypeScript 支持:Pinia 具有对 TypeScript 的原生支持,可以提供更好的类型安全和开发体验。(源码中使用的是 TypeScript 编写)

最后

非常感谢您抽出时间阅读本文!我希望这篇文章对您对学习 Vue.js 的过程有所帮助。如果您对本文有任何疑问或建议,欢迎在评论区留言和点赞哟~ 同时,我也希望您能够分享本文,让更多的人了解 Pinia,了解 Vue.js 的生态系统。感谢您的支持!

以下是本篇文章所实现的简易版 Pinia 源码仓库地址: github.com/CodeRookie2…

转载自:https://juejin.cn/post/7223315942566314041
评论
请登录