新一代状态管理器-Pinia

619 阅读4分钟

前言

Pinia是新一代的状态管理器,由Vue团队中核心成员所开发的,同时也被认为是下一代的Vuex,也就是Vuex5.x

其特点如下:

  • 完全支持typescript
  • 足够轻量,压缩后体积只有1.6kb
  • 模块化的设计,在打包时引入的每一个store都可以自动拆分
  • 没有模块嵌套,只有store概念,store之间可以自由使用,实现更好的代码分隔
  • actions支持同步和异步
  • 支持Vue Devtools

安装

yarn add pinia
// or
npm install pinia
// or
pnpm add pinia

使用

在vue3.x版本中直接通过createPinia创建Pinia实例即可,然后在main.ts中使用app.use(pinia)去加载pinia

import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const pinia = createPinia()
const app = createApp(App).use(pinia) 
app.mount('#app')

但如果想在vue2.x版本中使用的话,需要注册PiniaVuePlugin插件并且需要安装@vue/composition-api依赖包

https://github.com/vuejs/pinia
npm install pinia @vue/composition-api

// https://pinia.vuejs.org/getting-started.html#installation
import { createPinia, PiniaVuePlugin } from 'pinia'
Vue.use(PiniaVuePlugin)
const pinia = createPinia()

new Vue({
  el: '#app',
  // other options...
  pinia,
})

因为在pinia中只有store的概念,所以下面说一下它的基本用法

创建store

pinia提供了defineStore方法,并且支持两种方式可以直接创建store

import { defineStore } from 'pinia
const pinia = defineStore({id: '唯一标识', ...})
const pinia = defineStore('唯一标识', {...})

定义state

import { defineStore } from 'pinia'
export const userStore = defineStore({
  id: 'user',
  state: () => {
    return {
      name: '张三'
    }
  },
  getters: {...},
  actions: {...},
})

获取state

<template>
  <div>
    普通获取:{{user.name}}
    computed: {{_name}}
    解构:{{name}}
  </div>
</template>
<script lang="ts" setup>
import { userStore } from '../store/user'
import { computed } from 'vue'
import { storeToRefs } from 'pinia'

// 普通获取
const user = userStore()

// computed获取
const _name = computed(() => user.name)

// 解构,但是会失去响应,可以用pinia的 storeToRefs避免失去响应
const { name } = storeToRefs(user)
</script>

修改state

直接修改(不推荐)

...其他代码
<script lang="ts" setup>
import { userStore } from '../store/user'
const user = userStore()
// 修改state(不建议)
user.name = '李四'
</script>

建议通过actions去修改

export const useUserStore = defineStore({ 
    id: 'user', 
    state: () => { return { name: '张三' } }, 
    actions: { updateName (name) { this.name = name } } 
})
<script lang="ts" setup> 
  import { useUserStore } from '@/store/user' 
  const userStore = useUserStore() 
  userStore.updateName('王五') 
</script>

Getters

export const useUserStore = defineStore({
 id: 'user',
 state: () => {
   return {
     name: '张三'
   }
 },
 getters: {
   fullName: (state) => {
     return state.name + '真帅'
   }
 }
})
useUserStore.fullName   // 张三真帅

Actions

export const useUserStore = defineStore({
  id: 'user',
  actions: {
    async getUserName() {
      const { data } = await api.getUserName()
      return data
    }
  }
})

源码分析

以上简单说了一些Pinia的使用方式,下面来简单分析一下源码。 本文主要从创建Pinia(createPinia)定义store(defineStore)两个主要函数进行分析。

createPinia(方法路径:packages/pinia/src/index.ts)

export function createPinia(): Pinia {
  ...
  // 当前pinia实例 
  const pinia: Pinia = markRaw({
    // vue的插件机制,对外暴露install方法
    install(app: App) {
      // this allows calling useStore() outside of a component setup after
      // installing pinia's plugin
      // 设置当前活跃的 pinia,当存在多个活跃的pinia时,方便获取
      setActivePinia(pinia)
      if (!isVue2) {
        pinia._a = app
        // 通过provide传递pinia实例,提供给后续使用
        app.provide(piniaSymbol, pinia)
        // 设置全局属性 $pinia
        app.config.globalProperties.$pinia = pinia
        /* istanbul ignore else */
        if (__DEV__ && IS_CLIENT) {
          // @ts-expect-error: weird type in devtools api
          registerPiniaDevtools(app, pinia)
        }
        toBeInstalled.forEach((plugin) => _p.push(plugin)) // 加载pinia插件
        toBeInstalled = []
      }
    },

    // pinia对外暴露的插件用法
    use(plugin) {
      if (!this._a && !isVue2) {
        toBeInstalled.push(plugin)
      } else {
        _p.push(plugin)
      }
      return this
    },

    _p,
    // it's actually undefined here
    // @ts-expect-error
    _a: null,
    _e: scope,
    _s: new Map<string, StoreGeneric>(),
    state, // 所有状态
  })

  // pinia devtools rely on dev only features so they cannot be forced unless
  // the dev build of Vue is used
  if (__DEV__ && IS_CLIENT) {
    // 集成 vue devtools
    pinia.use(devtoolsPlugin)
  }

  return pinia
}

以上注释已表明

defineStore(方法路径:packages/pinia/src/store.ts 822行)

export function defineStore(
  // TODO: add proper types from above
  idOrOptions: any,
  setup?: any,
  setupOptions?: any
): StoreDefinition {
  let id: string
  let options
  const isSetupStore = typeof setup === 'function'
  
  // 根据传参格式,获取id和otions,可以defineStore(id, options),也可以defineStore({id: '唯一值', ...})
  if (typeof idOrOptions === 'string') {
    id = idOrOptions
    // the option store setup will contain the actual options in this case
    options = isSetupStore ? setupOptions : setup
  } else {
    options = idOrOptions
    id = idOrOptions.id
  }
  
  // 返回Store
  function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
    // 通过vue的getCurrentInstance方法获取vue实例
    const currentInstance = getCurrentInstance()
    
    // 判断pinia是否存在,不存在的话通过inject(piniaSymbol)获取(install时提供的app.provide(piniaSymbol, pinia)
    pinia =
      // in test mode, ignore the argument provided as we can always retrieve a
      // pinia instance with getActivePinia()
      (__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
      (currentInstance && inject(piniaSymbol))
    
    // 设置当前活跃的pinia实例,有多个pinia实例时,方便获取当前活跃实例
    if (pinia) setActivePinia(pinia)  // 路径:packages/pinia/src/rootStore.ts

    // activePinia不存在时,报错提示入口use(pinia)
    if (__DEV__ && !activePinia) {
      throw new Error(
        `[🍍]: getActivePinia was called with no active Pinia. Did you forget to install pinia?\n` +
          `\tconst pinia = createPinia()\n` +
          `\tapp.use(pinia)\n` +
          `This will fail in production.`
      )
    }
    pinia = activePinia!
      
    // 初始化时pinia._s.has(id)没有值
    if (!pinia._s.has(id)) {
    
      // creating the store registers it in `pinia._s`
      //  const isSetupStore = typeof setup === 'function'
      if (isSetupStore) {
        createSetupStore(id, setup, options, pinia)
      } else {
        createOptionsStore(id, options as any, pinia)
      }

      /* istanbul ignore else */
      if (__DEV__) {
        // @ts-expect-error: not the right inferred type
        useStore._pinia = pinia
      }
    }

    const store: StoreGeneric = pinia._s.get(id)!

    if (__DEV__ && hot) {
      const hotId = '__hot:' + id
      const newStore = isSetupStore
        ? createSetupStore(hotId, setup, options, pinia, true)
        : createOptionsStore(hotId, assign({}, options) as any, pinia, true)

      hot._hotUpdate(newStore)

      // cleanup the state properties and the store from the cache
      delete pinia.state.value[hotId]
      pinia._s.delete(hotId)
    }

    // save stores in instances to access them devtools
    if (
      __DEV__ &&
      IS_CLIENT &&
      currentInstance &&
      currentInstance.proxy &&
      // avoid adding stores that are just built for hot module replacement
      !hot
    ) {
      const vm = currentInstance.proxy
      const cache = '_pStores' in vm ? vm._pStores! : (vm._pStores = {})
      cache[id] = store
    }

    // StoreGeneric cannot be casted towards Store
    return store as any
  }

  useStore.$id = id

  return useStore
}

此函数首先根据传参格式,获取唯一标识id相关配置options,然后返回useStore函数。useStore函数最终会返回store,它首先通过getCurrentInstance方法获取vue实例,紧接着去判断Pinia是否存在,不存在的话通过inject(piniaSymbol)获取。然后去设置当前活跃的Pinia实例。通过setUp是不是函数去判断应该走createSetupStore函数还是createOptionsStore函数。下面主要分析createOptionsStore函数。

createOptionsStore(方法路径:packages/pinia/src/store.ts 105行)

function createOptionsStore<
  Id extends string,
  S extends StateTree,
  G extends _GettersTree<S>,
  A extends _ActionsTree
>(
  id: Id,
  options: DefineStoreOptions<Id, S, G, A>,
  pinia: Pinia,
  hot?: boolean
): Store<Id, S, G, A> {
  const { state, actions, getters } = options
  const initialState: StateTree | undefined = pinia.state.value[id]
  let store: Store<Id, S, G, A>

  function setup() {
    if (!initialState && (!__DEV__ || !hot)) {
      /* istanbul ignore if */
      if (isVue2) {
        set(pinia.state.value, id, state ? state() : {})
      } else {
        pinia.state.value[id] = state ? state() : {}
      }
    }

    // avoid creating a state in pinia.state.value
    // 主要是把传入的state变成响应式
    const localState =
      __DEV__ && hot
        ? // use ref() to unwrap refs inside state TODO: check if this is still necessary
          toRefs(ref(state ? state() : {}).value)
        : toRefs(pinia.state.value[id])

    return assign(
      localState,
      actions,
      // 把getters的value值由原本的普通函数转成计算属性
      Object.keys(getters || {}).reduce((computedGetters, name) => {
        computedGetters[name] = markRaw(
          computed(() => {
            setActivePinia(pinia)
            // it was created just before
            const store = pinia._s.get(id)!

            // allow cross using stores
            /* istanbul ignore next */
            if (isVue2 && !store._r) return

            // @ts-expect-error
            // return getters![name].call(context, context)
            // TODO: avoid reading the getter while assigning with a global variable
            // 返回回调函数中带有store参数,因此使用的时候可以getters:{test: (state) => state + 1}
            return getters![name].call(store, store)
          })
        )
        return computedGetters
      }, {} as Record<string, ComputedRef>)
    )
  }

 
  store = createSetupStore(id, setup, options, pinia, hot)

  store.$reset = function $reset() {
    const newState = state ? state() : {}
    // we use a patch to group all changes into one single subscription
    this.$patch(($state) => {
      assign($state, newState)
    })
  }

  return store as any
}

首先通过setUp函数去整合stategetters同时转换成响应式数据,然后通过assign()方法去整合actions,最终一并返回。然后通过createSetupStore函数去返回store

createSetupStore核心逻辑

定义了partialStore变量去整合_p、$id、$onAction、$patch、$subscribe(callback, options = {})、$dispose相关内容。

image.png

然后去整合store挂载到Pinia

const store: Store<Id, S, G, A> = reactive(
    assign(
      __DEV__ && IS_CLIENT
        ? // devtools custom properties
          {
            _customProperties: markRaw(new Set<string>()),
            _hmrPayload,
          }
        : {},
      partialStore
      // must be added later
      // setupStore
    )
  ) 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上
  pinia._s.set($id, store)

最后useStore函数返回store

const store: StoreGeneric = pinia._s.get(id)! // pinia._s.get(id)值就是上面pinia._s.set($id, store)设置的
return store as any

image.png