【手写源码】解析Pinia原理

大致原理

库的入口文件index.ts

主要就是两个方法

export { createPinia } from "./createPinia"
export {defineStore} from './defineStore'
createPinia

主要就是为了返回一个带install方法的对象,然后install的时候向全局暴露出pinia这个对象(这个对象不管在vue3中还是vue2中还是在其他不是vue的组件中都能使用),这个对象里有一个存放了所有store的_s属性,有用来停止所有state的响应式的_e属性,还有一个存放所有state 的state属性,还有插件列表和提供给外界注册插件的函数。

import {ref,effectScope} from 'vue'
import { piniaSymbol } from './piniaSymbol'
export let activePinia //通过这个全局变量来存放piniaStore,为了在不是vue组件中也能访问到piniaStore(比如router)。
export const setActivePinia = (piniaStore) => activePinia = piniaStore
export function createPinia() { //是一个插件方法,返回一个带install方法的对象。
  const scope = effectScope(true) // 创建一个依赖函数集,到时候方便一起暂停他们的响应式。
  const state = scope.run(()=>ref({})) //存放所有state。通过scope.run去包一层方便后续通过scope.stop()这个方法来全部暂停这些的响应式。
  const _plugins = [] //存放所有的插件(其实就是函数)
  const piniaStore = {
    use(plugin) { // 提供给外界用于注册插件
      _plugins.push(plugin)
      return this //返回this方便链式调用
    },
    _plugins,
    _stores: new Map(), //存放所有的store
    _e:scope, //用来停止所有state的响应式。(实际上pinia并没有提供停止所有 响应式的方法,但是我们可以在pinia中可以使用store1._p._e.stop()来终止所有effect,但当然pinia是不推荐这样做的(注:store1是指一个store实例))
    state,
    install(app) {
      setActivePinia(piniaStore) //将这个piniaStore暴露到全局上,为了在不是vue组件中也能访问到piniaStore(比如router)。
      app.provide(piniaSymbol, piniaStore) //这样就能让vue3的所有组件都可以通过app.inject(piniaSymbol)访问到piniaStore
      app.config.globalProperties.$pinia = piniaStore //这样就能让vue2的组件实例也可以共享piniaStore
    }
  }
  return piniaStore
}
defineStore

defineStore接受三种传参方式: (感觉传入options就是为了迎合vue2的写法)

  • 第一种是传入id + options。
  • 第二种是只传入options(id也包含在这里面)
  • 第三种是传入id + setup函数

所以进来这个函数第一件事就是处理第一个参数idOrOptions,把id和options分别提取出来。

然后返回一个useStore函数,这个函数里会获取整个pinia实例(即上面createPinia暴露出全局的对象),然后看里面有没有目前使用的store,没有就创建,然后把这个store返回给用户。(所以创建store是在use的时候完成的而不是define的时候)。

import {piniaSymbol} from './piniaSymbol'
import {getCurrentInstance,inject,reactive,effectScope,isRef,isReactive} from 'vue'
import { activePinia, setActivePinia } from './createPinia'

/**
 * defineStore接受三种传参方式: (感觉传入options就是为了迎合vue2的写法)
 * 第一种是传入id + options。
 * 第二种是只传入options(id也包含在这里面)
 * 第三种是传入id + setup函数
 */

export function defineStore(idOrOptions, optionsOrSetup{
  let id, options
  //处理第一个参数 idOrOptions
  if (typeof idOrOptions === 'string') { //是第一种传参方式
    id = idOrOptions
    options = optionsOrSetup
  } else { // 是第二种传参方式
    options = idOrOptions
    id = options.id
  }

  function useStore({
    const instance = getCurrentInstance() // 获得当前组件实例
    let piniaStore = instance && inject(piniaSymbol) //如果当前组件实例存在就注入整个piniaStore(因为只有在vue组件里才能使用inject)
    if (piniaStore) {
      setActivePinia(piniaStore)
    }
    piniaStore = activePinia //这样就可以哪怕不是在vue组件中使用也能拿到整个piniaStore(比如在router中使用)
    if (!piniaStore._stores.has(id)) { //如果是还没有这个store(即第一次使用这个useStore),那就创建这个store。!!在use的时候才会创建这个store!!
      if (typeof optionsOrSetup === 'function') { //传进来一个setup函数 ,是第三种传参方式
        createSetupStore(id, optionsOrSetup,piniaStore)
      } else { //前两种传参方式都用这个来构建store
        createOptionsStore(id,options,piniaStore)
      }
    }
    return piniaStore._stores.get(id) //获得目前use的这个store
  }

  return useStore //用户使用这个函数就能获得这个store
}
createSetupStore和createOptionsStore

创建store有两种方式(一种是用户在defineStore里传入了options,一种是用户在defineStore里传入了setup),但其实最后都是靠createSetupStore来创建store,createOptionsStore只不过是把参数封装成setup的形式然后再传给createSetupStore。

  • 如果是createSetupStore这种,则有定义一个store用于存放 不是用户定义的属性和方法以及内置的api,定义一个scope到时候可以停止自己store的响应式。然后对store里的所有属性进行遍历,继而对actions即setup里的函数进行一层包装(比如将原函数的this绑定到store上,还有如果原函数是异步函数的处理),把每个state都也存放到全局的state里。最后将存放 内置的api的store与用户define的store进行合并然后存放到全局pinia的 _s属性里。
  • 如果是createOptionsStore这种,则先将state, actions, getters从options中提取出来,然后写成setup函数的形式(state要放到pinia的state属性里,getters要把里面的函数变成计算属性,并将处理后的state、actions、getters合并再导出。)然后把这个setup函数传给上面的createSetupStore来构建store。然后再给这个store附上个 $reset方法(这个方法只有options定义的store才有)。
function isComputed(v// 计算属性是ref,同时也是一个effect
  return !!(isRef(v)&&v.effect)
}

/**defineStore传入了setup函数时调用这个函数
 * id 表示store的id
 * setup表示setup函数
 * piniaStore表示整个pinia的store
 * isOption表示用户是否用option语法define的store
*/

function createSetupStore(id, setup, piniaStore,isOption{
  let scope
  function $patch(){}
  const partialStore = {//内置的api存放到这个store里
    $patch
  }
  const store = reactive(partialStore) //store就是一个响应式对象,这个是最后暴露出去的store,会存放内置的api和用户定义的store

  if (!piniaStore.state.value[id] && !isOption) { // 整个pinia的store里还没有存放过目前这个state 且 用户用options语法来define的store
    piniaStore.state.value[id] = {}
  }

  //这个函数就是为了到时候方便停止响应式。(核心的创建store可以不要这部分代码)
  const setupStore = piniaStore._e.run(() => { //这样包一层就可以到时候通过pinia.store.stop()来停止全部store的响应式
    scope = effectScope()
    return scope.run(()=>setup()) //这样包一层就可以到时候通过scope.stop()来停止这个store的响应式
  })

  //遍历这个store里的所有属性,做进一步处理
  for (let key in setupStore) {
    const prop = setupStore[key]

     //处理action
    if (typeof prop == 'function') {
      setupStore[key] = wrapAction(key, prop)
    }

    //处理state
    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) { //如果他是ref或者是reactive则说明它是state(注意由于computed也是ref,所以要排除掉计算属性)
      if (!isOption) { //如果是setup语法,把里面的state也存到全局的state里
        piniaStore.state.value[id][key] = prop
      }
    }
  }

 /**对actions包一层,做一些处理。store里面存的actions实际都是经过了这个包装的actions。*/
  function wrapAction(name, action{
    return function ({
      let ret = action.apply(store, arguments//使this永远指向store

      //action执行后可能是一个promise,todo......

      return ret
    }
  }

  // 把不是用户定义的和是用户定义的都合并到store里,并给外面使用
  Object.assign(store,setupStore)
  piniaStore._stores.set(id, store)//将这个store存到piniaStore中
  return store
}


/**defineStore传入了options时调用这个函数 (感觉传入options就是为了迎合vue2的写法)*/
function createOptionsStore(id, options,piniaStore{
  const { state, actions, getters } = options

  function setup(//处理store里的state、actions、getters
    piniaStore.state.value[id] = state ? state() : {} //把这个store的state存到piniaStore里
    const localState = toRefs(piniaStore.state.value[id]) //把这个store的state转换成ref即变成响应式,因为options写法里的state并不是响应式的。
    return Object.assign( //这里返回的对象就是用户存放用户定义的属性和方法
      localState, //用户的state
      actions, // 用户的actions
      Object.keys(getters || {}).reduce((memo, name) => { //用户的getters,因为用户的getters这个对象里的属性都是函数,所以我要把这些函数都执行了变成计算属性
        memo[name] = computed(() => {
          let store = pinia._stores.get(id)
          return getters[name].call(store)
        })//call是为了保证this指向store
    },{}))
  }

  const store = createSetupStore(id, setup, piniaStore, true)

  store.$reset = function ({
    const rawState = state ? state() : {}
    store.$patch(state => {
      Object.assign(state,rawState)
    })
  }
}
store内置的api

这些api其实是在上面createSetupStore函数里编写的(除了$reset方法只有options定义的store才有,所以就写在上面createOptionsStore里了),这里抽离出来方便观看。

$patch
function createSetupStore(id, setup, piniaStore,isOption{
 //...其他代码...
  
  /**此函数用于批量修改state,有两种传参方式:一种是传入一个对象,这个对象里有部分或全部state;另一种是传入一个函数,这个函数的参数是state,函数体对state进行修改 */
  function $patch(partialStateOrMutator{
    if (typeof partialStateOrMutator === 'object') {
      mergeReactiveObject(pinia.state.value[id],partialStateOrMutator) //递归合并两个对象
    } else { //partialStateOrMutator是一个function
      partialStateOrMutator(pinia.state.value[id])
    }
  }

 //...其他代码...
}

function mergeReactiveObject(target, state//递归合并两个对象
  for (let key in state) {
    let oldValue = target[key];
    let newValue = state[key];
    if (oldValue && newValue && oldValue.constructor === Object && newValue.constructor === Object) { // 两个都是对象
      target[key] = mergeReactiveObject(oldValue, newValue);
    } else {
      target[key] = newValue;
    }
  }
  return target;
}
$subscribe
function createSetupStore(id, setup, piniaStore,isOption{
 //...其他代码...
 
 /**此函数用于在state发生变化的时候执行个函数,原理就是利用vue提供的watch去监听state变化。(套娃)
   * callback: 在state变化时要执行的函数。这个callback的参数是store的id和state。
   * options: 就是vue里watch需要的options参数
  */

  function $subscribe(callback, options = {}{
    scope.run(() => watch(piniaStore.state.value[id], state => { //scope.run包一层纯粹就是为了到时候便于停止响应式,没有其他任何实际作用。
      callback({storeId:id},state)
    },options))
  }
 //...其他代码...
}
$onAction
function createSetupStore(id, setup, piniaStore,isOption{
 //...其他代码...

 const actionSubscribtions = []//存放action执行之前的订阅函数
  /**此函数用于订阅一个函数在触发action之前或action执行之后或action发生错误的时候执行。参数是一个callback,这个callback里有after或onError的函数参数,整个callback会在action执行之前执行。 */
  function $onAction(callback{
    addSubscribtion.bind(null,actionSubscribtions)(callback)
  }
  
// 为了能触发订阅的函数 对createSetupStore里的wrapAction进行补充:
  /**对actions包一层,做一些处理。store里面存的actions实际都是经过了这个包装的actions。*/
  function wrapAction(name, action{
    return function ({

      // 存放action的之后和发生错误后的订阅函数
      const afterCallbackList = []
      const onErrorCallbackList = []
      function after(callback{
        afterCallbackList.push(callback)
      }
      function onError(callback{
        onErrorCallbackList.push(callback)
      }

      triggerSubscribtions(actionSubscribtions, { after, onError }) //触发action执行前的订阅函数
      let ret
      try {
        ret = action.apply(store, arguments//使this永远指向store 并执行action!!!
      } catch(e) {
        triggerSubscribtions(onErrorCallbackList, e) //触发action执行错误后的订阅
      }

      if (ret instanceof Promise) { //如果action是promise
        return ret.then(value => {
          return triggerSubscribtions(afterCallbackList,value)
        }).catch(e => {
          triggerSubscribtions(onErrorCallbackList,e)
          return Promise.reject(e)
        })
      }
      triggerSubscribtions(afterCallbackList, ret) //触发action执行后的订阅

      return ret
    }
  }
  
 //...其他代码...
}
$dispose
function createSetupStore(id, setup, piniaStore,isOption{
 //...其他代码...


  /**此函数用于Stops the associated effect scope of the store and remove it from the store registry. */
    function $dispose({
      scope.stop() //清除响应式
      actionSubscribtions = [] //取消订阅
      piniaStore._stores.delete(id) //清除这个store
    }
 //...其他代码...
}
$state属性
function createSetupStore(id, setup, piniaStore,isOption{
 //...其他代码...
  
  Object.defineProperties(store, '$state', { //给store添加一个属性$state。Setting it will replace the whole state.
      get() => pinia.state.value[id],
      set:state=>$patch($state=>Object.assign($state,state))
    })
 //...其他代码...
}
插件机制

所有插件都会存到总的pinia上,详情见createPinia

每次创建store都会调用插件方法。

function createSetupStore(id, setup, piniaStore,isOption{
 //...其他代码... 
 
   pinia._plugins.forEach(plugin => {
      Object.assign(store,scope.run(()=>plugin({store}))) //Object.assign是为了让插件的返回值作为store的属性
    })
 
 //...其他代码...
}
storeToRefs
import { toRaw, toRef, isRef ,isReactive} from "vue"
export function storeToRefs(store// 作用是跟toRefs一样的,只不过toRefs会处理函数的情况,于是pinia就写一个只处理响应式对象的。原理就是使用toRef。
  store = toRaw(store)
  const refs = {}
  for (let key in store) {
    const value = store[key]
    if (isRef(value)||isReactive(value)) {
      refs[key] = toRef(store,key)
    }
  }
  return refs
}

本文由 mdnice 多平台发布

你可能感兴趣的:(前端)