# Vue3教程 - 17 Pinia
之前学了父子组件之间传递数据,通过 属性
及回调函数
的形式来传递。但是如果涉及到多级组件嵌套,各个组件之间传递数据会非常麻烦。尤其是遇到没有父子关系的组件,在其间传递数据会更麻烦。
Pinia 就是为了保存组件之间的共享数据,如果组件之间有要共享的数据,可以把数据保存到 Pinia 中,Pinia 就是提供一个全局的共享数据存储区域,相当于一个数据仓库,各个组件都可以从中读取和修改数据。
在 Vue2 中我们使用 Vuex 做状态管理,在 Vue3 中使用 Pinia。
# 17.1 搭建Pinia
# 1 安装Pinia
# npm方式安装
npm install pinia
# yarn方式安装
yarn add pinia
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')
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,
};
},
});
2
3
4
5
6
7
8
9
10
11
defineStore
第一个参数是 store 的名称,第二个参数是一个对象,在其中定义 state
,state
是一个函数,返回值是一个对象,在对象中定义共享数据。
上面已经定义了共享的数据,下面看一下如何读取和修改 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>
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>
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>
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);
}
},
});
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>
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>
如果有很多的属性,每个属性都要通过 counterStore.
来调用,稍微有一些麻烦。
我们可以将 state
中的数据从 counterStore
身上解构出来,例如:
const { count } = counterStore;
但是这样从 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>
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;
},
},
});
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>
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;
},
},
2
3
4
5
# 17.6 监听数据变化
如果要监听 Pinia 中 store 数据的变化,可以使用 $subscribe
。
$subscribe
是一个函数,这个函数接受一个回调函数作为参数,该回调函数会在每次 store 状态更新时被调用。回调函数会接收到两个参数:mutation
和 state
。
mutation
:一个包含变化信息的对象,它描述了状态是如何被改变的(比如通过直接设置、通过 action 修改等),但它不直接包含旧状态的值。state
:store
的当前状态的快照。
举个栗子:
<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>
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
},
});
2
3
4
5
6
7
8
9
10
11
defineStore 第二个参数是一个对象,state
、actions
、getters
就是一个个选项。除了使用选项式的写法,还可以使用使用组合式的写法。
举个栗子:
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 }
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
使用组合式写法,defineStore
的第二个参数是一个函数,在函数中,可以定义响应式数据、函数等。最后需要将数据导出,如果数据多了都需要导出,确实有一丢丢麻烦。
需要注意,组合式写法定义 getters 需要使用计算属性 computed
来实现。