vue3复习-源码-pinia

163 阅读7分钟

复习vuex

vuex使用方法

store/index.js

import {createStore} from 'vuex'

const store = createStore({
    state(){
        return {
            cnt:111
        }
    },
    mutations: {
        addCnt(state){
            state.cnt++
        }
    }
})
export default store

test.vue

<template>
    <div>
        <button @click="test">{{store.state.cnt}}</button>
    </div>
</template>

<script setup lang="ts">
import {useStore} from 'vuex'
const store = useStore()
function test(){
    store.commit("addCnt")
}
</script>

<style scoped>

</style>

vuex源码实现

vuexMini.js

import {inject,provide,reactive,computed} from 'vue'
const storeKey = '__store__key__'
function useStore(){ 
    return inject(storeKey)//这里返回注入的 store
}
function createStore(options){
    return new Store(options)
}
class Store{
    constructor(options){
        this._state = reactive({
            data:options.state() //这里需要主动调用一次state才能返回数据
        })
        this._mutations = options.mutations
        this._actions = options.actions
        this.getters = {}
        const kes = Object.keys(options.getters)
        for (let index = 0; index < kes.length; index++) {
            const key = kes[index];
            const fun = options.getters[key]
            Object.defineProperty(this.getters,key,{
                get: ()=>{
                        return computed(()=>{ //设置缓存
                            return fun(this)
                        } 
                    ) 
                }
            })
        }
    }
    install(app) {//注册时候的代码,把store挂载到全局 
        app.provide(storeKey,this)
    }
    get state(){
        return this._state.data // 这里只返回内部变量的data
    }
    commit(key,palyload){
        this._mutations[key](this,palyload)
    }
    dispatch(key,palyload){
        this._actions[key](this,palyload)
    }
} 
export {createStore, useStore}

store/index.js

import {createStore} from '../vuexMini.js'

const store = createStore({
    state(){
        return {
            cnt:111
        }
    },
    mutations: {
        addCnt(store){  
            store.state.cnt++
        }
    },
    getters: {
        doubleCnt(store){ 
            return store.state.cnt * 2
        }
    },
    actions: {
        addCntByReq( store){
            new Promise( (resolve,reject)=> {
                setTimeout(() => {
                    resolve()
                }, 1000);
            }).then((res) => {
                store.commit("addCnt",100)
            })
            
        }
    }
})
export default store

test.vue

<template>
    <div>
        {{store.state.cnt}}
        {{store.getters.doubleCnt}}
        <button @click="test">xxx</button>
    </div>
</template>

<script setup lang="ts">
import {useStore} from '../vuexMini.js'
const store = useStore() 
function test(){ 
    store.commit("addCnt",store.state.cnt++) 
    store.dispatch('addCntByReq')
}
</script>

<style scoped>

</style>

pinia

pinia.vuejs.org/

优势

  • 同时兼容vue2 和 vue3
  • 支持option 和 composition的写法
  • 去掉了vuex中mutation的概念,不区分同步异步方法,而是在action里面直接编写async/await
import { defineStore } from 'pinia';

export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null,
    loading: false,
  }),
  actions: {
    async fetchUserInfo() {
      this.loading = true;  // 更新状态
      try {
        const response = await fetch('/api/user');  // 异步请求
        this.userInfo = await response.json();  // 设置用户信息
      } catch (error) {
        console.error("Failed to fetch user info:", error);
      } finally {
        this.loading = false;  // 完成后重置 loading 状态
      }
    }
  }
});

使用方法

npm install pinia
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';

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

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

option写法

// stores/count.js
import { defineStore } from 'pinia';

export const useCountStore = defineStore('count', {
  state: () => ({
    count: 0,  // 初始化计数器
  }),
  actions: {
    increment() {
      this.count++;
    },
    decrement() {
      this.count--;
    },
    async incrementAfterDelay() {
      // 模拟异步操作
      await new Promise(resolve => setTimeout(resolve, 1000));
      this.count++;
    }
  },
});

<template>
  <div>
    <h1>Count: {{ countStore.count }}</h1>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
    <button @click="incrementAfterDelay">Increment After Delay</button>
  </div>
</template>

<script setup>
import { useCountStore } from '@/stores/count';

// 初始化 store
const countStore = useCountStore(); 
const { increment, decrement, incrementAfterDelay } = countStore;
</script>

compositonApi写法

// stores/count.js
import { defineStore } from 'pinia';
import { ref } from 'vue';

export const useCountStore = defineStore('count', () => {
  // 使用 ref 创建响应式状态
  const count = ref(0);

  // 同步增减方法
  const increment = () => {
    count.value++;
  };

  const decrement = () => {
    count.value--;
  };

  // 异步增加方法
  const incrementAfterDelay = async () => {
    await new Promise(resolve => setTimeout(resolve, 1000)); // 模拟延迟
    count.value++;
  };

  return {
    count,
    increment,
    decrement,
    incrementAfterDelay,
  };
});

源码部署

版本 2.2.6

git clone https://github.com/vuejs/pinia.git
pnpm i #安装依赖
pnpm play # 运行

服务器启动后: image.png

点击demo-counter 例子 image.png

源码例子

我们打开代码看到这个定义和使用的文件

定义

packages/playground/src/stores/demo-counter.ts

export const useCounter = defineStore('demo-counter', {
  state: () => ({
    n: 0,
  }),
  getters: {
    double: (state) => state.n * 2,
  },
})

packages/playground/src/views/DemoCounter.vue

<template>
  <h2>Counter Store</h2>

  <p>Counter :{{ counter.n }}. Double: {{ counter.double }}</p>

  <button @click="counter.n++">Increment</button>

  <hr />

  <p>
    <code>counter.$state</code>:
  </p>

  <pre>{{ counter.$state }}</pre>
</template>

<script lang="ts" setup>
import { useCounter } from '../stores/demo-counter'

const counter = useCounter()
</script>

  • 通过useCounter获得对象
  • 通过counter.$state访问属性
  • 通过double获取计算属性

调试

我们在main.js加上debugger

image.png

执行了createPinia代码

  • 使用effectScope创建数据管理对象
  • 使用scope.run里面定义一个 ref({})对象作为state
  • 返回一个pinia实例,包含use 和 install方法,还注册了provide 对应实例的全局对象
  • 当 createApp(App).use(pinia) 时候触发install 逻辑

createPinia.ts

createPinia
import { Pinia, PiniaPlugin, setActivePinia, piniaSymbol } from './rootStore'
import { ref, App, markRaw, effectScope, isVue2, Ref } from 'vue-demi'
import { registerPiniaDevtools, devtoolsPlugin } from './devtools'
import { IS_CLIENT } from './env'
import { StateTree, StoreGeneric } from './types'

/**
 * Creates a Pinia instance to be used by the application
 */
export function createPinia(): Pinia {
  const scope = effectScope(true)
  // NOTE: here we could check the window object for a state and directly set it
  // if there is anything like it with Vue 3 SSR
  const state = scope.run<Ref<Record<string, StateTree>>>(() =>
    ref<Record<string, StateTree>>({})
  )!

  let _p: Pinia['_p'] = []
  // plugins added before calling app.use(pinia)
  let toBeInstalled: PiniaPlugin[] = []

  const pinia: Pinia = markRaw({
    install(app: App) {
      // this allows calling useStore() outside of a component setup after
      // installing pinia's plugin
      setActivePinia(pinia)
      if (!isVue2) {
        pinia._a = app
        app.provide(piniaSymbol, pinia)
        app.config.globalProperties.$pinia = pinia
        /* istanbul ignore else */
        if (__USE_DEVTOOLS__ && 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,
    // 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. Avoid old browsers like IE11.
  if (__USE_DEVTOOLS__ && typeof Proxy !== 'undefined') {
    pinia.use(devtoolsPlugin)
  }

  return pinia
}

/**
 * Dispose a Pinia instance by stopping its effectScope and removing the state, plugins and stores. This is mostly
 * useful in tests, with both a testing pinia or a regular pinia and in applications that use multiple pinia instances.
 * Once disposed, the pinia instance cannot be used anymore.
 *
 * @param pinia - pinia instance
 */
export function disposePinia(pinia: Pinia) {
  pinia._e.stop()
  pinia._s.clear()
  pinia._p.splice(0)
  pinia.state.value = {}
  // @ts-expect-error: non valid
  pinia._a = null
}

store.ts

packages/pinia/src/store.ts

defineStore

支持3种方式声明,主要就是 option 或 setup


export function defineStore(
  // TODO: add proper types from above
  idOrOptions: any,
  setup?: any,
  setupOptions?: any
): StoreDefinition {
  let id: string
  let options:
    | DefineStoreOptions<
        string,
        StateTree,
        _GettersTree<StateTree>,
        _ActionsTree
      >
    | DefineSetupStoreOptions<
        string,
        StateTree,
        _GettersTree<StateTree>,
        _ActionsTree
      >

  const isSetupStore = typeof setup === 'function'
  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 
  }

  function useStore(pinia?: Pinia | null, hot?: StoreGeneric): StoreGeneric {
    const hasContext = hasInjectionContext()
    pinia = 
    if (pinia) setActivePinia(pinia)
 

    pinia = activePinia!

    if (!pinia._s.has(id)) { // 这里通过_s缓存数据集合 
      // creating the store registers it in `pinia._s`
      if (isSetupStore) {
        createSetupStore(id, setup, options, pinia)
      } else {
        createOptionsStore(id, options as any, pinia)
      }
 
    }

    const store: StoreGeneric = pinia._s.get(id)!
 
    // StoreGeneric cannot be casted towards Store
    return store as any
  }

  useStore.$id = id

  return useStore
}



export function defineStore<
  Id extends string,
  S extends StateTree = {},
  G extends _GettersTree<S> = {},
  // cannot extends ActionsTree because we loose the typings
  A /* extends ActionsTree */ = {},
>(
  id: Id,
  options: Omit<DefineStoreOptions<Id, S, G, A>, 'id'>
): StoreDefinition<Id, S, G, A>

/**
 * Creates a `useStore` function that retrieves the store instance
 *
 * @param options - options to define the store
 */
export function defineStore<
  Id extends string,
  S extends StateTree = {},
  G extends _GettersTree<S> = {},
  // cannot extends ActionsTree because we loose the typings
  A /* extends ActionsTree */ = {},
>(options: DefineStoreOptions<Id, S, G, A>): StoreDefinition<Id, S, G, A>

/**
 * Creates a `useStore` function that retrieves the store instance
 *
 * @param id - id of the store (must be unique)
 * @param storeSetup - function that defines the store
 * @param options - extra options
 */
export function defineStore<Id extends string, SS>(
  id: Id,
  storeSetup: (helpers: SetupStoreHelpers) => SS,
  options?: DefineSetupStoreOptions<
    Id,
    _ExtractStateFromSetupStore<SS>,
    _ExtractGettersFromSetupStore<SS>,
    _ExtractActionsFromSetupStore<SS>
  >
): StoreDefinition<
  Id,
  _ExtractStateFromSetupStore<SS>,
  _ExtractGettersFromSetupStore<SS>,
  _ExtractActionsFromSetupStore<SS>
>

createSetupStore

createSetupStore方式创建store

  • $patch实现实时更新,参数partialStateOrMutator 是函数时候 会立即执行
  • mergeReactiveObjects方法用来合并对象数据,合并后保存在subscriptionMutation
  • 通过triggerSubscriptions方法触发
  • 创建一个 partialStore 对象 用于记录相关id $patch、Pinia实例 subscribe订阅的方法
  • 创建一个store = reactive(partialStore) 包裹
  • 最后返回store
  • const store = pinia._s.get(id)!可以获取对应的partialStore
function createSetupStore<
  Id extends string,
  SS extends Record<any, unknown>,
  S extends StateTree,
  G extends Record<string, _Method>,
  A extends _ActionsTree,
>(
  $id: Id,
  setup: (helpers: SetupStoreHelpers) => SS,
  options:
    | DefineSetupStoreOptions<Id, S, G, A>
    | DefineStoreOptions<Id, S, G, A> = {},
  pinia: Pinia,
  hot?: boolean,
  isOptionsStore?: boolean
): Store<Id, S, G, A> {
  let scope!: EffectScope

  const optionsForPlugin: DefineStoreOptionsInPlugin<Id, S, G, A> = assign(
    { actions: {} as A },
    options
  ) 

  // watcher options for $subscribe
  const $subscribeOptions: WatchOptions = { deep: true }
  /* istanbul ignore else */ 

  // internal state
  let isListening: boolean // set to true at the end
  let isSyncListening: boolean // set to true at the end
  let subscriptions: SubscriptionCallback<S>[] = []
  let actionSubscriptions: StoreOnActionListener<Id, S, G, A>[] = []
  let debuggerEvents: DebuggerEvent[] | DebuggerEvent
  const initialState = pinia.state.value[$id] as UnwrapRef<S> | undefined

  // avoid setting the state for option stores if it is set
  // by the setup
  if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
    /* istanbul ignore if */
    if (isVue2) {
      set(pinia.state.value, $id, {})
    } else {
      pinia.state.value[$id] = {}
    }
  }

  const hotState = ref({} as S)

  // avoid triggering too many listeners
  // https://github.com/vuejs/pinia/issues/1129
  let activeListener: Symbol | undefined
  function $patch(stateMutation: (state: UnwrapRef<S>) => void): void
  function $patch(partialState: _DeepPartial<UnwrapRef<S>>): void

  //数据更新的方法
  function $patch(
    partialStateOrMutator:
      | _DeepPartial<UnwrapRef<S>>
      | ((state: UnwrapRef<S>) => void)
  ): void {
    let subscriptionMutation: SubscriptionCallbackMutation<S>
    isListening = isSyncListening = false
    // reset the debugger events since patches are sync
    /* istanbul ignore else */
 
    if (typeof partialStateOrMutator === 'function') {
      partialStateOrMutator(pinia.state.value[$id] as UnwrapRef<S>)
      subscriptionMutation = {
        type: MutationType.patchFunction,
        storeId: $id,
        events: debuggerEvents as DebuggerEvent[],
      }
    } else {
      mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)
      subscriptionMutation = {
        type: MutationType.patchObject,
        payload: partialStateOrMutator,
        storeId: $id,
        events: debuggerEvents as DebuggerEvent[],
      }
    }
    const myListenerId = (activeListener = Symbol())
    nextTick().then(() => {
      if (activeListener === myListenerId) {
        isListening = true
      }
    })
    isSyncListening = true
    // because we paused the watcher, we need to manually call the subscriptions
    triggerSubscriptions(
      subscriptions,
      subscriptionMutation,
      pinia.state.value[$id] as UnwrapRef<S>
    )
  }

  const $reset = isOptionsStore
    ? function $reset(this: _StoreWithState<Id, S, G, A>) {
        const { state } = options as DefineStoreOptions<Id, S, G, A>
        const newState: _DeepPartial<UnwrapRef<S>> = state ? state() : {}
        // we use a patch to group all changes into one single subscription
        this.$patch(($state) => {
          // @ts-expect-error: FIXME: shouldn't error?
          assign($state, newState)
        })
      }

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

  /**
   * Helper that wraps function so it can be tracked with $onAction
   * @param fn - action to wrap
   * @param name - name of the action
   */
  const action = <Fn extends _Method>(fn: Fn, name: string = ''): Fn => {
    if (ACTION_MARKER in fn) {
      // we ensure the name is set from the returned function
      ;(fn as unknown as MarkedAction<Fn>)[ACTION_NAME] = name
      return fn
    }

    const wrappedAction = function (this: any) {
      setActivePinia(pinia)
      const args = Array.from(arguments)

      const afterCallbackList: Array<(resolvedReturn: any) => any> = []
      const onErrorCallbackList: Array<(error: unknown) => unknown> = []
      function after(callback: _ArrayType<typeof afterCallbackList>) {
        afterCallbackList.push(callback)
      }
      function onError(callback: _ArrayType<typeof onErrorCallbackList>) {
        onErrorCallbackList.push(callback)
      }

      // @ts-expect-error
      triggerSubscriptions(actionSubscriptions, {
        args,
        name: wrappedAction[ACTION_NAME],
        store,
        after,
        onError,
      })

      let ret: unknown
      try {
        ret = fn.apply(this && this.$id === $id ? this : store, args)
        // handle sync errors
      } catch (error) {
        triggerSubscriptions(onErrorCallbackList, error)
        throw error
      }

      if (ret instanceof Promise) {
        return ret
          .then((value) => {
            triggerSubscriptions(afterCallbackList, value)
            return value
          })
          .catch((error) => {
            triggerSubscriptions(onErrorCallbackList, error)
            return Promise.reject(error)
          })
      }

      // trigger after callbacks
      triggerSubscriptions(afterCallbackList, ret)
      return ret
    } as MarkedAction<Fn>

    wrappedAction[ACTION_MARKER] = true
    wrappedAction[ACTION_NAME] = name // will be set later

    // @ts-expect-error: we are intentionally limiting the returned type to just Fn
    // because all the added properties are internals that are exposed through `$onAction()` only
    return wrappedAction
  }

  const _hmrPayload = /*#__PURE__*/ markRaw({
    actions: {} as Record<string, any>,
    getters: {} as Record<string, Ref>,
    state: [] as string[],
    hotState,
  })

  const partialStore = { // 用于记录相关id $patch、Pinia实例 subscribe订阅的方法
    _p: pinia,
    // _s: scope,
    $id,
    $onAction: addSubscription.bind(null, actionSubscriptions),
    $patch,
    $reset,
    $subscribe(callback, options = {}) {
      const removeSubscription = addSubscription(
        subscriptions,
        callback,
        options.detached,
        () => stopWatcher()
      )
      const stopWatcher = scope.run(() =>
        watch(
          () => pinia.state.value[$id] as UnwrapRef<S>,
          (state) => {
            if (options.flush === 'sync' ? isSyncListening : isListening) {
              callback(
                {
                  storeId: $id,
                  type: MutationType.direct,
                  events: debuggerEvents as DebuggerEvent,
                },
                state
              )
            }
          },
          assign({}, $subscribeOptions, options)
        )
      )!

      return removeSubscription
    },
    $dispose,
  } as _StoreWithState<Id, S, G, A>

  /* istanbul ignore if */
  if (isVue2) {
    // start as non ready
    partialStore._r = false
  }

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

  const runWithContext =
    (pinia._a && pinia._a.runWithContext) || fallbackRunWithContext

  // TODO: idea create skipSerialize that marks properties as non serializable and they are skipped
  const setupStore = runWithContext(() =>
    pinia._e.run(() => (scope = effectScope()).run(() => setup({ action }))!)
  )!

  // overwrite existing actions to support $onAction
  for (const key in setupStore) {
    const prop = setupStore[key]

    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      // mark it as a piece of state to be serialized
      if (__DEV__ && hot) {
        set(hotState.value, key, toRef(setupStore, key))
        // createOptionStore directly sets the state in pinia.state.value so we
        // can just skip that
      } else if (!isOptionsStore) {
        // in setup stores we must hydrate the state and sync pinia state tree with the refs the user just created
        if (initialState && shouldHydrate(prop)) {
          if (isRef(prop)) {
            prop.value = initialState[key as keyof UnwrapRef<S>]
          } else {
            // probably a reactive object, lets recursively assign
            // @ts-expect-error: prop is unknown
            mergeReactiveObjects(prop, initialState[key])
          }
        }
        // transfer the ref to the pinia state to keep everything in sync
        /* istanbul ignore if */
        if (isVue2) {
          set(pinia.state.value[$id], key, prop)
        } else {
          pinia.state.value[$id][key] = prop
        }
      }
      
      // action
    } else if (typeof prop === 'function') {
      const actionValue = __DEV__ && hot ? prop : action(prop as _Method, key)
      // this a hot module replacement store because the hotUpdate method needs
      // to do it with the right context
      /* istanbul ignore if */
      if (isVue2) {
        set(setupStore, key, actionValue)
      } else {
        // @ts-expect-error
        setupStore[key] = actionValue
      }
 
      // list actions so they can be used in plugins
      // @ts-expect-error
      optionsForPlugin.actions[key] = prop
    } 
  }

  // add the state, getters, and action properties
  /* istanbul ignore if */
  if (isVue2) {
    Object.keys(setupStore).forEach((key) => {
      set(store, key, setupStore[key])
    })
  } else {
    assign(store, setupStore)
    // allows retrieving reactive objects with `storeToRefs()`. Must be called after assigning to the reactive object.
    // Make `storeToRefs()` work with `reactive()` #799
    assign(toRaw(store), setupStore)
  }

  // use this instead of a computed with setter to be able to create it anywhere
  // without linking the computed lifespan to wherever the store is first
  // created.
  Object.defineProperty(store, '$state', {
    get: () => (__DEV__ && hot ? hotState.value : pinia.state.value[$id]),
    set: (state) => {
      /* istanbul ignore if */
      $patch(($state) => {
        // @ts-expect-error: FIXME: shouldn't error?
        assign($state, state)
      })
    },
  }) 
    // avoid listing internal properties in devtools
    ;(['_p', '_hmrPayload', '_getters', '_customProperties'] as const).forEach(
      (p) => {
        Object.defineProperty(
          store,
          p,
          assign({ value: store[p] }, nonEnumerable)
        )
      }
    )
  }

  /* istanbul ignore if */
  if (isVue2) {
    // mark the store as ready before plugins
    store._r = true
  }

  // apply all plugins
  pinia._p.forEach((extender) => {
    /* istanbul ignore else */ 
      assign(
        store,
        scope.run(() =>
          extender({
            store: store as Store,
            app: pinia._a,
            pinia,
            options: optionsForPlugin,
          })
        )!
      ) 
  })
 

  // only apply hydrate to option stores with an initial state in pinia
  if (
    initialState &&
    isOptionsStore &&
    (options as DefineStoreOptions<Id, S, G, A>).hydrate
  ) {
    ;(options as DefineStoreOptions<Id, S, G, A>).hydrate!(
      store.$state,
      initialState
    )
  }

  isListening = true
  isSyncListening = true
  return store
}

createOptionsStore

option方式创建,发现最后还是调用了 createSetupStore 创建


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
    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,
      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 if */
              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
              return getters![name].call(store, store)
            })
          )
          return computedGetters
        },
        {} as Record<string, ComputedRef>
      )
    )
  }

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

  return store as any
}
partialStore

  const partialStore = {
    _p: pinia,
    // _s: scope,
    $id,
    $onAction: addSubscription.bind(null, actionSubscriptions),
    $patch,
    $reset,
    $subscribe(callback, options = {}) {
      const removeSubscription = addSubscription(
        subscriptions,
        callback,
        options.detached,
        () => stopWatcher()
      )
      const stopWatcher = scope.run(() =>
        watch(
          () => pinia.state.value[$id] as UnwrapRef<S>,
          (state) => {
            if (options.flush === 'sync' ? isSyncListening : isListening) {
              callback(
                {
                  storeId: $id,
                  type: MutationType.direct,
                  events: debuggerEvents as DebuggerEvent,
                },
                state
              )
            }
          },
          assign({}, $subscribeOptions, options)
        )
      )!

      return removeSubscription
    },
    $dispose,
  } as _StoreWithState<Id, S, G, A>

partialStore内部结构

image.png

最后返回的store

image.png

总结

  1. createPinia创建pinia实例,并用effectScope监听响应对象state
  2. 同时provide注入到全局
  3. defineStore定义声明store
  4. 根据传入类型执行createOptionsStore 或 createSetupStore
  5. createOptionsStore最终调完getters方法还是调用了createSetupStore
  6. 在createSetupStore 返回一个 reactive的 partialStore 对象
  7. 内部新增了$patch 方法做数据更新

迷你版实现

createPinia.ts

import { StateTree, StoreGeneric } from "pinia";
import { effectScope, markRaw, ref, Ref } from "vue";
import { App } from "vue-demi"; 
export function createPinia() { 
  const scope = effectScope(true);
  const state = scope.run<Ref<Record<string, StateTree>>>(() =>
    ref<Record<string, StateTree>>({})
  )!;
  const pinia = markRaw({
    install(app: App) {
      app.provide(Symbol("my-pinia"), pinia);
    },
    use() {},
    _s: new Map<string, StoreGeneric>(), // 记录所有数据
    state,
    _e: scope,
  });
  return pinia;
}

store.ts

import {
  computed,
  ComputedRef,
  effectScope,
  EffectScope,
  inject,
  markRaw,
  reactive,
  toRaw,
  toRefs,
} from "vue";
import { getCurrentInstance } from "vue"; 


export function defineStore(options: {
  id: string;
  state: any;
  getters: any;
  actions: any;
}) {
  let { id } = options;
  function useStore() {
    const currentInstance = getCurrentInstance(); // 获取实例
    let pinia: any;
    if (currentInstance) {
      pinia = inject(Symbol("my-pinia"));
    }
    if (!pinia._s.has(id)) { 
      createOptionsStore(id, options, pinia);//第一次初始化
    }
    const store = pinia._s.get(id); // 缓存中的store全部数据
    return store;
  }
  useStore.$id = id;
  return useStore;
} 

function createOptionsStore(id: string, options: any, pinia: any) {
  const { state, actions, getters } = options;
  function setup() {
    pinia.state.value[id] = state ? state() : {};
    const localState = toRefs(pinia.state.value[id]);
    return Object.assign(//属性拷贝
      localState,
      actions,
      Object.keys(getters || {}).reduce((computedGetters, name) => {
        computedGetters[name] = markRaw(
          computed(() => {
            const store = pinia._s.get(id)!;
            return getters![name].call(store, store);
          })
        );
        return computedGetters;// 使用computed转化getters方法
      }, {} as Record<string, ComputedRef>) 
    );
  }
  let store = createSetupStore(id, setup, pinia);


  store.$reset = function $reset() {
    const newState = state ? state() : {};
    this.$patch(($state: any) => {
      Object.assign($state, newState);
    });
  };

  return store;
}

function createSetupStore($id: string, setup: any, pinia: any) {  // setup为传入的setup方法

  // 所有信息 包括订阅发布的subscribe
  let partialStore = {
    _p: pinia,
    $id,
    $reset: () => {},
    $patch,
    $dispose,
    // $subscribe
  };

  // 将effect数据存放如pinia._e、setupStore
  let scope!: EffectScope;
  const setupStore = pinia._e.run(() => {
    scope = effectScope();
    return scope.run(() => setup());//这里执行的setup 会被响应式收集管理
  });

  // 属性方法合并
  const store: any = reactive(
    Object.assign(toRaw({}), partialStore, setupStore)
  );
  // pinia大对象 设置每个小store
  pinia._s.set($id, store);

    function $patch(partialStateOrMutator: any): void {  // 更新当前状态
    if (typeof partialStateOrMutator === "function") {
      partialStateOrMutator(pinia.state.value[$id]);
    }
  }
  
  function $dispose() {
    scope.stop(); 
    pinia._s.delete($id);
  }

  return store;
}

参考

玩转vue3