pinia 源码解析

概述

  • 上一篇我们已经大致了解了 pinia 的相关使用方法 以及 API 和如何使用 pinia 的插件, 这一次我们来看一下 pinia 在源码上是如何处理的, 这里我拆分出来了源码中的关键方法,将源码中 对 vue2 和 vue3 的考虑 ,开发模式 和 生产模式 的考虑, 热更新,CSR 和 SSR 以及插件 和 API 的注入等 诸多情况进行了删减,这里我们只考虑 vue3的情况, 把基础架构搭建了一下。

  • 目的: 为更好的理解 pinia 底层是如何实现的,通过拆分源码,加深对响应式系统的数据结构设计的原理,以及拓宽思路。

  • 实现:能够实现 state ,getters, 以及 actions 的响应式, 在末尾会贴出简化后的 60行代码,可以自己 用 vite 搭建一个 vue 的项目试着搞个 demo 看看。

此源码也引用了一起论坛上其他大神的诸多见解,在文章末尾会把链接贴出来,以供大家参考

写在之前

  • 依赖
    • vue-demi: vue 的很多库 在编写的过程中 会依赖于 ‘vue-demi’ 这个库, 这个库中暴露了 很多用于提供给插件编辑者的便利方法,很多方法开发者在开发的过程中是使用不上的,也很少见,我在引用的时候,也很难查找到很多对应方法的资料或者 demo,对一些方法的理解比较浅显,希望有过经验的前辈可以多多指教。
    • vue/devtools-api: pinia 同时还引入了 vue 开发者工具库, 这个库的引入,可以让我们在浏览器的 vue 插件中 能够查看到 pinia store 的状态,这次我们在源码拆分的过程中没有把这个库加进去。
  • 环境
    • process.env.NODE_ENV : pinia 对生产环境 和 开发环境做了区分, 就像 vue 中 对生产环境 和开发环境 给我们的 提示是不一样的一个道理,这里使用 process.env.NODE_ENV === production 进行的判断,同样区分的代码量是很大的,区分以后要做很多处理,这里我们的源码也只针对开发环境。
  • 结构
    • 在结构上面,和其他的包是一样的,dist 下 包含了几种不同的文件, 如 cjs,iife,mjs ,esm-browser针对用不同的模块化规范,以及生产环境和开发环境的版本,我们这里引用和查看的主要是 源码中的 esm-browser 版本

拆分

引用

  • 在正式贴代码之前, 先分享一下思路, 这里我们使用插件的时候, 最先考虑的是这个插件实现的功能,插件的功能自然都是挂载在其实例上,原理也是比较简单,就是给 vue 的 app 实例全局 provide 进去,这样我们所有子模块便都能够访问到该实例。
  • 这里我使用了几块进行处理, 三部分,第一部分是全局引用,第二部分是 createPinia, 第三部分是 defineStore。

全局引用

// 计算属性得到的响应式对象是 ComputedRef,其原型上包含 effect 方法, 是通过 computed 注入的
function isComputed(o) {
  return !!(isRef(o) && o.effect);
}
// markRaw 作用: 标记一个对象,使其永远不会再成为响应式对象
// effectScope 作用: 返回一个作用域(副作用生效的作用域),使用 run 方法收集响应式依赖,使用 stop 方法终止所有的副作用, 目的就是更方便的管理副作用的收集和销毁

let activePinia; // 主要是针对 ssr 的情况下 才会使用

// Sets or unsets the active pinia.   Used in SSR and internally when calling actions and getters ==> 针对 ssr
const setActivePinia = (pinia) => (activePinia = pinia)
// 首次 ssr 加载的时候 没有pinia 的情况下 注入
const getActivePinia = () => (getCurrentInstance() && inject(piniaSymbol)) || activePinia;
// 使用 Symbol 唯一值标记 pinia 实例
const piniaSymbol = (process.env.NODE_ENV !== 'production') ? Symbol('pinia') : Symbol();

createPinia

// 注册插件
function createPinia(){
  const scope = effectScope(true)
  // state 实际上就是根store 在组件使用时 使用 defineStore 来定义组件的 store, 实际上就是向这里 做 add 操作 是一个 reactive,在使用的时候 通过 id 拿到对应的 state
  const state = scope.run(()=>ref({}))
  // _p  用来缓存插件 会在创建 store 时 遍历缓存数组, 执行其中的插件方法
  let _p = []
  console.log('pinia插件注入',state,'+++',scope);
  const pinia = markRaw({
    // 插件使用 install 作为入口
    install(app){
      pinia._a = app
      app.provide(piniaSymbol,pinia)
    },
    // 使用其他插件 插件的调用,需要早 app.use(pinia) 之前, 这里也就能够看到为什么了。
    use(plugin){
      _p.push(plugin);
      return this
    },
    state,
    _p,
    _s: new Map(), // 存储每一个 store 的映射
    _e: scope,
    _a: null
  })
  return pinia
}

defineStore

/**
 * 
 * @param {*} idOptions  store id 一般为 string 标识本 store
 * @param {*} setup 配置项 { state,getters,actions...}
 * @param {*} setupOptions 
 */
function defineStore(idOptions,setup,setupOptions){
  let id;
  let options;
  // 获取配置项类型 当第二个参数传入的是函数的时候,则使用第三个参数为配置, 当只传入一个参数的时候, 则全部配置都包含在这个对象中
  const isSetupStore = typeof setup === 'function';
  // 获取 id 唯一值的类型 如果不是 string, 则获取 idOptions 中的 id
  if(typeof idOptions === 'string'){
    id = idOptions;
    options = isSetupStore ? setupOptions : setup
  } else {
    options = idOptions;
    id = idOptions.id
  }

  function useStore(pinia){
    // 获取当前实例
    const currentInstance = getCurrentInstance();
    // 通过 app 实例, 使用 inject 方法 引入 pinia
    pinia = currentInstance && inject(piniaSymbol)
    // 设置 pinia 没有实例的时候会走这里 设置当前活跃的是哪个pinia实例,当有多个pinia实例时,方便获取当前活跃的pinia实例 
    if(pinia) setActivePinia(pinia);
    pinia = getActivePinia()
    if(!pinia._s.has(id)){
      // 第一次 map 上没有id 这里进行初始化, 挂载 store
      createOptionsStore(id,options,pinia)
    }
    // 读取 根 store
    const store = pinia._s.get(id);
    console.log('store',store,pinia);
    return store
  };
  useStore.$id = id;
  return useStore
}

function createOptionsStore(id,options,pinia){
  const { state, actions, getters } = options;
  // 初始化 state 存储, 第一次的时候 是 undefined
  const initState = pinia.state.value[id]
  let store;
  //! 关键方法,createSetupStore 方法中调用这个方法, 拿到所有需要配置的 store 里面的 key, 进行针对性配置
  function setup(){
    if(!initState){
      // 添加 state
      pinia.state.value[id] = state ? state() : {};
    }
    // 通过一个新的 对象 避免直接操作 pinia.state.value , 使用 toRefs 展开引用
    const localState = toRefs(pinia.state.value[id]);
    // 合并 action
    return Object.assign(localState,actions,Object.keys(getters || {}).reduce((computedGetters,name)=>{
      computedGetters[name] = markRaw(computed(()=>{
        setActivePinia(pinia)
        const store = pinia._s.get(id)
        // 这里直接使用了 getters 中的方法, 返回一个改变了 this指向的原函数,并且把 store 实例传给了这个方法, 故在 getters 中可以接收一个参数就是 store, 并且不可以使用箭头函数
        return getters[name].call(store,store)
      }))
      return computedGetters
    },{}));
  }

  store = createSetupStore(id,setup,options,pinia)
    /* store.$reset = function $reset(){
    const newState = state ? state() : {};
    this.$patch(($state) => {
      assign($state,newState)
    })
  } */
  return store
}

function createSetupStore($id,setup,options,pinia){
  let scope
  // 获取配置项汇中的 state 是函数
  const buildState = options.state;
  // 获取 pinia 中当前的 store 第一次的时候是 undefined
  const initialState = pinia.state.value[$id];
  // 进行作用域包裹,获取需要处理的 store 中的 配置
  const setupStore = pinia._e.run(()=>{
    scope = effectScope();
    return scope.run(()=>setup())
  })
  // 装饰api
  const partialStore = {
    _p:pinia,
    $id
  }

  function wrapAction(name,fn){
    return function(){
      setActivePinia(pinia);
      console.log('函数执行了',this);
      const arg = Array.from(arguments);
      let ret
      try{
        console.log(this,'-------',store);
        ret = fn.apply(store,arg)
      }catch(err){
        console.log('err',err);
      }
      return ret
    }
  }

  // store 在这里 被初始化为 reactive
  const store = reactive(partialStore)
  pinia._s.set($id,store)
  // 遍历配置 进行处理
  for(const key in setupStore){
    const prop = setupStore[key]
    console.log('prop',key,'---',prop,isRef(prop),isComputed(prop),isReactive(prop));
    // 判断是否是 ref 的类型, 且是 getters 的类型, 这里处理选项中 getters
    if(isRef(prop) && !isComputed(prop)){
      //TODO 如果 state 没有值 进行处理 逻辑暂时没有细究
      if(!buildState){
        // 第一次 initialState 是 false 
        if(initialState){
          if(isRef(prop)){
            prop.value = initialState[key]
          }
        }else{
          pinia.state.value[$id][key] = prop;
        }
      }
    }else if (typeof prop === 'function'){
      //TODO 处理 actions 使用 源码中的wrapAction  key 是函数名  prop 是函数体
      setupStore[key] = wrapAction(key,prop)
      console.log('function',prop,key);
    }

  }
  //! 关键步骤, 改变 store 结构,将初始化的 store 平铺到源 store 上
  Object.assign(store,setupStore)
  Object.assign(toRaw(store),setupStore)
  Object.defineProperty(store,'_p',{
    enumerable:false
  })
  return store
}

精简源码

  • 可以直接在 demo 中使用, 使用方法就同 pinia相同,测试对应的响应式
    import { getCurrentInstance, inject, markRaw, effectScope, ref, reactive, computed, toRefs } from 'vue-demi';
const piniaSymbol = Symbol('pinia')
function createPinia(){
  const scope = effectScope(true)
  const state = scope.run(()=>ref({}))
  const  pinia = markRaw({
    install(app){
      pinia._a = app
      app.provide(piniaSymbol,pinia)
    },
    _s: new Map(), 
    state
  })
  return pinia
}
function createOptionsStore(id,options,pinia){
  const { state, getters, actions } = options;
  const initState = pinia.state.value[id]
  let store;
  function setup(){
    if(!initState) pinia.state.value[id] = state()
    const localState = toRefs(pinia.state.value[id])
    return Object.assign(localState,actions,Object.keys(getters).reduce((prev,name)=>{
      prev[name] = markRaw(computed(()=>{
        const store = pinia._s.get(id)
        return getters[name].call(store,store)
      }))
      return prev
    },{}))
  }
  store = reactive({
    _p:pinia,
    $id:id,
  })
  pinia._s.set(id,store)
  const setupStore = setup()
  for(const key in setupStore){
    //TODO 处理 actions 和 getters 
    const prop = setupStore[key]
  }
  Object.defineProperty(store,'_p',{
    enumerable:false
  })
  Object.assign(store,setupStore)
  return store
}
function defineStore(id,options){
  function useStore(){
    const currentInstance = getCurrentInstance()
    const pinia =  currentInstance && inject(piniaSymbol)
    if(!pinia._s.get(id)){
      createOptionsStore(id,options,pinia)
    }
    const store = pinia._s.get(id)
    return store
  }
  useStore.$id = id
  return useStore
}
export {
  createPinia,
  defineStore
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值