网络日志

【pinia源码】一、createPinia源码解析

前言

【pinia源码】系列文章主要分析pinia的实现原理。该系列文章源码参考pinia v2.0.14

源码地址:https://github.com/vuejs/pinia

官方文档:https://pinia.vuejs.org

本篇文章将分析createPinia的实现。

使用

通过createPinia创建一个pinia实例,供应用程序使用。

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

const pinia = createPinia()
const app = createApp(App)

app.use(pinia).mount('#app')

createPinia

createPinia不接受任何参数,它会返回一个pinia实例。

源码位置:packages/pinia/src/createPinia.ts

export function createPinia(): Pinia {
  const scope = effectScope(true)
  const state = scope.run<Ref<Record<string, StateTree>>>(() =>
    ref<Record<string, StateTree>>({})
  )!

  let _p: Pinia['_p'] = []
  let toBeInstalled: PiniaPlugin[] = []

  const pinia: Pinia = markRaw({
    install(app: App) {
      setActivePinia(pinia)
      if (!isVue2) {
        pinia._a = app
        app.provide(piniaSymbol, 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,
    _s: new Map<string, StoreGeneric>(),
    state,
  })

  if (__DEV__ && IS_CLIENT && !__TEST__) {
    pinia.use(devtoolsPlugin)
  }

  return pinia
}

createPinia中首先会创建一个effect作用域对象(如果你不了解effectScope,可参考:RFC),使用ref创建一个响应式对象。紧接着声明了两个数组_ptoBeInstalled,其中_p用来存储扩展store的所有插件,toBeInstalled用来存储那些未install之前使用pinia.use()添加的的plugin

// 创建effect作用域
const scope = effectScope(true)
// 创建响应式对象
const state = scope.run<Ref<Record<string, StateTree>>>(() =>
  ref<Record<string, StateTree>>({})
)!

// 存储扩展store的plugin
let _p: Pinia['_p'] = []
// install之前,使用pinia.use()添加的plugin
let toBeInstalled: PiniaPlugin[] = []

然后声明一个pinia对象(这个pinia会使用markRaw进行包装,使其不会转为proxy),将其return

const pinia: Pinia = markRaw({
  install(app: App) {
    // ...
  },

  use(plugin) {
    // ...
  },

  // 扩展store的plugins
  _p,
  // app实例
  _a: null,
  // effecet作用域对象
  _e: scope,
  // 在这个pinia中注册的stores
  _s: new Map<string, StoreGeneric>(),
  state,
})

if (__DEV__ && IS_CLIENT && !__TEST__) {
  pinia.use(devtoolsPlugin)
}

return pinia

这里重点看下installuse方法。

install

当使用app.use(pinia)时,触发pinia.install函数。在install中首先执行了setActivePinia(pinia),它会将pinia赋给一个activePinia的全局变量。

然后会判断是不是Vue2环境。如果不是Vue2,将app实例赋给pinia._a,然后将pinia注入到app实例,并将pinia设置为全局属性$pinia。如果此时toBeInstalled中有plugins(在install之前添加的plugins),那么会把这些plugins添加到pinia._p中,添加完之后,置空toBeInstalled

install(app: App) {
  setActivePinia(pinia)
  if (!isVue2) {
    pinia._a = app
    app.provide(piniaSymbol, pinia)
    app.config.globalProperties.$pinia = pinia
    if (__DEV__ && IS_CLIENT) {
      registerPiniaDevtools(app, pinia)
    }
    toBeInstalled.forEach((plugin) => _p.push(plugin))
    toBeInstalled = []
  }
}

use

使用use方法可添加一个plugin以扩展每个store。它接收一个plugin参数,返回当前pinia

如果this._a是空的,并且不是Vue2环境,会将plugin中暂存到toBeInstalled中,等待install时进行安装。否则,直接添加到this._p中。

use(plugin) {
  if (!this._a && !isVue2) {
    toBeInstalled.push(plugin)
  } else {
    _p.push(plugin)
  }
  return this
}

你可能有疑问,在installuse中都判断了Vue2的情况,难道pinia没有处理Vue2的情况吗?其实并不是,pinia提供了PiniaVuePlugin专门用来处理Vue2的情况。

如果是Vue2需要使用如下方式:

Vue.use(PiniaVuePlugin)

const pinia = createPinia()

new Vue({
  el: '#app',
  pinia,
})

PiniaVuePlugin

我们来看下PiniaVuePlugin的实现方式。

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

        if (!this.$pinia) {
          this.$pinia = pinia
        }

        pinia._a = this as any
        if (IS_CLIENT) {
          setActivePinia(pinia)
          if (__DEV__) {
            registerPiniaDevtools(pinia._a, pinia)
          }
        }
      } else if (!this.$pinia && options.parent && options.parent.$pinia) {
        this.$pinia = options.parent.$pinia
      }
    },
    destroyed() {
      delete this._pStores
    },
  })
}

PiniaVuePlugin通过向全局混入一个对象来支持pinia。这个对象有两个生命周期函数:beforeCreatedestory

beforeCreate中设置this.$pinia,以使vue实例可以通过this.$pinia的方式来获取pinia