Skip to content

Vue3 Pinia

Pinia 状态管理库, 状态即响应式数据

使用 Pinia

ts
import { createApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";

const app = createApp(App);
const store = createPinia();
app.use(store);
app.mount("#app");

Pinia 选项式

store 仓库是存储 state 状态的容器

ts
import { defineStore } from "pinia";
export const useUserStore /** 命名规范 useXxxStore */ = defineStore(
  "user", // storeId
  {
    state: () => ({
      name: "$reset",
      age: 1,
    }),
    // getters 类似计算属性, 也会缓存计算结果
    getters: {},
    // actions 中可以写同步/异步方法
    actions: {
      setDefaultSync() {
        ({ name: this.name, age: this.age } = { name: "default", age: 18 });
      },
      // 不能写箭头函数, 否则没有 this
      changeAge: function (delta: number) {
        this.age += delta;
      },
    },
  }, // options
);
vue
<script lang="ts" setup>
import { useUserStore } from "@/stores";
// store 仓库是存储 state 状态的容器
const userStore = useUserStore(); // userStore 是一个 Proxy 代理对象
// 方式 1, 直接修改 state, 是响应式的
function addAge() {
  userStore.age++;
}

// 方式 2, 使用 store.$patch 修改部分 state
function statePatch() {
  // store.$patch 可以接收新的 state
  userStore.$patch({
    age: userStore.age + 1,
  });

  // store.$patch 也可以接收一个修改函数
  userStore.$patch((state) => {
    state.name += "!";
  });
}

// 方式 3, 使用 store.$state 修改全部 state
function totalChange() {
  userStore.$state = {
    age: userStore.age + 1,
    name: userStore.name + "!",
  };
}
</script>

<template>
  <h1>Pinia 选项式</h1>
  <div>age: {{ userStore.age }}, name: {{ userStore.name }}</div>
  <div class="box">
    <button @click="() => userStore.setDefaultSync()">setDefaultSync</button>
    <button @click="addAge">直接修改 state</button>
    <button @click="statePatch">修改部分 state</button>
    <button @click="totalChange">修改全部 state</button>
    <!-- 方式 4, 使用 action 修改 state -->
    <button @click="() => userStore.changeAge(1)">
      使用 action 修改 state
    </button>
  </div>
</template>

<style lang="css" scoped>
.box {
  display: flex;
  flex-direction: column;
  /** 单行, 侧轴起点对齐 */
  align-items: flex-start;
}
</style>

storeToRefs

直接解构会失去响应式, 使用 pinia 的 storeToRefs 解构可以保留响应式 (也可以使用 vue 的 toRef, toRefs)

ts
// 直接解构会失去响应式
// const { age, name } = userStore;
// 使用 pinia 的 storeToRefs 解构可以保留响应式 (也可以使用 vue 的 toRef, toRefs)
const { age, name } = storeToRefs(userStore);

actions 和 getters

CAUTION

  • state 提供响应式数据, actions 提供操作响应式数据的方法
  • actions 中可以写同步/异步方法
  • getters 类似计算属性, 也会缓存计算结果
ts
function fetchDefault(): Promise<{ name: string; age: number }> {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        name: "default",
        age: 18,
      });
    }, 3000);
  });
}

export const useUserStore = defineStore(
  "user", // storeId
  {
    // ...
    // getters 类似计算属性, 也会缓存计算结果
    getters: {
      newName(): string {
        return `${this.name}__new`;
      },
    },
    // actions 中可以写同步/异步方法
    actions: {
      async setDefault() {
        ({ name: this.name, age: this.age } = await fetchDefault());
      },
    },
  }, // options
);

store 仓库实例 API

  • store.$patch 修改部分 state, 可以接收新的 state, 也可以接收一个修改函数
  • store.$state 修改全部 state
  • userStore.$reset store.$reset 重置 state 到初始状态 userStore.$reset()
  • store.$subscribe 侦听 state 响应式数据的改变, state 改变时调用传入的 callback, 类似 watch
  • store.$onAction 侦听 action 操作响应式数据的方法的调用, 调用 action 时调用传入的 callback

store.$subscribe

store.$subscribe((mutation, newState) => void)

ts
// store.$subscribe((mutation, newState) => void)
// mutation.event 副作用事件数组, 可以获取到新值 newValue 和旧值 oldValue 等
userStore.$subscribe(
  (mutation, newState) => {
    // debugger
    console.log(mutation, newState);
  } /** callback */,
  {
    detached: false, // detached: false 组件卸载时清除回调, 默认
    // 以下是 watch 侦听器的 WatchOptions
    deep: false, // 开启深层侦听
    immediate: false, // callback 默认懒执行
    flush: "pre", // 默认 state 更新前触发 callback
    // once: true // 一次性侦听器
  }, // options
);

userStore.$onAction

  • store.$onAction(context => void)
  • context.after((actionReturnVal) => void) 接收 action 方法调用后的回调函数
  • context.onError((err) => void) 接收 action 方法的错误回调函数
  • context.args 获取 action 方法的参数数组
  • context.store 获取 store 仓库实例
ts
// store.$onAction(context => void)
userStore.$onAction(
  (context) => {
    // context.after((actionReturnVal) => void) 接收 action 方法调用后的回调函数
    context.after((ret) => {
      console.log("Action return value:", ret);
    });
    // context.onError((err) => void) 接收 action 方法的错误回调函数
    context.onError((err) => {
      console.error(err);
    });
    // context.args 获取 action 方法的参数数组
    console.log(context.args);
    // context.store 获取 store 仓库实例
    console.log(context.store === userStore); // true
  } /** callback */,
  false, // detached: false 组件卸载时清除回调, 默认
);

Pinia 组合式 (使用 setup 语法)

WARNING

使用 setup 语法创建的 store 仓库实例未实现 $reset 方法, 需要手动实现

ts
export const useUserStore2 = defineStore("user2", () => {
  // ********** state **********
  const name = ref("$reset");
  const age = ref(1);
  const foobar = reactive({ foo: "bar" });
  // ********** getters (使用计算属性) **********
  const newName = computed(() => `${name.value}__new`);
  // ********** actions **********
  const setDefaultSync: () => void = () => {
    // 对于 reactive 创建的响应式变量, 一定不能改变该变量的地址, 否则会失去响应式
    foobar.foo = "bar";
    ({ name: name.value, age: age.value } = { name: "default", age: 18 });
  };

  const changeAge: (delta: number) => void = (delta: number) => {
    age.value += delta;
  };
  // 使用 setup 语法创建的 store 仓库实例没有 $reset 方法, 需要手动实现
  const $reset: () => void = () => {
    [name.value, age.value, foobar.foo] = ["$reset", 1, "bar"];
  };
  // 一定要 return, 类比组件的 setup
  return {
    name, // Ref<string>
    age, // Ref<number>
    foobar, // Reactive<{ foo: string }>
    newName, // ComputedRef<string>
    setDefaultSync, // () => void
    changeAge, // (delta: number) => void
    $reset,
  };
}); // storeSetup 函数
vue
<script lang="ts" setup>
import { useUserStore2 } from "@/stores";
const userStore2 = useUserStore2();
function totalChange() {
  userStore2.foobar.foo += "bar";
  userStore2.$state = {
    age: userStore2.age + 1,
    name: userStore2.name + "!",
    // 对于 reactive 创建的响应式变量, 一定不能改变该变量的地址, 否则会失去响应式
    // foobar: {
    //   foo: userStore.foobar.foo + 'bar',
    // }, // 会失去响应式
    foobar: userStore2.foobar, // 保留响应式
  };
}
</script>

<template>
  <h1>Pinia 组合式 (使用 setup 语法)</h1>
  <div>age: {{ userStore2.age }}, name: {{ userStore2.name }}</div>
  <div>foobar: {{ JSON.stringify(userStore2.foobar) }}</div>
  <div class="box">
    <button @click="() => userStore2.setDefaultSync()">setDefault</button>
    <button @click="totalChange">修改全部 state</button>
    <!-- 调用 action 修改 state -->
    <button @click="() => userStore2.changeAge(1)">
      调用 action 修改 state
    </button>
    <!-- getters -->
    <div>{{ userStore2.newName }}</div>
    <!-- 手动实现的 $reset -->
    <button @click="userStore2.$reset()">$reset</button>
  </div>
</template>

<style lang="css" scoped>
.box {
  display: flex;
  flex-direction: column;
  /** 单行, 侧轴起点对齐 */
  align-items: flex-start;
}
</style>

Pinia 持久化

  • Pinia 和 Vuex 的通病: 页面刷新后 state 状态丢失
  • 解决: 编写 Pinia 持久化插件 (localStorage, sessionStorage)

对比 localStorage 和 sessionStorage

  • localStorage: 数据存储到磁盘, 没有过期时间
  • sessionStorage: 数据缓存到内存, 会话结束时, 自动清除数据

CAUTION

  • 对于选项式语法创建的 store 仓库实例, 调用 toRaw(store.$state) 将 $state 转换为普通对象
  • 对于组合式语法创建的 store 仓库实例, 必须递归调用 toRaw(store.$state) 将 $state 转换为普通对象
ts
export function deepToRaw(observed: any) {
  const isObject = (val: any) => val !== null && typeof val === "object";
  // unref(obj) 如果 obj 是 ref 创建的响应式对象, 则返回 obj.value; 否则直接返回 obj
  const val = isRef(observed) ? unref(observed) : observed;
  if (!isObject(val)) {
    return val;
  }
  if (Array.isArray(val)) {
    const rawArr: any[] = [];
    val.forEach((item) => {
      rawArr.push(deepToRaw(item));
    });
    return rawArr;
  }
  const rawObj: any = {};
  Object.keys(val).forEach((key) => {
    rawObj[key] = deepToRaw(val[key]);
  });
  return rawObj;
}
ts
function setLocalStorage(key: string, value: unknown) {
  // debugger
  const rawValue = deepToRaw(value);
  // 对于选项式语法创建的 store 仓库实例, 调用 toRaw(store.$state) 将 $state 转换为普通对象
  // 对于组合式语法创建的 store 仓库实例, 必须递归调用 toRaw(store.$state) 将 $state 转换为普通对象
  localStorage.setItem(key, JSON.stringify(rawValue));
}

function getLocalStorage(key: string) {
  return localStorage.getItem(key)
    ? JSON.parse(localStorage.getItem(key) as string)
    : {};
}

let callNum = 0;
function makePiniaPersistencePlugin(options?: {
  keyPrefix?: string;
}): PiniaPlugin {
  const { keyPrefix = "pinia" } = options ?? {}; // 解构赋值时指定默认值
  return (piniaCtx: PiniaPluginContext) => {
    // 有几个 store 仓库实例, 调用几次 pinia 插件函数
    console.log("callNum:", ++callNum);
    const key = `${keyPrefix}-${piniaCtx.store.$id}`;
    const props = getLocalStorage(key);
    console.log(
      `getLocalStorage, key: ${key}, value: ${JSON.stringify(props)}`,
    );
    // 侦听 state 响应式数据的改变, state 改变时调用传入的 callback, 类似 watch
    piniaCtx.store.$subscribe(() => {
      setLocalStorage(key, deepToRaw(piniaCtx.store.$state));
    });
    return props;
  };
}

const app = createApp(App);
const pinia = createPinia();
pinia.use(makePiniaPersistencePlugin());

app.use(pinia);
app.mount("#app");