如何自己搞个pinia玩玩(抄的源码)

72 阅读3分钟

前言

学习pinia源码的笔记算是,只支持setupStore模式。

createPinia

import type { Ref } from 'vue-demi'
import { effectScope, markRaw, ref } from 'vue-demi'
import { type Pinia, piniaSymbol, setActivePinia } from './rootStore'
import type { StateTree } from './types'

export function createPinia() {
  // 创建一个effectScope
  // 提供管理和隔离响应式数据的能力
  const scope = effectScope(true)
  // 在创建的 effectScope 中运行一个函数,这个函数返回了一个对象
  const state = scope.run<Ref<Record<string, StateTree>>>(() =>
    ref<Record<string, StateTree>>({}),
  )!
  const plugins: Pinia['plugins'] = []
  // 标记不能变为响应式数据
  const pinia: Pinia = markRaw({
    install(app) {
      setActivePinia(pinia)
      // 关联vue应用实例
      pinia.app = app
      // 提供一个全局的pinia实例
      app.provide(piniaSymbol, pinia)
    },
    use(plugin) {
      // 添加插件
      plugins.push(plugin)
    },
    plugins,
    app: null,
    scope,
    store: new Map<string, any>(),
    state,
  })

  return pinia
}

总结一下就是创建了一个响应式作用域,创建了pinia实例

defineStore

import type { EffectScope } from 'vue-demi'
import { effectScope, hasInjectionContext, inject, reactive } from 'vue-demi'
import { type Pinia, activePinia, piniaSymbol, setActivePinia } from './rootStore'
import type { StateTree, Store, StoreGeneric, _StoreWithState } from './types'

const fallbackRunWithContext = <T>(fn: () => T) => fn()

const { assign } = Object

function createSetupStore<
  Id extends string, SS extends Record<any, unknown>,
  S extends StateTree,
>(
  $id: Id,
  setup: () => SS,
  pinia: Pinia,
): Store<Id, S> {
  // 当前store的scope
  let scope!: EffectScope

  if (__DEV__ && !pinia.scope.active)
    throw new Error('pina destroyed')
  // 初始化state
  pinia.state.value[$id] = {}

  function $reset() {

  }

  function $dispose() {

  }

  const partialStore = {
    pinia,
    $id,
    $reset,
    $dispose,
  } as unknown as _StoreWithState<Id, S>
  // 创建一个store
  const store: Store<Id, S> = reactive(partialStore) as Store<Id, S>
  // 设置到store上
  pinia.store.set($id, store)
  // https://cn.vuejs.org/api/application.html#app-runwithcontext
  const runWithContext
    = (pinia.app && pinia.app.runWithContext) || fallbackRunWithContext
  // 在当前上下文下运行
  // 在pinia总的scope下获取该store的scope
  // run steup函数捕获store中所有的响应式数据一起处理
  // 拿到的值相当于是store return的值
  const setupStore = runWithContext(() =>
    pinia.scope.run(() => (scope = effectScope()).run(setup)!),
  )!
  // 遍历setupStore 设置到store上
  for (const key of Object.keys(setupStore)) {
    const prop = setupStore[key]
    pinia.state.value[$id][key] = prop
    // store.$state[key] = prop
  }
  // 合并store与内置store(提供了一些方法)
  assign(store, setupStore)
  // 遍历pinia的插件
  pinia.plugins.forEach((cb) => {
    assign(store, scope.run(() => {
      cb({
        store,
        app: pinia.app,
        pinia,
      })
    }))
  })

  return store
}

export function defineStore<Id extends string, SS extends Record<any, unknown>>(
  id: Id,
  // 用于store推演
  storeSetup: () => SS,
) {
  if (__DEV__ && typeof id !== 'string')
    throw new Error('[🍍]: id passed to defineStore must be a string')

  function useStore(pinia?: Pinia | null): StoreGeneric {
    // 是否有inject的上下文
    const hasContext = hasInjectionContext()
    // 获取pinia实例
    pinia = pinia || (hasContext ? (inject(piniaSymbol, null)) : null)
    if (pinia)
      setActivePinia(pinia)

    if (__DEV__ && !pinia)
      throw new Error('[🍍]: pinia not installed. Did you forget to call app.use(pinia)?')

    pinia = activePinia!
    // 不存在该pinia
    if (!pinia.store.has(id)) {
      createSetupStore(id, storeSetup, pinia)
      if (__DEV__)
        // @ts-expect-error: not the right inferred type
        useStore._pinia = pinia
    }
    const store = pinia.store.get(id)!

    return store
  }

  useStore.$id = id
  return useStore
}

总结一下就是先从上下文里获取store如果没有再去创建实例,创建时先去设置一些内置的方法与属性,再去通过runWithContext与effectScope获取setupStore中所有的响应式数据并统一管理,此时获取的返回值为setupStore的返回值,将其与内置方法与属性合并存入store集合中

plugin

  • pinia实例上的use方法可以注册plugin函数
  • 在合并完store后遍历调用plugin函数做处理

类型推导

defineStore函数有两个范型可以获取id与setupStore的类型将类型传入createSetupStore函数中通过类型的处理推导出最终store的类型

测试

  1. pnpm create vite选择vue3+ts
  2. pnpm i @baicie/pinia
  3. 编写两个简单的store
import { createApp } from 'vue'
import './style.css'
import { createPinia } from '@baicie/pinia'
import App from './App.vue'
// 创建pinia实例
const pinia = createPinia()
// 注册plugin
pinia.use(({ store }) => {
  console.log('store', store)
})
const app = createApp(App)
app.use(pinia)
app.mount('#app')
import { defineStore } from '@baicie/pinia'
import { reactive } from 'vue'

export const useDemoStore = defineStore('demo', () => {
  const state = reactive({
    a: 1,
  })

  const add = () => {
    state.a++
  }

  return {
    state,
    add,
  }
})

export const useDemoStore2 = defineStore('demo2', () => {
  const state = reactive({
    a: 1,
  })

  const add = () => {
    state.a++
  }

  return {
    state,
    add,
  }
})

4.使用store

<script setup lang="ts">
import { useDemoStore, useDemoStore2 } from './store'
import HelloWorld from './components/HelloWorld.vue'

const store = useDemoStore()
const store2 = useDemoStore2()
</script>

<template>
  <button @click="() => store.add()">
    add
  </button>
  <div>
    {{ store.state.a }}
  </div>

  <HelloWorld msg="msg" />

  <button @click="() => store2.add()">
    add2
  </button>
  <div>
    {{ store2.state.a }}
  </div>
</template>

5.最后效果
image.png

最后

代码地址 github.com/baicie/mini…
npm地址 www.npmjs.com/package/@ba…