Pinia介绍及核心源码解析

332 阅读4分钟

Pinia 介绍

Pinia 最初是在 2019 年 11 月左右重新设计使用 Composition API 。从那时起,最初的原则仍然相同,但 Pinia 对 Vue 2 和 Vue 3 都有效,并且不必须使用组合 API, 除了安装和 SSR 之外,两者的 API 都是相同。

为什么使用 Pinia

  • dev-tools 支持
  • 热模块替换
  • 插件:使用插件扩展 Pinia 功能
  • 为 JS 用户提供适当的 TypeScript 支持
  • 服务器端渲染支持

与 Vuex 的比较

Pinia 最初是为了探索 Vuex 的下一次迭代会是什么样子,结合了 Vuex 5 核心团队讨论中的许多想法。最终,作者意识到 Pinia 已经实现了在 Vuex 5 中想要的大部分内容。 与 Vuex 相比,Pinia 提供了一个更简单的 API,具有更少的规范,提供了 Composition-API 风格的 API,最重要的是,在与 TypeScript 一起使用时具有可靠的类型推断支持。

RFC

Vuex 通过 RFC 从社区收集反馈,但 Pinia 没有,开发者根据开发应用程序、阅读他人代码、为使用客户工作以及在 Discord 上解答问题的经验来验证测试想法。

与 Vuex 3.x/4.x 的比较

Vuex 3.x 对应 Vue 2 而 Vuex 4.x 对应 Vue 3

Pinia API 与 Vuex ≤ 4 有很大不同,如下:

  • mutations 去除
  • 不需要自定义复杂包装器来支持 TS
  • 无需注入、导入函数、调用函数自动完成
  • 无需动态添加 Store
  • 不再有 modules 嵌套结构(扁平架构)
  • 没有命名空间模块

Pinia 使用

如果您的应用使用 Vue 2,还需要安装组合 API:@vue/composition-api。

安装

  • Vue3
import { createPinia } from 'pinia'

app.use(createPinia())
  • Vue2
// Vue 2,还需要安装一个插件 PiniaVuePlugin
import { createPinia, PiniaVuePlugin } from 'pinia'

Vue.use(PiniaVuePlugin)
const pinia = createPinia()

new Vue({
  el: '#app',
  // ...
  pinia,
})

定义 Store

  • optionsStore
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

// 第一种
export const useCountStore = defineStore({
  id: 'count',
  // 推荐使用 完整类型推断的箭头函数
  state: () => ({
    counter: 0
  }),
  getters: {
    doubleCount: (state) => state.counter * 2
  },
  actions: {
    increment() {
      this.counter++
    }
  }
})

// 第二种
export const useCountStore = defineStore('count', {
  state: () => ({
    counter: 0
  }),
  getters: {
    doubleCount: (state) => state.counter * 2
  },
  actions: {
    increment() {
      this.counter++
    }
  }
})
  • setupStore
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCountStore = defineStore('count', () => {
  const counter = ref(0)
  const doubleCount = computed(() => counter.value * 2);

  function increment() {
    counter.value++;
  }

  return { counter, doubleCount, increment };
})

使用 Store

<script setup>
import { useCountStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

// 模板中可直接使用
const countStore = useCountStore()
// 解构 state、getter、action
const { counter, doubleCount } = storeToRefs(countStore)
const { increment } = countStore
</script>

State

1.修改状态

  1. store.counter++
  2. $patch()
// 第一种
// 缺点:任何集合修改(例如,从数组中推送、删除、拼接元素)都需要您创建一个新集合
store.$patch({
  items: [{}, {}, ...]
  counter: store.counter + 1,
})

// 第二种
store.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.counter += 1
})
  1. $state
store.$state = { counter: 666 }
store.$state.counter = 666

2.重置状态

const store = useStore()
// 只支持 optionsStore
store.$reset()

3.订阅状态

store.$subscribe((mutation, state) => {
  // ...
})

Getters

1.访问其他 getter

与计算属性一样,可以组合多个 getter,通过 this 访问任何其他 getter。

export const useStore = defineStore('main', {
  state: () => ({
    counter: 0,
  }),
  getters: {
    doubleCount: (state) => state.counter * 2,
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  }
})

2.getter 传参

Getters 只是幕后的 computed 属性,因此无法向它们传递任何参数。 但是,可以从 getter 返回一个函数来接收参数

export const useStore = defineStore('main', {
  getters: {
    getUserById: (state) => (userId) => state.users.find((user) => user.id === userId)
  }
})

3.访问其他 Store 的 getter

import { useOtherStore } from './other-store'

export const useStore = defineStore('main', {
  state: () => ({
    // ...
  }),
  getters: {
    otherGetter(state) {
      const otherStore = useOtherStore()
      return state.localData + otherStore.data
    },
  },
})

Actions

同步操作、异步操作均支持

1.访问其他 store

import { useAuthStore } from './auth-store'

export const useSettingsStore = defineStore('settings', {
  state: () => ({
    // ...
  }),
  actions: {
    async fetchUserPreferences(preferences) {
      const auth = useAuthStore()
      if (auth.isAuthenticated) {
        this.preferences = await fetchPreferences()
      } else {
        throw new Error('User must be authenticated')
      }
    },
  },
})

2.订阅 Actions

const unsubscribe = someStore.$onAction(
  ({
    name, // action 的名字
    store, // store 实例
    args, // 调用这个 action 的参数
    after, // 在这个 action 执行完毕之后,执行这个函数
    onError, // 在这个 action 抛出异常的时候,执行这个函数
  }) => {
    // 记录开始的时间变量
    const startTime = Date.now()
    // 这将在 `store` 上的操作执行之前触发
    console.log(`Start "${name}" with params [${args.join(', ')}].`)

    // 如果 action 成功并且完全运行后,after 将触发。
    // 它将等待任何返回的 promise
    after((result) => {
      console.log(`Finished "${name}" after ${Date.now() - startTime}ms.\nResult: ${result}.`)
    })

    // 如果 action 抛出或返回 Promise.reject ,onError 将触发
    onError((error) => {
      console.warn(`Failed "${name}" after ${Date.now() - startTime}ms.\nError: ${error}.`)
    })
  }
)

// 手动移除订阅
unsubscribe()

Plugins(略过)

Plugins

核心源码(不含 API)分析

为什么读源码?考虑 Pinia 能否作为微应用统一状态管理,在测试过程中发现导出的 Store 模块被其他服务引入后不能触发响应式

流程图

pinia核心流程.png

部分核心源码

  1. app.use(createPinia())

createPinia()返回 pinia 实例对象,实例中包含 install、use 方法。install 方法用于 app 注册 Pinia 插件,use 方法用来给 pinia 扩展插件

function createPinia() {
  const scope = effectScope(true);
  const state = scope.run(() => ref({}));
  let _p = [];
  // 调用 app.use(pinia) 前 添加的插件
  let toBeInstalled = [];
  // markRaw 将对象标记为不可被转为代理
  const pinia = markRaw({
    install(app) {
      setActivePinia(pinia);
      if (!isVue2) {
        pinia._a = app;
        app.provide(piniaSymbol, pinia); // 给组件注入 pinia 实例
        app.config.globalProperties.$pinia = pinia; // 给组件注入 pinia 实例,兼容 options api 写法
        toBeInstalled.forEach((plugin) => _p.push(plugin));
        toBeInstalled = [];
      }
    },
    use(plugin) {
      // 未调用 app.use(pinia) 且为 Vue3
      if (!this._a && !isVue2) {
        toBeInstalled.push(plugin);
      } else {
        _p.push(plugin);
      }
      return this;
    },
    _p, // pinia实例
    _a: null, // vue实例
    _s: new Map(), // 存储store
    state,
  });
  return pinia;
}
  1. defineStore()

defineStore()用于定义 Store,并返回 useStore 方法,在 useStore 执行时会动态创建 store(如果是 setupStore 则调用 createSetupStore 创建 store,如果是 optionsStore 则调用 createOptionsStore 创建 store) 生成的 store 会存储到 pinia._s

function defineStore(idOrOptions, setup, setupOptions) {
  let id;
  let options;
  const isSetupStore = typeof setup === 'function';
  // 根据传参类型来获取 store id 及 配置项 options
  if (typeof idOrOptions === 'string') {
    id = idOrOptions;
    options = isSetupStore ? setupOptions : setup;
  } else {
    options = idOrOptions;
    id = idOrOptions.id;
  }
  function useStore(pinia, hot) {
    // 获取当前组件实例
    const currentInstance = getCurrentInstance();
    pinia = (__TEST__ && activePinia && activePinia._testing ? null : pinia) ||
      (currentInstance && inject(piniaSymbol, null))
    if (pinia) setActivePinia(pinia);
    pinia = activePinia;
    // 判断定义的 store 是否存在,如果不存在则创建
    if (!pinia._s.has(id)) {
      // 创建 store 并注册到 `pinia._s`
      if (isSetupStore) {
        // 创建 setup 语法类型 store
        createSetupStore(id, setup, options, pinia);
      } else {
        // 创建 options 语法类型 store
        createOptionsStore(id, options, pinia);
      }
    }
    // 根据 id 获取到对应 store 并返回
    const store = pinia._s.get(id);

    return store;
  }

  useStore.$id = id;

  return useStore;
}
  1. createSetupStore()

定义 $patch$reset$dispose$subscribe 方法,使用 reactive 创建响应式 store,遍历 setupStore 进行如下等价转换:

  • setupStore 中的 ref、reactive 定义的变量等价于 optionsStore 中的 state
  • setupStore 中的 computed 属性等价于 optionsStore 中的 getters
  • setupStore 中的导出函数等价于 optionsStore 中的 actions
function createSetupStore($id, setup, options = {}, pinia, hot, isOptionsStore) {
  let scope;
  const $subscribeOptions = {
    deep: true,
  };
  // internal state
  let isListening; // state 监听回调执行时机标记,防止频繁触发回调函数
  let isSyncListening; // state 监听回调执行时机标记,防止频繁触发回调函数
  let subscriptions = markRaw([]); // 订阅状态, callback 回调队列
  let actionSubscriptions = markRaw([]); // 订阅Actions, callback 回调队列
  const initialState = pinia.state.value[$id]; // 初始状态
  let activeListener;

  function $patch(partialStateOrMutator) {
    let subscriptionMutation;
    isListening = isSyncListening = false;
    if (typeof partialStateOrMutator === 'function') {
      partialStateOrMutator(pinia.state.value[$id]);
      subscriptionMutation = {
        type: MutationType.patchFunction,
        storeId: $id,
        events: debuggerEvents,
      };
    } else {
      // $patch 后状态更新
      mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator);
      subscriptionMutation = {
        type: MutationType.patchObject,
        payload: partialStateOrMutator,
        storeId: $id,
        events: debuggerEvents,
      };
    }
    const myListenerId = (activeListener = Symbol());
    // 状态修改完触发回调函数
    nextTick().then(() => {
      if (activeListener === myListenerId) {
        isListening = true;
      }
    });
    isSyncListening = true;
    // 触发订阅回调函数
    triggerSubscriptions(subscriptions, subscriptionMutation, pinia.state.value[$id]);
  }
  // $reset 不支持 setup 语法的 store
  const $reset = __DEV__
      ? () => {
        throw new Error(`🍍: Store "${$id}" is built using the setup syntax and does not implement $reset().`);
      }
      : noop;
  function $dispose() {
    scope.stop();
    subscriptions = [];
    actionSubscriptions = [];
    pinia._s.delete($id);
  }
  // 包装 action 方法,返回处理订阅操作的包装函数
  function wrapAction(name, action) {
    return function () {
      // ...
    };
  }
  // 基础(部分) store
  const partialStore = {
    _p: pinia,
    $id,
    $onAction: addSubscription.bind(null, actionSubscriptions),
    $patch,
    $reset,
    $subscribe(callback, options = {}) {
      const removeSubscription = addSubscription(subscriptions, callback, options.detached, () => stopWatcher());
      const stopWatcher = scope.run(() => watch(() => pinia.state.value[$id], (state) => {
        if (options.flush === 'sync' ? isSyncListening : isListening) {
          callback({
            storeId: $id,
            type: MutationType.direct,
            events: debuggerEvents,
          }, state);
        }
      }, assign({}, $subscribeOptions, options)));
      return removeSubscription;
    },
    $dispose,
  };

  // 使用 reactive 创建响应式 store
  const store = reactive(__DEV__ || USE_DEVTOOLS
      ? assign({ _hmrPayload, _customProperties: markRaw(new Set()) }, partialStore)
      : partialStore);

  // 注册 store 到 `pinia._s`
  pinia._s.set($id, store);
  const setupStore = pinia._e.run(() => {
    scope = effectScope();
    return scope.run(() => setup());
  });
  // 遍历 setupStore 属性,对其进行转换
  for (const key in setupStore) {
    const prop = setupStore[key];
    // prop 是 ref(但不是 computed)或者 reactive
    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      // 热更新相关,暂时忽略
      if (__DEV__ && hot) {
        set(hotState.value, key, toRef(setupStore, key));
      } else if (!isOptionsStore) {
        // 给 `pinia.state.value[$id]` 赋值
        if (isVue2) {
          set(pinia.state.value[$id], key, prop);
        } else {
          pinia.state.value[$id][key] = prop;
        }
      }
      // 如果 prop 是 actions
    } else if (typeof prop === 'function') {
      // 对 actions 进行重新包装并返回包装后的函数
      const actionValue = __DEV__ && hot ? prop : wrapAction(key, prop);
      if (isVue2) {
        set(setupStore, key, actionValue);
      } else {
        setupStore[key] = actionValue;
      }
    } else if (__DEV__) {
      // 如果 prop 是 computed
      if (isComputed(prop)) {
        if (IS_CLIENT) {
          // 将计算属性 key 存储到 getters队列 中
          const getters = setupStore._getters || (setupStore._getters = markRaw([]));
          getters.push(key);
        }
      }
    }
  }
  if (isVue2) {
    Object.keys(setupStore).forEach((key) => {
      set(store, key, setupStore[key]);
    });
  } else {
    assign(store, setupStore);
    assign(toRaw(store), setupStore);
  }
  // 拦截 store 中 $state 属性,便于通过 store.$state 直接修改状态
  Object.defineProperty(store, '$state', {
    get: () => (__DEV__ && hot ? hotState.value : pinia.state.value[$id]),
    set: (state) => {
      $patch(($state) => {
        assign($state, state);
      });
    },
  });
  
  return store;
}
  1. createOptionsStore()

根据 options 构造 setup 函数(保证其返回值和 setupStore 中 setup回调返回值相同),调用 createSetupStore 方法生成响应式的 store,重写 $reset 方法

function createOptionsStore(id, options, pinia, hot) {
  const { state, actions, getters } = options;
  const initialState = pinia.state.value[id];
  let store;
  // 传递到 createSetupStore 方法中执行,获取
  function setup() {
    // 如果 pinia.state.value[id] 不存在,则进行初始化
    if (!initialState && (!__DEV__ || !hot)) {
      if (isVue2) {
        set(pinia.state.value, id, state ? state() : {});
      } else {
        pinia.state.value[id] = state ? state() : {};
      }
    }
    // 将 pinia.state.value[id] 各属性值转为响应式对象
    const localState = __DEV__ && hot
        ? toRefs(ref(state ? state() : {}).value)
        : toRefs(pinia.state.value[id]);
        // 处理 getters 并将处理后的 getters 和 actions 合并到 localState 中
    return assign(localState, actions, Object.keys(getters || {}).reduce((computedGetters, name) => {
      computedGetters[name] = markRaw(computed(() => {
        setActivePinia(pinia);
        const store = pinia._s.get(id);
        if (isVue2 && !store._r)
          return;
        return getters[name].call(store, store);
      }));
      return computedGetters;
    }, {}));
  }
  // 调用 createSetupStore 创建 store
  store = createSetupStore(id, setup, options, pinia, hot, true);
  // 重写 $reset 方法
  store.$reset = function $reset() {
    const newState = state ? state() : {};
    this.$patch(($state) => {
      assign($state, newState);
    });
  };
  return store;
}

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情