不得不说,这个状态管理库是真🍍!—Pinia源码透析
如果你正在开发大型Vue 3 项目,那么Pinia将是你最好的拍档。它能够轻松地管理和组织应用程序的状态,让你的开发变得更加简单和高效。在这篇文章中,我们将介绍一个菠萝🍍 状态管理库——Pinia!相比于传统的状态管理库,Pinia 更加优秀,更加强大。它基于 Vue 3 和 Composition API 构建,采用响应式的数据结构,并具备插件机制、丰富的 API 和 TypeScript 支持等诸多优点。
在接下来的篇章中,我们将深入剖析 Pinia 的核心原理,探讨如何使用 Pinia 优化应用程序性能,并分享一些 Pinia 的最佳实践经验。废话不多说,让我们开始吧!
(PS:以下文章仅对 Vue3.* 版本做说明,不会过多地说明在 Vue 2.* 版本中的兼容实现)
createPinia
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
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
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
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
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 ,onAction,subscript及状态变更后的监听回调派发(插件应用)。
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 的一些优点,主要如下:
- 简单易用:提供了一个简单、直观和易于使用的 API,可以轻松地创建和管理 Store。
- 响应式状态管理:依赖 Vue 的响应式状态,这意味着当状态发生变化时,相关的组件会自动重新渲染。
- 插件化:提供了插件化的架构,可以很容易地扩展其功能,并与其他库进行集成。
- 性能优化:针对性能进行了优化,采用了一些优化策略,例如惰性加载(useStore)、批处理($patch)等。
- TypeScript 支持:Pinia 具有对 TypeScript 的原生支持,可以提供更好的类型安全和开发体验。(源码中使用的是 TypeScript 编写)
最后
非常感谢您抽出时间阅读本文!我希望这篇文章对您对学习 Vue.js 的过程有所帮助。如果您对本文有任何疑问或建议,欢迎在评论区留言和点赞哟~ 同时,我也希望您能够分享本文,让更多的人了解 Pinia,了解 Vue.js 的生态系统。感谢您的支持!
以下是本篇文章所实现的简易版 Pinia 源码仓库地址: github.com/CodeRookie2…
转载自:https://juejin.cn/post/7223315942566314041