Vue 3源码极客之:`Vue`的`Pinia`:`Store`的`subscription`和`mutation`的实现。

各位观众老爷们,大家好!今天咱们聊聊Vue 3源码里Pinia这个小家伙,特别是它里面Store的subscription和mutation是怎么实现的。别怕,咱们用大白话讲,保证您听得懂,记得住!

开场白:Pinia,你的状态管理好帮手

Pinia,是Vue的官方状态管理库,它简单、轻量,而且类型安全。它解决了Vuex的一些痛点,比如模块命名空间冗余,以及在TypeScript中的类型推断问题。今天咱们不讲Pinia的基本用法,直接扒它的源码,看看subscription和mutation这两个核心功能是怎么运作的。

第一部分:Store的创建与初始化

要理解subscription和mutation,首先得知道Store是怎么创建的。简单来说,Pinia的Store就是一个响应式的对象,里面包含了state、getters和actions。

// Pinia的核心创建函数 createPinia()
function createPinia(): Pinia {
  const scope = effectScope(true) // 创建一个effect作用域,用于管理副作用
  const state = scope.run(() => reactive({}))! // 创建一个响应式的全局state,所有store共享
  let _p: Pinia['_p'] = [] // 插件数组
  let installed = false

  const pinia: Pinia = markRaw({
    install(app: App) {
      if (installed) {
        return __DEV__
          ? warn('Pinia installation is already in progress. You can only install Pinia once.')
          : void 0
      }

      installed = true
      pinia._a = app // 将Vue app实例存起来
      app.provide(piniaSymbol, pinia) // 通过provide/inject的方式传递pinia实例
      app.config.globalProperties.$pinia = pinia
      _p.forEach((plugin) => scope.run(() => plugin({ pinia, app, store: null as any }))) // 运行所有插件
    },

    use(plugin) {
      _p.push(plugin)
      return this
    },

    _p,
    _a: null,
    _e: scope,
    _s: new Map<string, StoreGeneric>(), // 存储所有store的map
    state,
  })

  return pinia
}

// 定义Store的函数 defineStore()
export function defineStore<Id extends string, S extends StateTree, G extends _GettersTree<S>, A extends _ActionsTree>(
  id: Id,
  options: DefineStoreOptions<Id, S, G, A>
): StoreDefinition<Id, S, G, A> {
  return defineStore(id, () => options, { defineStoreOptions: options })
}

// 真正定义Store的函数
function defineStore<Id extends string, S extends StateTree = {}, G extends _GettersTree<S> = {}, A extends _ActionsTree = {}>(
  id: Id,
  setup: () => _UnwrapAll<S> & _UnwrapAll<G> & A,
  options?: DefineStoreOptions<Id, S, G, A>
): StoreDefinition<Id, S, G, A> {
  let idInOptions: DefineStoreOptions['id']
  if (__DEV__ && options && 'id' in options) {
    idInOptions = options.id
  }

  // 确保id的唯一性
  if (__DEV__ && idInOptions && idInOptions !== id) {
    warn(
      `"${idInOptions}" was passed as an option "id" but it is different than the store id "${id}" passed as first argument. Both must match.`
    )
  }

  const useStore: StoreDefinition<Id, S, G, A> = function useStore(pinia?: Pinia | null, hot?: StoreGeneric): Store<Id, S, G, A> {
    const currentPinia =
      pinia ?? currentActivePinia ?? this.pinia
    if (!currentPinia && !hot) {
      throw new Error(
        `[🍍]: getActivePinia was called with no active Pinia. Did you forget to install pinia in your app?` +
          `n` +
          `timport { createPinia } from 'pinia'` +
          `n` +
          `tconst pinia = createPinia()` +
          `n` +
          `tapp.use(pinia)`
      )
    }

    pinia = currentPinia

    if (this && this.__pinia === pinia) {
      return this
    }

    if (pinia._s.has(id)) {
      return pinia._s.get(id)! as Store<Id, S, G, A>
    }

    let scope!: EffectScope
    if (__DEV__ || IS_SSR) {
      scope = effectScope()
    } else {
      scope = getCurrentScope()!.scope
    }

    const store: Store<Id, S, G, A> = scope.run(() => {
      let store: Store<Id, S, G, A>

      if (options?.state) {
        store = reactive(options.state()) as _DeepPartial<S>
      } else {
        store = {} as _DeepPartial<S>
      }

      // 添加一些响应式属性,例如 $id, $patch, $reset
      const setupStore = setup()

      for (const key in setupStore) {
        const prop = setupStore[key]
        if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
          // 确保ref和reactive都是响应式的
          proxyRefs(store)[key] = prop
        }
      }

      Object.assign(store, setupStore) // 将setup返回的属性合并到store中

      // 添加一些方法,例如 $patch, $reset
      Object.assign(store, {
        $id: id,
        $patch,
        $reset,
        $dispose: () => { // 添加销毁方法
          scope.stop()
          pinia._s.delete(id)
        },
      })

      return store
    })!

    pinia._s.set(id, store) // 将store存储到pinia实例中

    return store as Store<Id, S, G, A>
  }

  return useStore
}

上述代码简化了defineStore的实现,主要做了以下几件事:

  1. 创建响应式对象: defineStore 内部会创建一个响应式对象 (使用 reactive 或者 ref 等),用于存储 store 的 state。
  2. 合并属性: 将用户定义的 state、getters 和 actions 合并到这个响应式对象中。
  3. 添加内部属性: 例如 $id$patch$reset$dispose,这些属性是 Pinia 内部使用的。
  4. 存储 Store: 将创建好的 Store 存储到 Pinia 实例的 _s 属性中 (一个 Map 对象)。

第二部分:Subscription的实现

Subscription,顾名思义,就是订阅Store的状态变化。Pinia提供了$subscribe方法,允许我们在状态发生改变时执行一些回调函数。

// Store的$subscribe方法
function $subscribe(
  callback: StoreOnActionListener<Id, S, G, A>,
  options: SubscriptionOptions = {}
): () => void {
  const subs = subscriptions as SubscriptionCallback<S>[]
  if (!subs) {
    subscriptions = []
  }

  subscriptions!.push(callback)

  const stop = () => {
    const i = subscriptions!.indexOf(callback)
    if (i > -1) {
      subscriptions!.splice(i, 1)
    }
  }

  if (!options.detached && activeScope) {
    onScopeDispose(stop)
  }

  return stop
}

$subscribe方法其实很简单:

  1. 存储回调函数: 它将传入的回调函数 (callback) 存储到一个数组 (subscriptions) 中。
  2. 返回取消订阅函数: 它返回一个函数 (stop),用于取消订阅。调用这个函数会将回调函数从 subscriptions 数组中移除。
  3. 处理detached: 如果 options.detachedfalse (默认值),并且当前存在活动作用域,则会在作用域销毁时自动取消订阅。

关键点:如何触发Subscription?

Subscription的回调函数不是凭空触发的,它需要在状态发生改变时被调用。这个触发的时机就在$patch和Action执行完毕之后。

第三部分:Mutation的实现($patch)

Mutation,简单来说,就是直接修改Store的state。Pinia提供了$patch方法,允许我们批量修改state,或者传入一个函数来修改state。

// Store的$patch方法
function $patch(
  stateMutation: ((state: _DeepPartial<S>) => void) | _DeepPartial<S>
): void {
  let doingMutation = false

  if (typeof stateMutation === 'function') {
    isBulkUpdating = doingMutation = true
    stateMutation(store)
    isBulkUpdating = doingMutation = false
  } else {
    isBulkUpdating = doingMutation = true
    Object.keys(stateMutation).forEach((key) => {
      // @ts-expect-error: the type is defined as readonly
      store[key] = stateMutation[key]
    })
    isBulkUpdating = doingMutation = false
  }

  // 触发subscription
  if (subscriptions) {
    subscriptions.slice().forEach((callback) => {
      callback({
        storeId: $id,
        type: MutationType.patchObject,
        events: stateMutation,
      }, store.$state)
    })
  }
}

$patch方法有两种使用方式:

  1. 传入对象: store.$patch({ name: '张三', age: 18 }),直接修改state的属性。
  2. 传入函数: store.$patch(state => { state.age++ }),允许更复杂的修改逻辑。

关键点:Mutation触发Subscription

$patch 方法执行完毕后,会遍历 subscriptions 数组,并调用每个回调函数。回调函数会接收到一个包含mutation信息的对象,例如:

  • storeId: Store的ID。
  • type: Mutation的类型,这里是 MutationType.patchObject
  • events: 修改的内容,如果传入的是对象,就是这个对象;如果传入的是函数,就是这个函数。

第四部分:Action的实现与Subscription

Action,是Pinia中修改state的另一种方式。Action可以包含任意的异步逻辑,并且可以提交mutation。

// 模拟 action 的定义
const actions = {
  async increment() {
    // 模拟异步操作
    await new Promise(resolve => setTimeout(resolve, 100));
    this.count++; // 修改 state
  },
  async updateName(newName: string) {
    await new Promise(resolve => setTimeout(resolve, 100));
    this.name = newName;
  }
};

// 模拟 Store 的创建
const store = {
  id: 'myStore',
  count: 0,
  name: 'Initial Name',
  ...actions,
  $onAction: (callback: any) => {
    // 假设这里存储了 action 的回调
  }
};

// 模拟 $onAction 的实现 (简化版)
store.$onAction = function(callback: any) {
  const originalActions: any = {};
  for (const key in actions) {
    if (typeof actions[key] === 'function') {
      originalActions[key] = actions[key];
      actions[key] = async function(...args: any[]) {
        const beforeResult = callback({
          store: store,
          name: key,
          args: args,
          type: 'before',
        });
        let afterResult, error;
        try {
          const result = await originalActions[key].apply(this, args);
          afterResult = callback({
            store: store,
            name: key,
            args: args,
            type: 'after',
          });
          return result;
        } catch (e) {
          error = e;
          callback({
            store: store,
            name: key,
            args: args,
            type: 'error',
          });
          throw e;
        }
      };
    }
  }
};

关键点:

  1. $onAction注册回调: 使用$onAction方法注册回调函数,这些回调函数会在action执行的不同阶段被调用(before、after、error)。
  2. 包装原始Action: $onAction内部会遍历Store的所有actions,并用新的函数包装它们。这些新的函数会在调用原始action之前和之后执行回调函数。
  3. 提供Action信息: 回调函数会接收到一个包含action信息的对象,例如:
    • store: Store实例。
    • name: Action的名称。
    • args: Action的参数。
    • type: Action执行的阶段 (before, after, error)。
  4. 触发 subscription: action 执行完之后, Pinia 内部会触发 subscription, 从而通知状态的改变。

第五部分:Subscription和Mutation的配合使用

Subscription和Mutation是Pinia中非常重要的两个概念。它们配合使用,可以实现很多强大的功能,例如:

  • 日志记录: 可以在subscription的回调函数中记录状态的改变,方便调试。
  • 状态持久化: 可以在subscription的回调函数中将状态保存到localStorage或sessionStorage中,实现状态的持久化。
  • 撤销/重做: 可以记录所有的mutation,然后实现撤销和重做功能。

Subscription和Mutation的对比

特性 Subscription Mutation
作用 订阅状态变化,执行回调函数。 直接修改Store的state。
触发时机 state发生改变时(通过$patch或Action)。 调用$patch方法时。
回调函数参数 包含mutation信息的对象。 无直接参数,但可以通过store.$state访问state。
使用场景 日志记录、状态持久化、撤销/重做等。 修改state。

总结:Pinia的精髓

Pinia的Subscription和Mutation机制,是其核心功能之一。它们提供了一种简单而强大的方式来管理Vue应用的状态。通过Subscription,我们可以监听状态的变化,并执行相应的操作。通过Mutation,我们可以直接修改状态,从而驱动应用的更新。

结尾:源码面前,了无秘密

今天咱们一起扒了Pinia的源码,了解了Subscription和Mutation的实现。希望您能从中受益,对状态管理有更深入的理解。记住,源码面前,了无秘密!只要您肯花时间,就能掌握任何技术。

下次有机会,咱们再聊聊Pinia的其他功能,比如getters和plugins。感谢各位观众老爷的观看!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注