揭秘Pinia核心原理,3分钟内玩转Pinia!

3,080 阅读7分钟

pinia是vue的专属状态管理器,允许跨组件或者跨页面共享数据,pinia在西班牙语中是菠萝的意思,菠萝花是一组各自独立的花朵,它们共同结合在一起,与store类似,每一个都是独立的,但又是相互关联的。

pinia与vuex的对比

  • 在规划vuex下一个迭代版本的时候,发现pinia已经实现了Vuex5的许多想法,因此pinia就作为了Vuex的代替方案
  • pinia更轻便,使用起来更简单方便,可以像写composable函数一样来写store
  • 更好的typeScript支持
  • 支持vueDevtools,对状态的变化进行追踪
  • 废弃了mutations,对于模块的管理更加扁平化
  • 支持在vue2和vue3中进行使用,并且支持mapStates,mapGetters,mapActions等方法

pinia的基本使用

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

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

定义store支持两种方式,一种是options的方式,一种是composables的方式

export const useCountStore('countStore',{
    state: () => {
        count: 0
    },
    getters: () => {
        double () {
            return this.count * 2
        }
    },
    actions:{
        increase () {
             this.count++
        }
    }
})
export const useCountStore = defineStore('countStore',()=>{
    const count = ref(0)
    function increase(){
        count.value++
    }
    const double = computed(()=> count.value * 2)

    return {
        count,
        double,
        increase
    }
})

一个最简版本的pinia实现

pinia实现的基本原理

import { piniaSymbol } from './rootStore'
export function createPinia(){
    const pinia = {
        _s: new Map(), // 用来存储所有的store
        install(app){
            app.provide(piniaSymbol,pinia)
        }
    }
    return pinia
}

在这段代码中createPinia方法返回了一个pinia对象,因为pinia是一个vue插件,所有一定有一个install方法,在install里面将创建的pinia对象通过provide注入到每一个组件中,这样在组件中就可以使用inject来获取到这个pinia,同时给pinia添加一个_s属性,这个_s是一个Map对象,用来保存后续使用defineStore创建出来的store,piniaSymbol是一个Symbol值

export function defineStore (
    id,
    {
        state,
        getters,
        actions
    }
){
    const store = reactive({})
    // 将state挂载到store上
    if(state && typeof state === 'function'){
        const _state = state()
        for (let key in _state){
            store[key] = _state[key]
        }
    }

    // 将getters挂载到store上
    if(getters && Object.keys(getters).length > 0){
        for (let getter in getters){
            store[getter] = computed(getters[getter].bind(store,store))
        }
    }
    function wrapAction(methodName){
        return function(){
            actions[methodName].apply(store,arguments)
        }
    }

    // 将actions挂载到store上
    if(actions && Object.keys(actions).length > 0){
        for(let methodName in actions){
            store[methodName] = wrapAction(methodName)
        }
    }
    return ()=>{
        const pinia = inject(piniaSymbol);
        if(!pinia._s.has(id)){
            pinia._s.set(id,store)
        }
        const _store = pinia._s.get(id)
        return _store
    }
}

上面这段代码,实现了defineStore方法,这个方法返回了一个使用reactive包括的store对象,对没错,pinia的store就是一个用reactive包裹的对象,为什么要用reactive进行包裹呢?因为要让store具有响应性,核心的地方就在下面这段代码,defineStore是在vue组件中使用的,那么我们就可以使用inject获取到前面在createPinia中注入的pinia对象,这样就能够实现不同组件状态共享了,因为所有组件共享的都是同一个pinia,然后将我们创建的store对象添加到pinia的_s属性上,传入的id作为_s的key值,store作为_s的value值,这里进行一下判断,如果_s已经存在这个store了就不在继续添加,最后再从_s中读取出来这个store进行返回。

 return ()=>{
    const pinia = inject(piniaSymbol);
    if(!pinia._s.has(id)){
        pinia._s.set(id,store)
    }
    const _store = pinia._s.get(id)
    return _store
}

我们创建的store是一个空对象,需要将传入的state,getters,和actions进行处理合并到store上面,如下合并state

if(state && typeof state === 'function'){
    const _state = state()
    for (let key in _state){
        store[key] = _state[key]
    }
}

需要注意,合并getters需要使用computed进行包裹,因为getters的内容是会根据state的值的变化进行变化的,所以getter的内容很明显是一个computed,需要使用bind来改变this指向,让getter里面的this都指向当前store,代码如下

if(getters && Object.keys(getters).length > 0){
    for (let getter in getters){
        store[getter] = computed(getters[getter].bind(store,store))
    }
}

接下来就是合并actions,我们使用一个柯里化函数wrapAction进行包裹,抽离出来一个wrapAction的目的,是为了后续方便在这里进行拓展,这里使用apply来改变this指向,使得action方法里的this指向当前store,代码如下

function wrapAction(methodName){
    return function(){
        actions[methodName].apply(store,arguments)
    }
}

// 将actions挂载到store上
if(actions && Object.keys(actions).length > 0){
    for(let methodName in actions){
        store[methodName] = wrapAction(methodName)
    }
}

总结一下,pinia能实现在不同的组件状态共享,是因为每个store都是一个单例模式 vueuse的createSharedComposable也是一个单例模式,也能用来实现状态共享的问题

export function createSharedComposable(composable){
    let state
    return function(){
        if(!state){
            state = composable()
        }
        return state
    }
}

上面这个最简版的pinia实现可以说就是pinia的实现精髓了,pinia的源码也是这么实现的,看完了是不是三分钟就可以写出来自己的pinia了,以后去面试,再也不用害怕手写pinia这样的面试题了,有了上面的这个简版的基础,那么后面在来深入学习pinia源码,就能轻松很多了。更文不易,看到这里如果对你有帮助的话,给个一键三连吧。

pinia源码的createPinia方法分析

createPinia实现的源码

import { ref, effectScope } from 'vue'
import { piniaSymbol } from './rootStore'

export function createPinia(){
    const scope = effectScope()
    const state = scope.run(()=> ref({})) // 用来存储每个store的state
    const _p = []  // 收集插件
    const pinia = {
        _s: new Map(), // 用map来收集所有的store
        _e: scope,
        use(plugin){  // 用来注册插件
            _p.push(plugin)
            return this
        },
        _p,
        install(app){
            app.provide(piniaSymbol,pinia)   // 通过provide将pinia注入到vue组件中
            app.config.globalProperties.$pinia = pinia
            pinia._a = app // 将vue根实例挂载到_a属性上
        },
        _a: null,
        state
    }
    return pinia
}

createPinia的代码功能导图

pinia的defineStore的实现

第一步,先分析入参和返回结果 defineStore的入参有三种形式

const store = defineStore('id',{
    state:()=>{},
    getter:()=>{},
    actions:{}
})
const store = defineStore({ // options入参
    id,
    state:()=>{},
    getter:()=>{},
    actions:{}
})
const store = defineStore('id',()=>{state,getters,actions}) // setup的形式入参

处理入参

// id + options
// options
// id + setup方法
export function defineStore(idOrOptions,setup){
    let id
    let options
    if(typeof idOrOptions === 'string'){
        id = idOrOptions
        options = setup
    }else{
        options = idOrOptions;
        id = idOrOptions.id
    }
    const isSetUpStore = typeof setup === 'function'
}

处理返回函数

export function defineStore(idOrOptions,setup){
    let id
    let options
    if(typeof idOrOptions === 'string'){
        id = idOrOptions
        options = setup
    }else{
        options = idOrOptions;
        id = idOrOptions.id
    }
    // setUp可能是一个函数
    const isSetUpStore = typeof setup === 'function'
    
    function useStore(){
        let instance = getCurrentInstance()
        const pinia = instance && inject(piniaSymbol)
        if(!pinia._s.has(id)){ // 第一次useStore
            if(isSetUpStore){
                createSetUpStore(id,setup,pinia)
            }else{
                createOptionsStore(id,options,pinia)
            }
        }
        const store = pinia._s.get(id)
        return store
    }
    return useStore
}

这里处理options的定义方式,和setup也就是composables定义的方式是不一样的,处理options使用createOptionsStore

function createOptionsStore(id,options,pinia){
    const { state, actions, getters } = options
    let scope;
    const store = reactive({})   // store 就是一个响应式对象
  
    function setup(){  // 对用户传递的state,actions,getters做处理
       const localState =  pinia.state.value[id] = state ? state() : {}
       // getters
       return Object.assign(
        localState, // 用户的状态
        actions,    // 用户的动作
        // 使用computed对getters进行封装
        Object.keys(getters || {}).reduce((memo,name)=>{
            memo[name] = computed(()=> {
                return getters[name].apply(store,localState)
            })
            return memo
        },{}))
    }

    const setupStore = pinia._e.run(()=>{
        scope = effectScope()
        return scope.run(()=> setup() )
    })

    function wrapAction(name,action){
        return function(){ 
            let ret = action.apply(store,arguments)
            // action 执行后可能是promise
            return ret
        }
    }
    for(let key in setupStore){
        const prop = setupStore[key]
        if(typeof prop === 'function'){
            setupStore[key] = wrapAction(key,prop) // 函数劫持
        }
    }
    // pinia._e.stop() // 停止全部
    // scope.stop() // 停止自己
    Object.assign(store,setupStore)
    pinia._s.set(id,store)
    return store
}

处理setupStore使用createSetUpStore

function createSetUpStore(id,setup,pinia,isOption){
    let scope;
    const store = reactive({})   // store 就是一个响应式对象
    const initialState = pinia.state.value[id] // setup默认是没有初始化状态的
    if(!initialState && !isOption){
        pinia.state.value[id] = {}
    }

    const setupStore = pinia._e.run(()=>{
        scope = effectScope()
        return scope.run(()=> setup() )
    })

    function wrapAction(name,action){
        return function(){ 
            let ret = action.apply(store,arguments)
            // action 执行后可能是promise
            return ret
        }
    }
    for(let key in setupStore){
        const prop = setupStore[key]
        if(typeof prop === 'function'){
            setupStore[key] = wrapAction(key,prop) // 函数劫持
        }
        if((isRef(prop) && !isComputed(prop)) || isReactive(prop)){
            if(!isOption){
                console.log('props',key)
                pinia.state.value[id][key] = prop
            }
        }
    }
    // pinia._e.stop() // 停止全部
    // scope.stop() // 停止自己
    Object.assign(store,setupStore)
    pinia._s.set(id,store)
    return store
}

将两个函数相同的部分抽离出来优化createOptionsStore方法,最终实现如下

function createOptionsStore(id,options,pinia){
    const { state, actions, getters } = options
   
    function setup(){  // 对用户传递的state,actions,getters做处理
       const localState =  pinia.state.value[id] = state ? state() : {}
       // getters
       return Object.assign(
        localState, // 用户的状态
        actions,    // 用户的动作
        // 使用computed对getters进行封装
        Object.keys(getters || {}).reduce((memo,name)=>{
            memo[name] = computed(()=> {
                const store = pinia._s.get(id)
                return getters[name].apply(store,localState)
            })
            return memo
        },{}))
    }
    return createSetUpStore(id,setup,pinia,true)
}

pinia的内置方法实现

$patch

除了用 store.count++ 直接改变 store,可以调用 $patch 方法。它允许你用一个 state 的补丁对象在同一时间更改多个属性

function mergeRectiveObject(target,state){
    for(let key in state){
        let oldValue = target[key]
        let newValue = state[key]
        if(isObject(oldValue) && isObject(newValue)){
            mergeRectiveObject(oldValue,newValue) // 递归合并
        }else{
            target[key] = newValue
        }
    }
    return target
}

function $patch(partialStateOrMutatior){
     if(typeof partialStateOrMutatior === 'object'){
         // 用新的状态合并老的状态
         mergeRectiveObject(pinia.state.value[id],partialStateOrMutatior)
     }else{
         partialStateOrMutatior(pinia.state.value[id])
     }
 }
const partialStore = {
    $patch
}
const store = reactive(partialStore)   // store 就是一个响应式对象

$patch的原理就是对象的深度合并

$reset

$reset用来重置store的状态

store.$reset = function(){
    const initState = state ? state() : {}
    store.$patch((state)=>{
        Object.assign(state,initState)
    })
}

$reset这个方法只支持options的传参形式,不支持composables的传参形式,因为composables的传参,在setup函数执行的时候,state的值可能会被修改,不好拿到初始值,在composables中有更好的方式处理,直接通过actions改变state的值就行了,官方的代码是这样子的

  const $reset = isOptionsStore
    ? function $reset(this: _StoreWithState<Id, S, G, A>) {
        const { state } = options as DefineStoreOptions<Id, S, G, A>
        const newState = state ? state() : {}
        // we use a patch to group all changes into one single subscription
        this.$patch(($state) => {
          assign($state, newState)
        })
      }
    : /* istanbul ignore next */
    __DEV__
    ? () => {
        throw new Error(
          `🍍: Store "${$id}" is built using the setup syntax and does not implement $reset().`
        )
      }
    : noop

$subScribe

$subScribe用于订阅状态的变化,当转态发生变化需要执行某些操作,可以使用此方法

const partialStore = {
    $patch,
    $subscribe(callback,options = {}){
        // 每次状态变化都会执行订阅
        scope.run(()=> watch(pinia.state.value[id],(state)=>{
            callback({storeId:id},state)
        },options))
    }
}

$subScribe的实现很简单,只是用watch去监听store上的state状态值的变化,然后执行回调函数。

$onAction

用于监听action的调用,store.$onAction({after,error}) => {  }),在action执行完成之后调用after回调函数 要实现此功能很明显是使用的发布订阅者模式,因此需要实现一个发布订阅的工具函数

export function addSubscription(subscriptions,callback){
    subscriptions.push(callback)
    const removeSubscription = ()=>{
        const index = removeSubscriptions.indexof(callback)
        idx > -1 && subscriptions.splice(idx,1)
    }
    return removeSubscription
}
export function triggerSubscription(subscriptions,...args){
    subscriptions.slice().forEach(cb => cb(...args))  
    // 这里的slice(), 没有传递参数,只返回一个原数组的浅拷贝,这是常见的用法,用来遍历数组的副本,而不修改原始数据
}
let actionSubscriptions = []
const partialStore = {
   $onAction: addSubscription.bind(null, actionSubscriptions),
}
const store = reactive(partialStore)

在执行$onAction的时候,先将方法订阅添加到actionSubscriptions这个对象里面,在执行actions动作的时候进行触发

function wrapAction(name,action){
    return function(){ 
        const afterCallbackList = []
        const onErrorCallbackList = []
        function after(callback){
            afterCallbackList.push(callback)
        }
        function onError(callback){
            onErrorCallbackList.push(callback)
        }
        triggerSubscription(actionSubscriptions,{ after, onError })
        
        let ret
        try{
            ret = action.apply(store,arguments)
            triggerSubscription(afterCallbackList, ret)
        }catch(e){
            triggerSubscription(onErrorCallbackList,e)
        }

        if(ret instanceof Promise){
            return ret.then((value)=>{
                return  triggerSubscription(afterCallbackList, value)
            }).catch(e=>{
                triggerSubscription(onErrorCallbackList,e)
                return Promise.resolve(e)
            })
        }
        // action 执行后可能是promise
        return ret
    }
}

$disPose

此方法用来注销store

const partialStore = {
    $dispose(){
        scope.stop()
        actionSubscriptions = []
        pinia._s.delete(id)
    }
}

$state

$state可以用来直接修改store的状态

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

storeToRefs

使用storeToRefs可以让store里面的内容解构不丢失响应式

回顾一下toRef与toRefs的实现

function toRef(obj,key){
	const wrapper = {
        get value(){
            return obj[key]
        }
    }
    return wrapper
}

function toRefs(obj){
    const ret = {}
    for(const key in obj){
        ret[key] = toRef(obj,key)
    }
    return ret
}
import { toRaw, toRef, isRef, isReactive } from 'vue'
export function storeToRefs(store){
    store = toRaw(store)
    const refs = {}
    for(let key in store){
        // 排除对函数的操作
        if(isRef(value) || isReactive(value)){
            refs[key] = toRef(store,key)
        }
    }
    return refs
}

storeToRefs的实现跟toRefs的实现一样,只是排除了对函数的处理

pinia插件的实现

pinia.use((store) => {
    store.hello = 'Hello pinia'
})
// createPinia.js
const _p = []
const pinia = {
    use(plugin){
        _p.push(plugin)
    },
    _p,  
}

// store.js
function createSetUpStore(){
     const store = reactive({})
     pinia._p.forEach(plugin => {
         plugin({ store })
     })
     return store
}

从上面代码发现插件的实现非常简单

vue2中插件的实现原理

pinia在vue2中使用要借助PiniaVuePlugin

import { createPinia, PiniaVuePlugin } from 'pinia'
Vue.use(PiniaVuePlugin)
const pinia = createPinia()
new Vue({
  el: '#app',
  pinia,
})

实现源码如下

export const PiniaVuePlugin: Plugin = function (_Vue) {
  // Equivalent of
  // app.config.globalProperties.$pinia = pinia
  _Vue.mixin({
    beforeCreate() {
      const options = this.$options
      if (options.pinia) {
        const pinia = options.pinia as Pinia
        // HACK: taken from provide(): https://github.com/vuejs/composition-api/blob/main/src/apis/inject.ts#L31
        /* istanbul ignore else */
        if (!(this as any)._provided) {
          const provideCache = {}
          Object.defineProperty(this, '_provided', {
            get: () => provideCache,
            set: (v) => Object.assign(provideCache, v),
          })
        }
        ;(this as any)._provided[piniaSymbol as any] = pinia

        // propagate the pinia instance in an SSR friendly way
        // avoid adding it to nuxt twice
        /* istanbul ignore else */
        if (!this.$pinia) {
          this.$pinia = pinia
        }

        pinia._a = this as any
        if (IS_CLIENT) {
          // this allows calling useStore() outside of a component setup after
          // installing pinia's plugin
          setActivePinia(pinia)
        }
        if (USE_DEVTOOLS) {
          registerPiniaDevtools(pinia._a, pinia)
        }
      } else if (!this.$pinia && options.parent && options.parent.$pinia) {
        this.$pinia = options.parent.$pinia
      }
    },
    destroyed() {
      delete this._pStores
    },
  })
}

实现原理,获取Vue实例,通过mixin实现数据共享,将vuex也是通过同样的原理进行实现的、 手写版本代码仓库:github.com/PoliWen/myP…,如果对你有帮助的话,给个小心心吧。