Pinia基本使用与源码分析-手动实现部分主要功能

331 阅读4分钟

基本使用

  • 在main.js中注册pinia
import { Vue,createApp } from 'vue'
import App from './App'
const app = createApp(App)
// 引入pinia
import { createPinia } from 'pinia'
// 初始化pinia,并注册
const pinia = createPinia()
app.use(pinia).mount('#app')
  • /store/counter.js 声明store的配置
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter',{
    state(){
        return {
            count: 10,
            price: 100
        }
    },
    getters:{
        totalPrice(){
            return ${this.count * this.price}`;
        }
    },
    actions:{
        increment(num){
            this.count += num
        }
    }
})
  • 在App.vue中使用store
<script setup>
// 引用counter仓库
import { useCounterStore } from '@/store/counter'
// 初始化仓库
const store = useCounterStore();
</script>

<template>
  <button type="button" @click="handleChangeSum">count is: {{ countStore.count }}</button>
  <button type="button">price is: {{ countStore.price }}</button>
  <h1>总价格:{{ countStore.totalPrice }}</h1>
</template>

一、修改属性的四种方式

/**
 * 1. 直接修改
 * 因为pinia中的state属性都是响应式的,pinia支持直接修改属性
 */
 const dispatchIncrement = ()=>{
     store.count+=100;
 }
 
/**
 * 2. 使用$patch更改属性
 * $patch支持两种修改属性的方法(对象形式或回调函数形式)
 */
 const dispatchIncrement = ()=>{
     // $patch对象形式
     store.$patch({ count: store.count + 100})
 }
 
/**
 * 3. 使用$patch更改属性 (回调函数形式)
 */
 const dispatchIncrement = ()=>{
     // $patch对象形式
     store.$patch((state)=>{ state.count+=100 })
 }
 
/**
 * 4. 使用actions修改属性
 */
 const dispatchIncrement = ()=>{
     store.increment(100)
 }

二、state属性解构实现响应式

import { useCounterStore } from '@/store/counter'
import { storeToRefs } = 'pinia'
// 初始化仓库
const store = useCounterStore();
// 通过storeToRefs实现解构后依然是响应式状态 (内部通过toRef实现)
const { count,price,totalPrice }  = storeToRefs(store)


<template>
  <button type="button" @click="handleChangeCount">count is: {{ count }}</button>
  <button type="button">price is: {{ price }}</button>
  <h1>总价格:{{ totalPrice }}</h1>
</template>

三、actions

counter.ts

export const useCounterStore = defineStore('counter', {
  actions: {
    getRandomNum(delay: number): Promise<number> {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve(Math.random());
        }, delay);
      });
    },
  },
});

App.vue

import { useCounterStore } from '@/store/counter'
const store = useCounterStore()

const getRandomNumClick = async () => {
  // 两秒之后获取一个随机数
  const number = await store.getRandomNum(2000);
  console.log(number);
}

<template>
  <button @click="getRandomNumClick">获取随机数</button>
</template>

四、监听store的变化

// 监听store的变化
countStore.$subscribe((mutations, state) => {
  console.log(mutations, state);
})

五、重置数据

// 调用$reset方法重置数据
const reset = () => {
  countStore.$reset()
}

<button @click="reset">重置</button>

源码分析与实现

一、createPinia

  • 该方法返回一个pinia对象,内部提供install方法,方便注册
  • _a 用于保存Vue的实例对象
  • _m 参数用于保存所有的模块
  • _e 最外层的作用域scope
  • state 通过作用域创建的ref对象,初始值是一个空对象{}
import { markRaw,EffectScope } from 'vue'
import type { App } from 'vue'
interface Pinia {
  install:(app:App)=>void
  _e: EffectScope;
  _m: Map<any, any>;
  _a?:App;
  state:Ref<Record<string,any>>
}
export function craetePinia(){
  // 创建一个scope用于控制依赖收集
  const scope = effectScope(true);
  // 初始化一个state 用于保存store所有的状态
  const state = scope.run(()=>ref({}))!
  
  // 声明一个pinia仓库(不能被响应式)
  const pinia = markRaw<Pinia>({
    install(app:App){
    // 保存Vue的实例对象
    pinia._a = app;
    // 将pinia注入组件
    app.provide(SymbolPinia,pinia);
    // 将pinia挂载到全局
    app.config.globalProperties.$pinia = pinia;
  }
    _e: scope, // pinia依赖收集的作用域
    _m: new Map, // 管理仓库的集合
    state // 存放所有的状态
  })
  return pinia;
}

二、defineStore

  1. store对象
    • 每一个store都是一个reactive对象
    • 处理state,getters,actives,将三者中的属性与store合并
    • 将合并好的store对象存到pinia._m的集合内,key为该仓库id,值为store
  2. state
    • 从模块的配置项中取出state并执行
    • 通过toRefs将state中的属性转为响应式
    • 将结果合并到store
  3. getters
    • 每一个getter都是一个计算属性的结果,具有缓存特性,getter中的this指向store
    • 重新为getter赋值,他的结果是computed的结果,并且在计算属性内通过call调用原始getter函数
    • 将结果合并到store
  4. actions
    • 重写action的方法,通过apply调用原始action,改变action函数的this指向
    • 将结果合并到store
import {
  computed,
  effectScope,
  getCurrentInstance,
  inject,
  reactive,
  toRefs,
  ComputedRef,
  UnwrapRef,
  isRef,
  isReactive,
} from 'vue';
import { SymbolPinia } from './rootStore';
import { Pinia, StoreOptions, StoreOptionsId, StateTree } from './types';
import { isObject } from './utils';

// defineStore第一个参数可以是id 或者是一个配置项
export function defineStore(idorOptions: string, options: StoreOptions): () => void;
export function defineStore(idorOptions: StoreOptionsId): () => void;
export function defineStore(idorOptions: string | StoreOptionsId, optionsSetup?: StoreOptions) {
  let id: string;
  let options: StoreOptions | StoreOptionsId;

  // 用户传入的可能第一个值是字符串的id
  if (typeof idorOptions === 'string') {
    id = idorOptions;
    options = optionsSetup!;
  } else if (typeof idorOptions === 'object') { //传入的第一个参数是一个包含id的配置项
    id = idorOptions.id;
    options = idorOptions;
  }

  // 创建这个store 并添加到pinia._m中
  function useStore() {
    // 获取组件的实例
    const currentInstance = getCurrentInstance();
    // 使用inject获取pinia
    const pinia = currentInstance && inject<Pinia>(SymbolPinia);
    // 从pinia._m属性中获取仓库
    let store = pinia?._m.get(id);
    // 第一次获取没有这个仓库 则创建仓库
    if (!store) pinia?._m.set(id, (store = createOptionsStore(id, options, pinia)));

    return store;
  }
  return useStore;
}

function createOptionsStore(id: string, options: StoreOptions | StoreOptionsId, pinia: Pinia) {
  // 从配置中取出用于创建的state actions getters属性
  let { state, actions, getters } = options;
  // 每一个仓库都是一个响应式对象
  let store = reactive({});

  function setup() {
    /**
     * 处理state
     * 将state中的数据添加到pinia.state中
     * state中所有的值都应该是响应式的
     */
    pinia.state.value[id] = state ? state() : {};
    const localState = toRefs(pinia.state.value[id]) as any;

    /**
     * 处理getters
     * 因为每一个getter都是一个具有缓存的计算属性,直接使用computed处理即可
     */
    let localGetters = Object.keys(getters || {}).reduce((computedGetters, name) => {
      computedGetters[name] = computed(() => {
        return getters?.[name].call(store, store);
      });
      return computedGetters;
    }, {} as Record<string, ComputedRef>);

    // 返回处理后的结果
    return Object.assign(localState, actions, localGetters);
  }

  // 往最外层的作用域内添加依赖(最外层作用域的scope可以管理所有模块的依赖)
  const setupStore = pinia._e.run(() => {
    // 每一个store也有自己的作用域effect
    const scope = effectScope();
    // 使用setup收集参数
    return scope.run(() => setup());
  });

  /**
   * 处理actions
   * 改变action函数的this执行
   */
  for (let key in setupStore) {
    let prop = setupStore[key];
    // 拦截action并改写action的方法
    if (typeof prop === 'function') {
      prop = wrapAction(key, prop);
    }
  }

  function wrapAction(key: string, actionValue: Function) {
    return function (...args: any[]) {
      let res = actionValue.apply(store, args);
      return res;
    };
  }
  // 返回一个响应式的store对象
  return Object.assign(store, setupStore);
}

三、$patch()

  1. 合并更新操作,参数可以是对象或函数
  2. 新值与旧值嵌套对象的情况下,递归拷贝覆盖
// $patch可以是对象或函数
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)
) {
  if (typeof partialStateOrMutator === 'function') {
    // 函数直接执行
    partialStateOrMutator(pinia._m.get(id));
  } else {
    // 对象选择拷贝覆盖
    mergeReactiveObjects(pinia._m.get(id), partialStateOrMutator);
  }
}

function mergeReactiveObjects<T extends StateTree>(target: T, patchToApply: _DeepPartial<T>): T {
  // 将数据合并到store中
  for (let key in patchToApply) {
    // 原型链上的属性不做处理
    if (!patchToApply.hasOwnProperty(key)) continue
    let subPatch = patchToApply[key]!; // 新的数据
    let targetValue = target[key]; // 旧的数据
    // 新数据和旧数据仍然是对象的话 需要递归处理 (ref和reactive对象不做处理)
    if (
      isObject(subPatch) &&
      isObject(targetValue) &&
      target.hasOwnProperty(key) &&
      !isRef(subPatch) &&
      !isReactive(subPatch)
    ) {
      // 递归拷贝
      target[key] = mergeReactiveObjects(targetValue, subPatch);
    } else {
      // @ts-expect-error
      target[key] = subPatch;
    }
  }
  return target;
}

 const partialStore = {
    $patch,
 };
 
 // 返回合并后的整个store对象
 Object.assign(store, partialStore, setupStore);
 return store;

四、$reset()

  1. $reset函数用于重置state为初始状态
  2. 重新调用state方法,使用$patch更新
Object.assign(store, partialStore, setupStore);
/**
 * 重置state中的状态
 */
store.$reset = function () {
  // 重新获取state的结果
  const newState = state ? state() : {};
  // 使用patch将原始结果更新
  this.$patch(($state: _DeepPartial<UnwrapRef<S>>) => {
    Object.assign($state, newState);
  });
};
return store;

五、$subscript()

  1. $subscript函数用于监听state中属性的变化
  2. 内部使用watch实现
  const partialStore = {
    $patch,
    // 监听属性的变化
    $subscript(callback: Function, options = {}) {
      scope.run(() => {
        watch(pinia.state.value[id], $state => {
          callback({ id, type: 'direct' }, $state);
        });
      });
    },
  };

  Object.assign(store, partialStore, setupStore);
  return store;

六、storeToRefs

  • 将store中的属性通过toRef进行转为响应式属性
import { isReactive, isRef, toRaw, toRef } from 'vue';
import { StateTree } from './types';

export function storeToRefs(store: StateTree) {
  store = toRaw(store);
  const ref = {} as Partial<typeof store>;
  for (let key in store) {
    if (isRef(store[key]) || isReactive(store[key])) {
      ref[key] = toRef(store, key);
    }
  }
  return ref;
}