# Vue3教程 - 17 Pinia

之前学了父子组件之间传递数据,通过 属性回调函数的形式来传递。但是如果涉及到多级组件嵌套,各个组件之间传递数据会非常麻烦。尤其是遇到没有父子关系的组件,在其间传递数据会更麻烦。

Pinia 就是为了保存组件之间的共享数据,如果组件之间有要共享的数据,可以把数据保存到 Pinia 中,Pinia 就是提供一个全局的共享数据存储区域,相当于一个数据仓库,各个组件都可以从中读取和修改数据。

在 Vue2 中我们使用 Vuex 做状态管理,在 Vue3 中使用 Pinia。

# 17.1 搭建Pinia

# 1 安装Pinia

# npm方式安装
npm install pinia

# yarn方式安装
yarn add pinia
1
2
3
4
5

# 2 挂载数据存储对象

在项目的 main.ts 中引入 pinia 并挂载到Vue实例上。

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

// 1.引入pinia
import { createPinia } from 'pinia'

const app = createApp(App)

// 2.创建pinia
const pinia = createPinia()

// 3. 安装pinia
app.use(pinia)

// 安装路由
app.use(router)
app.mount('#app')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

下面演示一下 Pinia 的使用。

# 17.2 使用Pinia

下面通过一个样例来学习 Pinia 的运用。

实现一个功能,在一个组件中有两个按钮:增减和减少,当点击增减和减少按钮的时候,修改 pinia 中的值,在另一个组件中获取到pinia 中的数据并显示出来。这两个组件是平级的关系,没有父子关系,在之前是很难做到的。

# 1 编写src/stores/count.ts

如果在一开始新建项目的时候,就选择了 引入 Pinia 用于状态管,那么会在项目的 src 下创建一个 stores 目录,并包含了一个 counters.ts文件。这里我们新建自己的文件,所以忽略或删除。

如果创建项目的时候,没有选择 引入 Pinia 用于状态管,那么我们也按照这样的方式来操作。首先在 src 下创建一个 stores 目录。

在 Vuex 中,如果我们没有划分模块的话,是将很多的状态使用一个文件来管理,但是在 Pinia 中,我们可以针对不同的状态数据使用不同的文件来管理。例如,针对计数的共享数据,可以写在 src/stores/counter.ts 中,针对用户的共享数据,写在 src/stores/user.ts 中。这样会更加的清晰。

所以新建 src/stores/counter.ts ,定义共享的数据,编写内容如下:

文件的命名方式推荐采用 useXxxStore 的方式。

import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", {
  // state用来存储数据
  state() {
    return {
      // 定义数据
      count: 0,
    };
  },
});
1
2
3
4
5
6
7
8
9
10
11

defineStore 第一个参数是 store 的名称,第二个参数是一个对象,在其中定义 statestate 是一个函数,返回值是一个对象,在对象中定义共享数据。

上面已经定义了共享的数据,下面看一下如何读取和修改 pinia 中共享的数据。

# 2 读取共享数据

编写 CounterCom.vue 组件,在组件中读取 Pinia 中共享的数据。

代码如下:

<template>
    <div>
        <!-- 通过 counterStore.属性 获取共享数据 -->
        <h3>{{ counterStore.count }}</h3>

        <!-- 和store中的值实现绑定 -->
        <input type="text" v-model="counterStore.count">
    </div>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';

const counterStore = useCounterStore();

</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

在组件中引入 store 文件,和使用 hooks 方式一样,然后就可以使用 counterStore.count 读取到 Pinia 中 state 中定义的数据。

还使用了一个 input 绑定了 count 的数据,这样通过文本框也可以直接修改 Pinia 中 state 中定义的数据,但是这种方式不推荐,如果要实现双向绑定,需要将组件和组件自己定义的数据实现双向绑定,同时监听组件中数据的变化,然后去修改 Pinia 中的数据。

# 3 修改共享数据

创建 OperateCom.vue 文件,主要是用来修改 Pinia 中的数据。

修改数据有三种方式,下面介绍一下:

<template>
    <div>
        <button @click="increment">增加</button>
        <button @click="decrement">减少</button>
    </div>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';

const counterStore = useCounterStore();

function increment() {
    // 第一种方式:直接修改
    counterStore.count++;
}
function decrement() {
    // 第二种方式:通过对象的方式修改
    counterStore.$patch({
        count: counterStore.count - 1
    });
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

在组件中定义了增减和减少的按钮,当点击按钮的时候,调用组件中定义的方法,在定义的方法中操作 store 中的数据。

上面介绍了两种修改 store 中数据的方法:

  • 方式一:直接修改;

  • 方式二:通过 $patch() 函数,通过传递一个对象,可以同时修改多个数据。适合同时修改多个数据。

  • 方式三:使用 actions 进行修改,下面再来介绍。

# 4 测试

我就直接在 App.Vue 中演示了,在其中引入并使用这两个组件;

<template>
  <div>
    <CounterCom></CounterCom>
    <br>
    <OperateCom></OperateCom>
  </div>
</template>

<script lang="ts" setup>
import CounterCom from '@/components/CounterCom.vue';
import OperateCom from  '@/components/OperateCom.vue';

</script>
1
2
3
4
5
6
7
8
9
10
11
12
13

运行项目,当点击OperateCom组件中的增加和减少的按钮的时候,CounterCom中显示的数值会自动变化。

# 17.3 actions

actions 也就是在 Pinia 中定义方法,在方法中可以对 state 中的数据进行复杂的处理。

举个栗子:

# 1 定义actions属性

定义 actions 属性,并在其中定义函数,在函数中使用 this 可以访问 state 中定义的数据。

在函数中还可以进行异步操作或发送网络请求等。

import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", {
  // state用来存储数据
  state() {
    return {
      // 定义数据
      count: 0,
    };
  },
  actions: {    // ----定义action
    incrementCount(value: number) {  // 可以传递参数
        // 修改数据
        this.count += value;
    },
    decrementCount(value: number) {
        // 可以进行异步操作
        setTimeout(() => {
            // 修改数据
            this.count -= value;
        }, 1000);
    }
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

在上面定义了两个 actions 函数。

# 2 在组件中调用 Actions

在组件中,你可以通过 counterStore 直接就可以调用 actions 定义的函数。

<template>
    <div>
        <button @click="increment">增加</button>
        <button @click="decrement">减少</button>
    </div>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';

const counterStore = useCounterStore();

function increment() {
    // 调用action
    counterStore.incrementCount(1)
}
function decrement() {
    // 调用action
    counterStore.decrementCount(1)
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 17.4 storeToRefs

在上面的代码中,在模板中读取 store 中的数据使用的是:

<h3>{{ counterStore.count }}</h3>
1

如果有很多的属性,每个属性都要通过 counterStore. 来调用,稍微有一些麻烦。

我们可以将 state 中的数据从 counterStore 身上解构出来,例如:

const { count } = counterStore;
1

但是这样从 counterStore 解构出来,count 就失去响应式了。

所以这里可以使用 storeToRefs ,使用如下:

<template>
    <div>
        <!-- 获取共享数据 -->
        <h3>{{ count }}</h3>
    </div>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';
// 1.从pinia引入storeToRefs
import { storeToRefs } from 'pinia';

const counterStore = useCounterStore();
// 2.解构,并使用storeToRefs包裹counterStore
const { count } = storeToRefs(counterStore);

</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这样如果有多个属性,可以同时结构出来,使用的时候,就比较方便。

其实使用 toRefs 也是可以的,但是使用 toRefs 会将 counterStore 的方法等各种属性都进行包装,太重了,不推荐。

# 17.5 getters属性

当从 Pinia 中获取数据的时候,想要做一些转换,可以使用 getters

例如在之前的 state 中存储的 count,如果想要获取 count 的时候,在 count 的基础上乘以2,那么就可以使用 getters

store 中定义 getters 属性,在属性中定义对应的属性及对应的函数。

import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", {
  // state用来存储数据
  state() {
    return {
      // 定义数据
      count: 0,
    };
  },
  actions: {
    // ...action
  },
  getters: {  // 定义getters
    doubleCount(state) {
      return state.count * 2;
    },
  },
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

方法的参数是 state。在函数中返回结果乘以 2 即可。


通过上面定义的 getter,在使用的时候,就和使用 count 一样使用定义的 doubleCount 即可。

<template>
    <div>
        <h3>{{ count }}</h3>

        <!-- 和count一样读取 -->
        <h3>{{ counterStore.doubleCount }}</h3>
        <!-- 解构后的 -->
        <h3>{{ doubleCount }}</h3>
    </div>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';

const counterStore = useCounterStore();
// 一样可以解构
const { count, doubleCount } = storeToRefs(counterStore);

</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

所以如果相对 state 中的数据进行一些处理,可以使用 getter 来完成。

另外在 getter 的函数中,直接可以使用 this 访问到 state 中的数据,下面这样写也可以:

getters: {
  doubleCount(): number {  // 需要告诉返回值类型
    return this.count * 10;
  },
},
1
2
3
4
5

# 17.6 监听数据变化

如果要监听 Pinia 中 store 数据的变化,可以使用 $subscribe

$subscribe 是一个函数,这个函数接受一个回调函数作为参数,该回调函数会在每次 store 状态更新时被调用。回调函数会接收到两个参数:mutationstate

  • mutation:一个包含变化信息的对象,它描述了状态是如何被改变的(比如通过直接设置、通过 action 修改等),但它不直接包含旧状态的值。
  • statestore 的当前状态的快照。

举个栗子:

<template>
    <div>
        <h3>{{ count }}</h3>
    </div>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter';
import { storeToRefs } from 'pinia';

const counterStore = useCounterStore();

// 监听 store 中的state变化
counterStore.$subscribe((mutation, state) => {
    console.log('状态变化了:', mutation, '新状态:', state)
    console.log('新的count:', state.count)
  });

const { count } = storeToRefs(counterStore);

</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

直接用 store 调用 $subscribe 函数即可。

# 17.7 store的组合式写法

在前面定义 store 的时候,使用的是选项式的写法:

export const useCounterStore = defineStore("counter", {
  state() {
    // ...state
  },
  actions: {
    // ...action
  },
  getters: {
    // ...getters
  },
});
1
2
3
4
5
6
7
8
9
10
11

defineStore 第二个参数是一个对象,stateactionsgetters 就是一个个选项。除了使用选项式的写法,还可以使用使用组合式的写法。


举个栗子:

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

export const useCounterStore = defineStore('counter', () => {
  // 相当于在state中定义数据
  const count = ref(0)

  // 定义getters,需要通过computed来实现
  const doubleCount = computed(() => count.value * 2)

  // 定义函数,相当于定义actions
  function incrementCount() {
    count.value++
  }

  // 最后要导出
  return { count, doubleCount, incrementCount }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

使用组合式写法,defineStore 的第二个参数是一个函数,在函数中,可以定义响应式数据、函数等。最后需要将数据导出,如果数据多了都需要导出,确实有一丢丢麻烦。

需要注意,组合式写法定义 getters 需要使用计算属性 computed 来实现。