Jinuss's blog Jinuss's blog
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 《Vue》
    • 《React》
    • 《Git》
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

东流

前端可视化
首页
  • 前端文章

    • JavaScript
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 《Vue》
    • 《React》
    • 《Git》
    • JS设计模式总结
  • HTML
  • CSS
  • 技术文档
  • GitHub技巧
  • 学习
  • 实用技巧
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • React

  • Vue

  • JavaScript文章

  • 学习笔记

  • openlayers

  • threejs

  • MapboxGL

  • 工具

  • 源码合集

    • Pinia源码浅析
      • 《Vue3源码》笔记
      • vue-router源码浅析
      • pinia-plugin-persistedstate源码浅析
      • 《Openlayers源码》笔记
      • 《Leaflet源码》笔记
    • 前端
    • 源码合集
    东流
    2024-04-26
    目录

    Pinia源码浅析

    # Pinia 源码浅析

    # 概述

    Pinia 中只用到了 vue-demi 一种库,vue-demi 的介绍可以参考 vue-demi。

    Pinia可以在 vue2 和 vue3 中用于数据或状态的管理,同时 pinia 还提供了极其丰富的浏览器调试插件工具,更多详细内容可以参考 Pinia 官网 (opens new window)。

    # vue2 中使用 Pinia

    pinia提供了PiniaVuePlugin,在 vue2 中需要手动注册

    import Vue from "vue";
    import { PiniaVuePlugin, createPinia } from "pinia";
    
    Vue.use(PiniaVuePlugin);
    const pinia = createPinia();
    
    new Vue({
      el: "#app",
      // ...
      pinia,
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # PiniaVuePlugin 原理

    PiniaVuePlugin是一个函数,注册时会被作为 install 方法,接受传入的参数 Vue,然后再通过Vue.mixin全局混入breforeCreate和destroyed这两个生命周期钩子函数,影响到每一个 Vue 实例。

    # breforeCreate

    在这个钩子中 将选项中的 pinia 实例挂载到 vue 上。

      beforeCreate() {
       ...
        this._provided[piniaSymbol]=pinia
        this.$pinia = pinia;
        pinia._a=this //将vue挂载到pinia上,方便于pinia注册插件
       ...
      }
    
    1
    2
    3
    4
    5
    6
    7
    # destroyed

    在销毁组件时,将组件实例从 pinia 实例中移除。

    # vue3 中使用 Pinia

    vue3 中使用 Pinia 需要先调用createPinia创建 Pinia 实例,返回 pinia 对象,再手动注册 Pinia 实例。

    # createPinia 创建 Pinia 实例

    createPinia函数是Pinia的核心函数,它返回一个Pinia实例,该实例包含install方法,用于安装Pinia插件,以及use方法,用于注册插件,比如数据持久化插件piniaPluginPersistedstate

    createPinia首先通过vueDemi.effectScope创建一个独立的作用域scope,再通过scope.run方法返回一个vueDemi.ref({}) ref 对象作为state

    由上可知 vue2 中使用Pinia也会调用createPinia方法创建实例,因此在install属性方法中 Pinia 判断了当前环境是否是 vue2,如果不是,则执行app.config.globalProperties.$pinia = pinia;将 pinia 实例挂载到 vue 实例上

     export function createPinia() {
         const pinia =vueDemi.markRaw({
             install:(app)=>{/*...*/},
             use:(plugin)=>{/*...*/},
             _p,// 需要通过pinia.use的插件集合
             _a:null // 指向vue
             _e:scope,//独立作用域
             _s:new Map(),
             state, // 全局ref对象
         }
         return pinia
     }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # defineStore 定义 Store

    defineStore 定义 store,接受一个唯一的 id、setup和 配置项setupOptions,其中定义并返回了useStore函数,

    如果 Pinia 中没有定义了同样的id,就判断参数setup是否是一个函数,如果是函数,则调用createSetupStore,否则调用createOptionsStore创建 store。 如果 Pinia 中已经定义了同名id的 store,否则通过id获取 store 并返回。

    # createSetupStore

    createSetupStore 函数是 Pinia 中的核心部分, 接受 6 个参数,分别是id,setup,options,pinia,hot,isOptionsStore。createSetupStore函数返回一个 store,这个 store 是一个对象,包含$id,$state,$patch,$reset,$subscribe,$subscribeWith,$dispose等方法。

    # createOptionsStore

    # storeToRefs

    storeToRefs接受一个参数store,作用是从store中解构出响应式数据。

    pinia 内部实现storeToRefs的逻辑是先判断当前环境是否是 vue2, 如果是 vu2,则通过vueDemi.toRef工具函数将 store 转为响应式数据,然后返回; 如果当前是 vue3,则分为两步:

    1. 定义一个空对象 refs,通过vueDemi.toRaw将 store 转为普通对象并返回
    2. 循环遍历第 1 步中的对象,通过vueDemi.isRef和vueDemi.isReactive判断其值是否是响应式数据,如果是,则通过vueDemi.toRef将响应式数据转为普通数据,然后返回;
    3. 返回 refs

    # mapStores

    mapStores 允许在不使用组合式 API (setup()) 的情况下使用存储(stores),通过生成一个对象来在组件的computed 字段中进行扩展。它接受多个 store 实例,并返回一个对象,其中每个属性都对应一个 store 实例。我们可以通过 store 的 id+Store 来访问

    用法示例

    export default {
      computed: {
        ...mapStores(useUserStore, useCartStore),
      },
      created() {
        this.userStore.fetchUser(); //eg store with id "user"
        this.cartStore.fetchCart(); //eg store with id "cart"
      },
    };
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    mapStores 的实现很简单,遍历参数,然后返回一个对象,key 是每一个 store 的 id 加上后缀Store,value 是 store。后缀Store是 Pinia 默认的,我们也可以通过 Pinia 暴露的setMapStoreSuffix方法设置这个后缀。

    function mapStores(...stores) {
      return stores.reduce((reduced, useStore) => {
        // @ts-expect-error: $id is added by defineStore
        reduced[useStore.$id + mapStoreSuffix] = function () {
          return useStore(this.$pinia);
        };
        return reduced;
      }, {});
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    # mapState

    mapState,允许在不适用组合式 API 的情况下使用 state,接受两个参数,第一个参数是 store,第二个参数是 store 的 key。key 可以是字符串数组也可以是对象,如果是对象,对象的 key 是函数的情况下,则可能在 ts 中使用时报错,因为由于某种原因,TS 无法将 storeKey 的类型推断为函数。

    mapState等价于mapGetters,不过是mapGetters被 deprecated 弃用了。

    用法示例如下

    /*
    state() => {
      return {
        count: 0,
        message:"hello"
      }
    }
    */
    
    export default defineComponent({
      computed: {
        ...mapState(useCounterStore(), ["count", "message"]),
      },
      created() {
        console.log(this.count, this.mesaage);
      },
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    源码如下

    function mapState(useStore, keysOrMapper) {
      return Array.isArray(keysOrMapper)
        ? keysOrMapper.reduce((reduced, key) => {
            reduced[key] = function () {
              return useStore(this.$pinia)[key];
            };
            return reduced;
          }, {})
        : Object.keys(keysOrMapper).reduce((reduced, key) => {
            // @ts-expect-error
            reduced[key] = function () {
              const store = useStore(this.$pinia);
              const storeKey = keysOrMapper[key];
              // for some reason TS is unable to infer the type of storeKey to be a
              // function
              return typeof storeKey === "function"
                ? storeKey.call(this, store)
                : store[storeKey];
            };
            return reduced;
          }, {});
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    # mapWriteableState

    mapWriteableState允许在不使用组合式 API 的情况下,修改设置 state。因为 state 返回的是一个对象,可以利用重写对象的set设置其值 其内部实现如下

    function mapWritableState(useStore, keysOrMapper) {
      return Array.isArray(keysOrMapper)
        ? keysOrMapper.reduce((reduced, key) => {
            // @ts-ignore
            reduced[key] = {
              get() {
                return useStore(this.$pinia)[key];
              },
              set(value) {
                // it's easier to type it here as any
                return (useStore(this.$pinia)[key] = value);
              },
            };
            return reduced;
          }, {})
        : Object.keys(keysOrMapper).reduce((reduced, key) => {
            // @ts-ignore
            reduced[key] = {
              get() {
                return useStore(this.$pinia)[keysOrMapper[key]];
              },
              set(value) {
                // it's easier to type it here as any
                return (useStore(this.$pinia)[keysOrMapper[key]] = value);
              },
            };
            return reduced;
          }, {});
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29

    # mapActions

    同上,通过mapActions可以访问使用到 store 实例的 action 方法。mapActions内部实现和mapState逻辑大同小异。

    使用示例如下

    export default {
      methods: {
        ...mapActions(useCounterStore, ["increment"]),
      },
    };
    
    1
    2
    3
    4
    5

    # skipHydrate和shouldHydrate

    skipHydrate函数用于在热更新时跳过 hydrate 阶段。hydrate,即水合,在 vue3 中,hydrate是一个用于将 VNode 树转化为真实 DOM 的过程,它尝试复用服务器端渲染(SSR)生成现有 DOM 结构,这个过程是在客户端启动时进行的,目的是加快页面的渲染速度,减少首屏加载时间。

    内部实现原理是首先判断当前环境,如果是 vue3,则给对象加一个skipHydrateSymbol属性,反之给skipHydrateMap追加一个键值对[obj]:1,这样通过shouldHydrate判断是否需要刷新该对象。

    而shouldHydrate在通过createSetupStore创建 store 时会调用,判断是否需要进行水合

    # acceptHMRUpdate 热模块替换函数

    acceptHMRUpdate函数接受两个参数,第一个参数是store,第二个参数是hot,即import.meta.hot,acceptHMRUpdate函数返回一个函数,这个函数接受一个参数,这个参数是store,这个函数会判断store是否是import.meta.hot的module.$id,如果是,则返回store,否则返回null。

    内部实现原理:

    • 订阅 HMR 更新:Pinia 在创建 store 实例时,会订阅 webpack 的 HMR API,以便在更新时接受到通知。

    • 保存先前的状态:在接受到 HMR 更新之前,Pinia 会首先保存当前的状态。

    • 应用更新:当收到 HMR 更新通知时,Pinia 会调用 acceptHMRUpdate 方法。

    • 恢复状态:在应用更新之前,Pinia 会将状态回滚到先前保存的状态,以确保状态不会因为更新而丢失。

    • 应用更新:Pinia 会应用收到的更新,例如添加、删除或修改状态。

    • 重新应用状态:更新应用后,Pinia 会再次应用保存的状态,以确保在更新过程中不丢失任何状态。

    通过这种方式,Pinia 可以安全地在开发过程中接受并应用 HMR 更新,从而保持应用程序的状态与代码的一致性,而不需要刷新整个页面。

    使用示例如下:

    const useUser = defineStore(...)
    if (import.meta.hot) {
       import.meta.hot.accept(acceptHMRUpdate(useUser, import.meta.hot))
     }
    
    1
    2
    3
    4
    编辑 (opens new window)
    上次更新: 2025/04/09, 10:15:29
    pythontutor网站推荐
    《Vue3源码》笔记

    ← pythontutor网站推荐 《Vue3源码》笔记→

    最近更新
    01
    GeoJSON
    05-08
    02
    Circle
    04-15
    03
    CircleMarker
    04-15
    更多文章>
    Theme by Vdoing | Copyright © 2024-2025 东流 | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式