Pinia源码解析【2.0.33】

lxf2023-05-05 08:22:02

往期文章:

  • Vue3.2x源码解析(一):vue初始化过程
  • Vue3.2x源码解析(二):组件初始化
  • Vue3.2x源码解析(三):深入响应式原理
  • Vue3.2x源码解析(四):异步更新队列

pinia是 Vue 的专属状态管理库,并且可以同时支持 Vue 2 和 Vue 3。

在Vue3的项目中,我们都会优先使用Pinia,所以了解其基本的底层原理,有助于我们在项目中更好的应用。

1,createPinia

在Vue3项目中使用pinia时,都会从pinia中引入一个createPinia方法,然后使用这个方法创建一个pinia实例

import { createPinia } from 'pinia'
// 创建pinia
const pinia = createPinia()

接下来我们就从import { createPinia } from 'pinia'这句代码开始pinia源码的学习

首先查看createPinia的源码:

// packages/pinia/src/createPinia.ts# 创建pinia方法
export function createPinia(): Pinia {
  const scope = effectScope(true)
  // 初始化响应式state:默认为一个ref数据【目的是存储:所有store的state的集合】
  // RefImpl:{ value: proxy:{} }
  const state = scope.run(() => ref({}))
  
  // 插件列表
  let _p: Pinia['_p'] = []
  // 待安装插件列表
  let toBeInstalled: PiniaPlugin[] = []
​
  # 创建pinia实例对象
  // 这里用markRaw方法标记了Pinia对象,不需要被转换为响应式数据
  const pinia: Pinia = markRaw({
    # 定义的Install方法:每个vue插件都需要显示定义,在app.use时调用插件的install方法
    install(app: App) {
      // 设置为当前活跃的pinia
      setActivePinia(pinia)
      # 在Vue3应用的情况下:
      if (!isVue2) {
        // 存储Vue3 app应用实例
        pinia._a = app
        # app.provide()提供一个值,可以在应用中的所有后代组件中注入使用【重点】
        app.provide(piniaSymbol, pinia)
        // 设置全局属性$pinia:在vue应用内的每个组件都可以通过this.$pinia访问
        app.config.globalProperties.$pinia = pinia
        // 注册开发者工具
        if (USE_DEVTOOLS) {
          registerPiniaDevtools(app, pinia)
        }
        // 将插件列表添加到_p属性
        toBeInstalled.forEach((plugin) => _p.push(plugin))
        // 清空待安装插件列表
        toBeInstalled = []
      }
    },
​
    # pinia对外暴露的插件注册方法
    use(plugin) {
      if (!this._a && !isVue2) {
        // vue3: 添加到待安装插件列表:初始化时使用
        toBeInstalled.push(plugin)
      } else {
        // vue2:直接添加到_p属性
        _p.push(plugin)
      }
      return this
    },
​
    _p, // 插件列表
    _a: null, // vue3 app应用实例
    _e: scope, 
    _s: new Map<string, StoreGeneric>(), # 一个map结构,存储storeId,与store实例
    state, // 所有的state的集合:每个store初始化时,都会将处理后state数据挂载到Pinia.state.value[$id]下面
  })
​
  // 注册开发者工具插件
  if (USE_DEVTOOLS && typeof Proxy !== 'undefined') {
    pinia.use(devtoolsPlugin)
  }
​
  # 返回pinia实例对象
  return pinia
}

可以看出createPinia方法的内容比较简单,主要就是创建了一个Pinia实例并返回。pinia实例里面定义了两个方法和几个属性,

我们首先看两个方法:

  • install方法:将pinia注册到vue实例。
  • use方法:将其他插件注册到pinia实例。
const app = createApp(App)
const pinia = createPinia()
​
// 注册pinia:会在内部调用pinia的install方法
app.use(pinia)

install方法内容并不多,最主要就是将pinia实例注入到全局,让vue应用下的每个组件实例都可以使用。

app.provide(piniaSymbol, pinia)

然后重点介绍两个属性:

  • pinia._s:可以看出这个属性是一个map结构,它的作用就是存储我们在项目中定义的store实例,它是以键值对的方式进行注册pinia._s.set($id, store),它的作用是在我们多次引用useStore时,不会重复新建Store,而是直接返回已存在的对象。
  • pinia.state:这个属性被初始化为一个ref数据,它的作用是存储每个store实例中的state数据,每个store在创建时,都会将state进行处理后挂载到Pinia.state.value[$id]下面,具体的行为可以在后面createXXXStore中查看。

2,defineStore

下面我们开始阅读defineStore的源码:

// packages/pinia/src/createPinia.ts/**
 *  两个参数:
 *  参数1,store的唯一标识符;
 *  参数2,可接收两类值,Setup函数或者Options对象;【相当于使用组合式和选项式,随便用哪种都行】
 */
# 定义store方法【重点】
export function defineStore(
  idOrOptions: any,
  setup?: any,
  setupOptions?: any
): StoreDefinition {
​
  // 声明两个变量
  let id: string
  let options
​
  # 根据第二参数的类型来:判断是否使用的setupStore模式
  const isSetupStore = typeof setup === 'function'
  // 确定两个变量【赋值】
  if (typeof idOrOptions === 'string') {
    // 默认的情况:defineStore('counter', {state:()=>{},....})
    id = idOrOptions
    // 这里如果为setupStore,setupOptions就是undefined,即options为undef
    options = isSetupStore ? setupOptions : setup
  } else {
    // defineStore({'counter', state:()=>{},....})
    options = idOrOptions
    id = idOrOptions.id
  }
​
  # 定义了一个useStore方法【重点】
  // 即我们调用时的函数:const useMainStore = defineStore('main')
  function useStore(pinia?, hot?) { ... }
​
  // 设置store唯一标识符
  useStore.$id = id
​
  # 返回useStore
  return useStore
}

defineStore方法比较重要,是我们在定义Store时要经常使用的,defineStore方法只有两个参数:

  • 参数1:store实例的id,必须保证独一无二。
  • 参数2:可接收两类值,Setup函数或者Options对象

这里defineStore方法里面大部分代码都是useStore函数的内容,所以我们先把它折叠起来,后面再分析。

注意:我们在阅读开源项目源码的时候,一定要学会内容拆分,因为有的函数源码量非常大,有的能达到几百行甚至上千行【比如Vue3源码baseCreateRenderer基础渲染器函数有两千多行代码】,我们要拆分其内容,降低复杂度,然后才能更好的解析其主要逻辑。在熟悉其主要逻辑之后,我们可以进行源码调试,通过step步入每个函数内容,验证之前的逻辑分析。

当我们把useStore函数折叠后,defineStore方法的内容就很简洁明了了,首先处理传入的参数,然后定义了一个useStore方法并且返回,这里的重点是定义的useStore方法:

// 在项目中
const useMainStore = defineStore('main', {...})

这里我们在项目中声明的useMainStore函数内容:就是调用defineStore后返回的useStore方法。

下面我们继续解析useStore源码:

useStore
# Store方法
function useStore(pinia?, hot?) {
  // 获取当前组件实例
  const currentInstance = getCurrentInstance()
  # 将pinia注入到当前组件实例并使用
  pinia = currentInstance && inject(piniaSymbol, null)
  // 设置为当前活跃的pinia
  if (pinia) setActivePinia(pinia)
  pinia = activePinia!
​
  // pinia的_s属性为一个map结构,存储键值对:键为storeId,值为store实例
  # 从map结构中查询该storeId,如果不存在,则执行创建逻辑
  if (!pinia._s.has(id)) {
    // creating the store registers it in `pinia._s`
    # 新建一个Store实例,并且将它添加到pinia._s属性中
    // 根据之前的isSetupStore变量值,划分为两种模式创建逻辑:
    if (isSetupStore) {
      // setupStore模式
      createSetupStore(id, setup, options, pinia)
    } else {
      // OptionsStore模式
      createOptionsStore(id, options as any, pinia)
    }
  }
​
  # 已存在情况:取出id对应的Store实例
  const store: StoreGeneric = pinia._s.get(id)!
​
  # 返回store实例
  return store as any
}

useStore方法一进来调用了getCurrentInstance()来获取当前的组件实例,也就是说:useStore在哪个组件中调用就会得到它的组件实例,然后在当前组件中注入pinia实例,使用pinia实例,这里拿到pinia实例是为了把即将创建的Store实例注册到Pinia._s属性中,以及将state中的数据都挂载到Pinia.state.value[$id]下面。Pinia._s属性是一个map结构,在最前面的createPinia方法有展示过【它的key是storeID,它的值是store实例】。

注意: 这里能够通过inject(piniaSymbol, null)获取到pinia,是因为在Install方法里面app.provide()已经提供了。同时我们应该知道每一个Symbol()都是唯一的,即Symbol() !== Symbol(),所以我们可以根据piniaSymbol可以获取到正确的pinia。

这里根据storeId来查询Pinia._s属性值是否存在对应的store实例:

  • 存在:则取出并直接返回store实例。
  • 不存在:则根据之前isSetupStore变量的值来决定创建store实例的逻辑。

综上所述: useStore方法主要作用就是返回Store实例供我们使用【无则创建,有则直接返回,避免重复创建】。

下面我们开始分析创建store的过程。

有两种定义Store的模式,对应着两种创建Store的方法:

# 1OptionsStore模式
export const useCounterStore = defineStore('counter', {
  state: () => {
    return { count: 0 }
  },
  actions: {
    increment() {
      this.count++
    },
  },
})
# 2,SetupStore模式
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }
  return { count, increment }
})
createOptionsStore

我们首先查看createOptionsStore源码:

# 创建OptionsStore
function createOptionsStore(id: Id, options, pinia: Pinia, hot?: boolean){
​
  # 从options对象中取出选项参数
  const { state, actions, getters } = options
  # 获取初始state:根据storeId获取Store中的初始state数据
  // pinia.state是一个ref对象数据,这里的value刚开始为一个空对象。所以这里的initialState = undefined
  const initialState: StateTree | undefined = pinia.state.value[id]
  // 创建store变量
  let store: Store<Id, S, G, A>
  
  # 定义setup方法【重要】
  function setup() {
    // 如果不存在初始化数据,则执行state()
    if (!initialState && (!__DEV__ || !hot)) {
      if (isVue2) {
        // vue2的$set方法,给value对象新增一个storeId属性,值为state数据对象
        // 在optionsStore模式下:默认是调用state()返回初始的state对象
        set(pinia.state.value, id, state ? state() : {})
      } else {
        # 给ref数据下新增一个storeId属性,存储初始state数据
        // 调用state() 返回值就是初始的state对象
        pinia.state.value[id] = state ? state() : {}
      }
    }
​
    // avoid creating a state in pinia.state.value
    # 重点,转换成ref数据
    const localState = toRefs(pinia.state.value[id])
    
    // Object.assign() 合并成setupStore原始对象
    return assign(
      localState,
      actions,
      Object.keys(getters || {}).reduce((computedGetters, name) => {
        
        // getter
        computedGetters[name] = markRaw(
          computed(() => {
            setActivePinia(pinia)
            // it was created just before
            const store = pinia._s.get(id)!
​
            // allow cross using stores
            /* istanbul ignore next */
            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>)
    )
  }
  
  # 调用createSetupStore创建store实例
  store = createSetupStore(id, setup, options, pinia, hot, true)
​
  # 返回store实例
  return store as any
}

可以看见createOptionsStore从options选项对象中取出所需要的数据,然后定义了一个setup函数来处理相关的数据【初始化Store的方法】,最后调用了createSetupStore这个方法来创建了一个Store实例,并且返回这个实例。

综上所述: 当我们使用OptionsStore模式来定义Store时,createOptionsStore方法内部会将我们传入数据包装成一个setup函数,最终内部还是调用了createSetupStore这个方法来创建的Store实例,即将OptionsStore转换成了SetupStore类型,所以createSetupStore函数才是Pinia中创建Store的真正核心,下面我们继续深入createSetupStore方法源码。

createSetupStore

createSetupStore方法的源码非常多,有五百多行,这里我们就不展示完整代码了,只贴一些关键的逻辑代码:

# 创建SetupStore
function createSetupStore($id, setup, options, pinia, hot?, isOptionsStore?) {
    ...
    // 获取初始state:根据storeId获取Store中的初始state数据
    const initialState = pinia.state.value[$id] | undefined
    # 专门处理setupStore模式
    if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
      if (isVue2) {
        set(pinia.state.value, $id, {})
      } else {
        // 初始化为一个空对象
        pinia.state.value[$id] = {}
      }
    }
    ...
    // 定义了一些方法
    function $patch() {}
    function $reset() {} // 调用$reset()方法可以将 state 重置为初始值
    function $dispose() {}
    function wrapAction() {}
    ...
    # 定义了一个基础store:并且在它的实例上挂载了一些方法
    const partialStore = {
      _p: pinia,
      // _s: scope,
      $id,
      $onAction: addSubscription.bind(null, actionSubscriptions),
      $patch, // 修改state
      $reset, // 重置state
      $subscribe() {}, // 监听
      $dispose,
    }
    ...
    # 使用partialStore为原对象: 创建一个响应式的Store实例
    const store = reactive(partialStore)
    
    # 将store添加到pinia._s的map结构中【重点】
    pinia._s.set($id, store)
    
    // 调用setup() 生成原始setupStore对象
    const setupStore = setup();
    
    # 遍历 setupStore 属性,对其进行处理转换【重点是针对setup模式下的state数据挂载】
    for (const key in setupStore) {}
    # 将处理后的属性和方法【用户定义的】挂载到:Store实例上
    if (isVue2) {
      // vue2:通过set方法来新增响应式数据属性
      Object.keys(setupStore).forEach((key) => {
        set(store, key, setupStore[key])
      })
    } else {
      // vue3:将处理后setupStore中的内容挂载到Store实例上【state数据和actions中的方法】
      assign(store, setupStore)
    }
​
    # 定义$state访问器属性,可以通过 store.$state 直接修改状态
    Object.defineProperty(store, '$state', {
      // 访问代理
      get: () => pinia.state.value[$id],
      // 非直接修改的setter,而是在内部使用了$patch方法
      set: (state) => {
        $patch(($state) => {
          assign($state, state)
        })
      },
    })
    
    # 返回store实例对象
    return store
}

createSetupStore方法里面的内容很多,下面我们贴上一些重点的逻辑逐个解析。

(一)挂载初始state数据对象:

// 获取初始state:根据storeId获取Store中的初始state数据
const initialState = pinia.state.value[$id] | undefined
# 专门处理setupStore模式
if (!isOptionsStore && !initialState && (!__DEV__ || !hot)) {
  if (isVue2) {
    set(pinia.state.value, $id, {})
  } else {
    // 初始化为一个空对象
    pinia.state.value[$id] = {}
  }
}

其实上面这段逻辑在createOptionsStore方法中也存在,而这里是专门处理setupStore模式定义store用的,所以这里新增了一个变量isOptionsStore来做区分。

这段代码主要的作用就是pinia.state.value属性下挂载一个空的state数据对象,key为传入的StoreID。

setupStore模式下:state直接被初始化为一个空对象,因为刚开始它并不知道state会有什么内容,需要经过setup()调用,初始化里面的ref/reactive等响应式数据,处理之后才会挂载到这个空对象里面。

OptionsStore模式下:直接调用state函数,即可得到初始的state数据对象。

# 调用state()
pinia.state.value[id] = state ? state() : {}

(二)下面我们再看看$reset方法:

# OptionsStore模式下,重写了$reset方法,
const $reset = isOptionsStore ? function $reset(){} : noop

OptionsStore模式下:我们可以通过store.$reset()重置state数据对象。

而$reset方法的内容其实也很简单:

# $reset方法
function $reset(this: _StoreWithState<Id, S, G, A>) {
  // 取出state方法
  const { state } = options as DefineStoreOptions<Id, S, G, A>
  # 调用state方法:获取初始的state对象
  const newState = state ? state() : {}
  // 利用$patch方法修改state数据,
  this.$patch(($state) => {
    // Object.assign:用原始数据覆盖现在的数据
    assign($state, newState)
  })
}

setupStore模式下:**无法通过reset重置,因为这个模式下state的初始状态是通过调用setup函数得到的。如果我们调用reset重置**,因为这个模式下state的初始状态是通过调用setup函数得到的。如果我们调用reset在开发模式会给出以下警告提示,而在生产模式下不会造成任何副作用。

Store "${$id}" is built using the setup syntax and does not implement $reset()

(三)存储已经创建的store实例:

# 将store添加到pinia._s的map结构中【重点】
pinia._s.set($id, store)

在之前的useStore函数中,我们有通过pinia._s.has(id)来判断是否已存在目标Store,如果存在则直接返回Store对象。这里用一个map结构来存储已经创建好的Store实例,避免重复新建Store,因为我们可能会在多个组件中引用一个Store对象。

// 多次引用不会重复新建Store
const mainStore = useMainStore();

在Vue3的源码中也存在这样的逻辑处理,使用一个全局的map结构来存储项目中的target与Proxy对象。

(四)下面我们再看看对setupStore对象的处理:

  • 首先是原始store对象的生成:
// setupStore是最原始的store对象
const setupStore = setup();

setupStore模式下setup函数内容比较简单:

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  function increment() {
    count.value++
  }
  
  # 暴露的数据和方法
  return { count, increment }
})

调用结果就是return返回的对象。

OptionsStore模式下setup内容比较复杂,需要额外的处理:

# OptionsStore的setup初始化过程
function setup() {
  # 获取原始state数据
  // 调用state() 返回值就是初始的state对象
  if (!initialState && (!__DEV__ || !hot)) {
    if (isVue2) {
      set(pinia.state.value, id, state ? state() : {})
    } else {
     # 给ref数据下新增一个storeId属性,存储初始state数据
     // 调用state() 返回值就是初始的state数据 【普通对象】
      pinia.state.value[id] = state ? state() : {}
    }
  }
  
  // toRefs方法:返回一个普通对象,它的每个属性都是被转换后的ObjectRefImpl数据
  // localState对象下的每个属性都是被转换后的ref数据
  // { count: ObjectRefImpl{} } 添加这样的代理后,所有针对ObjectRefImpl的修改,实际都是对原对象的修改main: {count:0}
  const localState = toRefs(pinia.state.value[id])
    
  # 返回处理后的store数据对象
  // 对象合并:将【ref数据/actions下面的方法/getters中的函数】等内容都挂载到结果对象上,并返回
  return assign(
    localState, // 里面的属性都为ObjectRefImpl数据
    actions, // 里面的方法
    // 处理getters对象中定义的内容:使用computed(getter)将每个getter函数转换成计算属性
    Object.keys(getters || {}).reduce((computedGetters, name) => {})
  )
}

OptionsStore的setup方法内容比较多,主要就是为了处理state数据【将state中的内容全部转换为ref数据】和getters,最后使用对象的Object.assign()方法将state、actions、getters处理后的内容合并到一个新对象中,并且返回。

所以OptionsStore的setup方法调用后的返回值:就是我们需要的原始store对象即setupStore。

  • 循环处理原始setupStore对象中的数据。
# 循环处理
for (const key in setupStore) {
  // 获取val值
  const prop = setupStore[key]
  // 如果是ref/reactive响应式数据
  if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
    // 这里并没有处理options模式,因为已经在前面setup()中处理了
    # 专门处理setupStore模式
    if (!isOptionsStore) {
      // 将state中的数据直接挂载到state对象上
      if (isVue2) {
        set(pinia.state.value[$id], key, prop)
      } else {
        # 直接将初始的数据挂载到value[$id][key]
        // prop为ref/reactive等数据: 直接赋值【相同的引用地址】
        pinia.state.value[$id][key] = prop
      }
    }
​
  # action 方法
  // 两个模式公共的逻辑,包装actions的方法
  } else if (typeof prop === 'function') {
    // prop为函数,使用wrapAction包装处理
    const actionValue = wrapAction(key, prop)
    # 将包装后的方法重新挂载到setupStore对象
    if (isVue2) {
      set(setupStore, key, actionValue)
    } else {
      setupStore[key] = actionValue
    }
  }
}
  • 最后挂载到store实例上:
# 将setupStore中的属性和方法挂载到新建的Store实例上
if (isVue2) {
  // vue2:通过set方法来新增响应式数据属性
  Object.keys(setupStore).forEach((key) => {
    set(store, key, setupStore[key])
  })
} else {
  // vue3:将处理后setupStore中的内容挂载到Store实例上【state数据和actions中的方法】
  assign(store, setupStore)
}

因为这里的store实例是前面新建的响应式数据对象,只存在最基础的属性和方法 【基础版Store】

const store = reactive(partialStore)

所以这里需要将我们真正的store中的内容 【即state数据和actions中的方法】 挂载到新建的这个Store实例上,这样这个Store实例才算真正的创建完成,包含了我们所需要的属性和方法。

(五)最后我们再看看 $state属性:

# 定义了一个访问器属性$state
Object.defineProperty(store, '$state', {
  // 访问代理
  get: () => pinia.state.value[$id],
  // 非直接修改的setter,而是在内部使用了$patch方法
  set: (state) => {
    $patch(($state) => {
      # 覆盖key,而非整个替换
      assign($state, state)
    })
  }
})
  • getter:可以通过store.$store访问state数据,getter内部设置了对应的访问代理。
  • setter:这里setter并不是直接去修改store的state,而是内部通过patch来安全的修改:

需要预防出现下面这样的操作:你不能完全替换掉 store 的 state,因为那样会破坏其响应性。

store.$state = { count: 24 }

所以setter内部是通过$patch方法来修改state的,这样即使出现上面这样的代码也不会破坏state的响应性。

3,总结

再总结一下两种模式定义Store的创建过程:

optionsStore模式
  1. options对象取出state、getters、actions
  2. 定义setup函数。
  3. 重写$reset方法。
  4. 创建partialStore基础store对象,将$patch$reset$subscribe等实例方法挂载到该对象上。
  5. 使用reactive(partialStore)方法创建响应式Store对象。
  6. 将创建好的store对象以键值对方式($id, store)注册到Pinia._s属性中。
  7. 调用setup()函数:设置pinia.state.value[id] = state(),然后将localState每个属性设置为ObjectRefImpl数据,将getters中的每个getter转换成计算属性,最后将处理完成后的state、getters、actions合并到一个对象中并返回,生成setupStore
  8. 循环setupStore对象,optionsStore模式在这里仅仅是包装一下actions中的方法。
  9. setupStore的内容合并到Store对象。
  10. 给Store对象定义一个$state访问器属性。
  11. 返回Store对象。
setupStore模式
  1. 初始化state,设置pinia.state.value[$id] = {}空对象。
  2. 创建partialStore基础store对象,将$patch$reset$subscribe等实例方法挂载到该对象上。
  3. 使用reactive(partialStore)方法创建响应式Store对象。
  4. 将创建好的store对象以键值对方式($id, store)注册到Pinia._s属性中。
  5. 调用setup()函数,初始化内部的ref/computed/reactive响应式数据,获取return返回的setupStore
  6. 循环setupStore对象,循环设置pinia.state.value[$id][key] = prop,同时包装一下actions中的方法。
  7. setupStore的内容合并到Store对象。
  8. 给Store对象定义一个$state访问器属性。
  9. 返回Store对象。

我们再打印查看一下,创建完成的Store实例的内容:

export const useMainStore = defineStore('main', {
  state: () => {
    return {
      count: 0
    }
  },
  actions: {
    addCount() {
      this.count++
    }
  }
})
const mainStore = useMainStore()
console.log(mainStore)

查看Store实例的内容:

Pinia源码解析【2.0.33】

4,扩展

最后我们再扩展一下,日常我们直接修改state和通过$patch方法修改state的原理区别。

还是按两种模式展示:

// options模式
const mainStore = useMainStore()
// setup模式
const userStore = useUserStore()
直接修改原理

optionsStore模式下:实际上是通过ObjectRefImpl数据内部代理的形式,实现对原对象的修改。

mainStore.count = 1

在前面我们就已经知道了optionsStore模式下的Store中有一行重要代码:

// pinia.state.value[id]  就是我们在mianStore刚开始的 state对象;{ count: 0 }
const localState = toRefs(pinia.state.value[id])

这里用toRefs方法:将原始的state对象作为参数,返回一个新对象,其属性都是转换后的ObjectRefImpl数据,了解toRefs方法源码的都知道,对ObjectRefImpl数据的设置,实际上都是会代理到原对象,即state对象。

class ObjectRefImpl<T extends object, K extends keyof T> {
  ...
  
  set value(newVal) {
    // 原理:修改原对象
    this._object[this._key] = newVal
  }
}

Pinia源码解析【2.0.33】

setupStore模式下:因为注册时引用的同一个ref数据对象,所以通过userStore.age的修改,pinia.state.value.user.age也会变化,因为它们指向了同一个对象。

userStore.age = 1

Pinia源码解析【2.0.33】

Pinia源码解析【2.0.33】

案例验证:

const userStore = useUserStore()
const p = mainStore._p
// 验证: 指向同一个对象
console.log(p.state.value.user.age === userStore.age) // true
$patch方法修改原理

$patch批量修改方法原理就比较简单了,两种模式都是一样的原理:

# 修改state
// 1,对象参数
store.$patch({
  count: store.count + 1,
  age: 120,
  name: 'DIO',
})
// 2,函数参数
store.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})
# patch方法
function $patch( partialStateOrMutator ): void {
  // 声明Mutation对象
  let subscriptionMutation: SubscriptionCallbackMutation<S>
  // 暂停订阅:避免修改state的过程中频繁触发回调
  isListening = isSyncListening = false
  
  # 参数为函数的情况:
  if (typeof partialStateOrMutator === 'function') {
    // 直接调用函数,参数为id对应的state数据
    partialStateOrMutator(pinia.state.value[$id])
      
    subscriptionMutation = {
      type: MutationType.patchFunction,
      storeId: $id,
      events: debuggerEvents as DebuggerEvent[],
    }
  } else {
    # 参数为对象的情况:
    // 调用mergeReactiveObjects方法,递归合并传入state,覆盖更新value[$id]的key值
    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>
  )
}
  • 参数为函数的情况下:直接调用函数,参数为目标对象,直接修改属性即可。
  • 参数为对象的情况下:递归处理,使用新对象的值,覆盖更新value[$id][key]的值。