1. Pinia基本概念
面试题:Pinia 相比 Vuex 有什么样的优点?为什么现在官方推荐使用 Pinia ?
Pinia,是一个 Vue 阵营的新的状态管理库,现在 Vue 官方已经推荐使用 Pinia 来代替 Vuex,或者你可以把 Pinia 看作是 Vuex 的最新的版本。
- Pinia 的基本介绍
- Pinia 优势
1-1. Pinia 的基本介绍
Pinia 是一个西班牙语的单词,表示“菠萝”的意思。因为菠萝是由一小块一小块组成的,这个和 Pinia 的思想就非常的吻合,在 Pinia 中,每个 Store 仓库都是单独的扁平化的存在的。
Pinia 是由 Vue 官方团队中的一个成员开发的,最早是在 2019 年 11 左右作为一项实验性工作所提出的,当时的目的是将组合 API 融入到 Vuex 中,探索新版本的 Vuex 应有的形态,随着探索的进行,最终发现 Pinia 已经实现了 Vuex5 大部分的提案,因此 Pinia 就作为了最新版本的 Vuex,但是为了尊重作者本人,名字保持不变,仍然叫做 Pinia。
相比 Vuex,Pinia 的 API 更少而且更简单,还支持组合式 API,还可以和 Typescript 一起使用来做类型的推断。
pinia 官网:https://pinia.vuejs.org/
1-2. Pinia 优势
在 Pinia 中,已经不存在 mutations,只有 state、getters、actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter',{
state: () => ({
count: 0
}),
getters: {
doubleCount: state => state.count * 2
},
actions: {
increment() {
this.count++
},
}
})在上面的代码中,我们创建了一个仓库,该仓库中提供三个选项,分别是 state、getters 以及 actions。
actions 里面支持同步和异步来修改 store,相当于将之前 Vuex 中的 mutation 和 action 合并了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import { defineStore } from 'pinia'
export const useCounterStore = defineStore({
// ...
actions: {
// 同步的修改仓库状态
increment() {
this.count++
},
decrement() {
this.count--
},
// 异步的修改仓库状态
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.increment()
},
async decrementAsync() {
await new Promise(resolve => setTimeout(resolve, 1000))
this.decrement()
}
}
})可以和 TypeScript 一起使用,以此来获得类型推断的支持
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35import { defineStore } from 'pinia'
// 这里定义了一个名为 Todo 的接口
interface Todo {
id: number;
text: string;
done: boolean;
}
export const useTodoStore = defineStore({
id: 'todo',
state: () => ({
todos: [] as Todo[],
}),
getters: {
completedTodos: state => state.todos.filter(todo => todo.done),
},
actions: {
// text 指定了是 string 类型
addTodoItem(text: string) {
const id = state.todos.length + 1
const newTodo = { id, text, done: false }
state.todos.push(newTodo)
},
// todo 指定了是 Todo 类型
toggleTodoItem(todo: Todo) {
todo.done = !todo.done
},
async fetchTodos() {
const response = await fetch('https://jsonplaceholder.typicode.com/todos')
const todos = await response.json() as Todo[]
state.todos = todos
},
},
})关于 Store 仓库,每一个 Store 仓库都是独立的扁平化的存在的,不再像 Vuex 里面是通过 modules 嵌套
支持插件扩展,可以通过插件(函数)来扩展仓库的功能,为仓库添加全局属性或者全局方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// ...
// 这里定义了一个名为 localStoragePlugin 的插件,本质上就是一个函数
const localStoragePlugin = (context: PiniaPluginContext) => {
const key = 'my-app-state'
// 从 localStorage 中恢复状态
context.state = localStorage.getItem(key) || context.state
// 监听 state 变化,将变化保存到 localStorage
context.subscribe((mutation) => {
localStorage.setItem(key, context.state)
})
}
// ...
// 创建 Pinia 实例,并注册 localStoragePlugin 插件
const pinia = createPinia()
pinia.use(localStoragePlugin)更加轻量,压缩之后体积只有 1kb 左右,基本上可以忽略这个库的存在
真题解答
题目:Pinia 相比 Vuex 有什么样的优点?为什么现在官方推荐使用 Pinia ?
参考答案:
Pinia 是由 Vue.js 团队成员开发的下一代状态管理仓库,相比 Vuex 3.x/4.x,Pinia 可以看作是 Vuex5 版本。
Pinia 具有如下的优势:
mutations 不复存在。只有 state 、getters 、actions。
actions 中支持同步和异步方法修改 state 状态。
与 TypeScript 一起使用具有可靠的类型推断支持。
不再有模块嵌套,只有 Store 的概念,Store 之间可以相互调用。
支持插件扩展,可以非常方便实现本地存储等功能。
更加轻量,压缩后体积只有 1kb 左右。
2. Pinia快速入门
面试题:是否使用过 Pinia?简单谈一下 Pinia 的使用?
2-1. 安装 Pinia
首先第一步,需要安装 Pinia,可以通过下面的命令进行安装:
1 | npm install pinia |
安装完毕后,需要在 Vue 应用中挂载 Pinia
1 | import { createPinia } from "pinia"; |
在 src 目录下面创建仓库目录 store,在该目录下面创建对应的仓库文件,注意名字一般是 useXXXStore
在仓库文件中,我们可以通过 defineStore 来创建一个 pinia 仓库,如下:
1 | // 仓库文件 |
创建的时候支持两种风格,选项式 API 以及组合式 API。
2-2. 选项式风格
该风格基本上和之前的 Vuex 是非常相似的,只不过没有 mutation 了,无论是同步的方法还是异步的方法,都写在 actions 里面。
1 | // 仓库文件 |
在组件中使用仓库数据时,首先引入仓库方法,并执行该方法:
1 | import { useCounterStore } from "@/store/useCounterStore.js"; |
如果是要获取数据,为了保持数据的响应式,应该使用 storeToRefs 方法。
1 | import { storeToRefs } from "pinia"; |
如果是获取方法,直接从 store 里面解构出来即可。
1 | // 从仓库将方法解构出来 |
另外,仓库还提供了两个好用的 api:
- store.$reset :重置 state
- store.$patch:变更 state
1 | // 通过建立一个新的状态对象,将 store 重设为初始状态 |
2-3. 组合式风格
组合式风格就和 Vue3 中的使用方法是一样的,通过 ref 或者 reactive 来定义仓库数据。
通过普通的方法来操作仓库数据。无论是数据还是方法最终需要导出出去。
通过 computed 来做 getter。
1 | import { defineStore } from "pinia"; |
在一个仓库中,可以使用其他仓库的 getter 数据。两种风格都可以使用。
真题解答
题目:是否使用过 Pinia?简单谈一下 Pinia 的使用?
参考答案:在 Pinia 中,核心概念有
- state:仓库的核心,主要是用于维护仓库的数据
- getters:用于对数据做二次计算的,等同于 store 的 state 的计算值
- actions :对仓库状态进行操作的方法
相比 Vuex,Pinia 中没有 mutations,同步方法和异步方法都放在 actions 里面。Pinia 同时支持 Vue2 和 Vue3,内部支持两种编码风格,分别是:
- 选项式 API :编码风格基本就和之前的 Vuex 是相似的
- 组合式 API : 编码风格和 Vue3 非常相似,使用 ref 或者 reactive 来定义仓库数据,使用 computed 来做 getters,actions 里面的方法直接书写即可,最后将数据和方法通过 return 导出。
3. 添加插件
面试题:是否给 Pinia 添加过插件?具体添加的方式是什么?
在 Pinia 中,我们可以为仓库添加插件,通过添加插件能够扩展以下的内容:
- 为 store 添加新的属性
- 定义 store 时增加新的选项
- 为 store 增加新的方法
- 包装现有的方法
- 改变甚至取消 action
- 实现副作用,如本地存储
- 仅应用插件于特定 store
3-1. 自定义插件
首先建议插件单独拿一个目录来存放,一个插件就是一个方法:
1 | function deepClone(obj) { |
每个插件在扩展内容时,会对所有的仓库进行内容扩展,如果想要针对某一个仓库进行内容扩展,可以通过 context.store.$id 来指定某一个仓库来扩展内容。
插件书写完毕后,需要通过 pinia 实例对插件进行一个注册操作。
1 | // 引入自定义插件 |
3-2. 添加第三方插件
有一些第三方插件,直接通过 npm 安装使用即可。
具体的使用方法一定要参阅文档。
3-3. 真题解答
题目:是否给 Pinia 添加过插件?具体添加的方式是什么?
参考答案:在 Pinia 中可以非常方便的添加插件。一个插件就是一个函数,该函数接收一个 context 上下文对象,通过 context 对象可以拿到诸如 store、app 等信息。
每个插件在扩展内容时,会对所有的仓库进行内容扩展,如果想要针对某一个仓库进行内容扩展,可以通过 context.store.$id 来指定某一个仓库来扩展内容。
插件书写完毕后,需要通过 pinia 实例对插件进行一个注册操作。
另外,我们还可以使用一些第三方插件,直接通过 npm 安装使用即可。安装完毕后,使用方法和自定义插件是一样的,具体的使用方法一定要参阅文档。
4. 最佳实践与补充内容
面试题:在目前的 Vue 应用中,使用状态管理库进行状态管理时有哪些最佳实践?请列举一至两条
4-1. 最佳实践
4-1-1. 分离状态逻辑和业务逻辑
实际上这个就是我们使用状态管理库的目的,我们使用状态管理库,就是为了将组件的状态分离出来,这样可以方便我们维护,也方便组件之间进行状态的共享。
没有使用状态管理库:
使用状态管理库之后:
但是需要注意一点,并非所有的 Vue 应用都需要使用状态管理库,这个取决于我们所开发的应用的规模大小。如果只是小规模的 Vue 应用,使用状态管理库反而显得更麻烦。
4-1-2. 选择 Pinia 来进行状态管理
目前 Vue 官方已经推荐开发者使用 Pinia 来替代 Vuex 作为状态管理库,你可以将 Pinia 看作是 Vuex5.x
相比 Vuex,Pinia 真的真的真的很轻量,大小只有 1kb 左右,基本上可以忽略
当然相比之前的 Vuex,还有一些其他的优点:
https://pinia.vuejs.org/zh/introduction.html#comparison-with-vuex
另外如果你之前的项目使用的是 Vuex,那么你可以看一下官方的迁移指南:
https://pinia.vuejs.org/zh/cookbook/migration-vuex.html
4-1-3. 避免直接操作 store 的状态
虽然我们可以直接操作 store 的状态,但是在 Pinia 中我们最好还是避免直接操作 store 里面的状态,而是通过对应的 getters 来读取,actions 来修改
1 | <!-- 计数器--> |
1 | // 待办事项 |
与其对应的应该使用 getters 和 actions 等 API 来处理状态的读取和修改
1 | <button class="btn" @click="increment">+</button> |
1 | function addHandle() { |
这样做的好处在于提高了代码的可维护性,应该数据的改变始终来自于 actions 的方法,而不是分散于组件的各个部分。
4-1-4. 使用 TypeScript
Pinia 本身就是使用 typescript 编写的,因此我们在使用 pinia 的时候,能够非常方便的、非常自然的使用 typescript,使用 typescript 可以更好的提供类型检查和代码提示,让我们的代码更加可靠和易于维护。
官方文档对应:https://pinia.vuejs.org/zh/core-concepts/state.html#typescript
4-1-5. 将状态划分为多个模块
在一个大型应用中,如果将所有组件的状态放置在一个状态仓库中,那么会显得该状态仓库非常的臃肿。因此一般在大型项目中,是一定会将状态仓库进行拆分的。
在早期的 Vuex 中,就已经支持将状态仓库按照不同的功能模块进行拆分,只不过在 Vuex 时期,状态仓库拆分时按照的是嵌套的方式进行代码组织的。
在 Pinia 中,组织状态仓库的形式不再采用像 Vuex 一样的嵌套,而是采用的是扁平化的设计,每一个状态仓库都是独立的,这个其实也是 Pinia 这个名字的来源。
4-2. 补充内容
辅助函数
- mapStores
- mapActions
- …
- https://pinia.vuejs.org/zh/core-concepts/state.html#resetting-the-state
- https://pinia.vuejs.org/zh/cookbook/options-api.html
1
2
3
4
5
6
7
8
9
10
11
12// Vue2 的写法
import { mapState, mapActions } from "pinia";
import { useCartStore } from "../store/useCartStore.js";
export default {
computed: {
...mapState(useCartStore, ["cartData", "totalPrice"]),
},
methods: {
...mapActions(useCartStore, ["increment", "decrement", "deleteItem"])
}
};订阅 state 以及 action
1
2
3
4
5
6
7
8
9
10
11
12import { useCounterStore } from '../store/useCounterStore.js';
const store = useCounterStore(); //拿到仓库
// 监听 state 的变化
store.$subscribe((mutation, state)=>{
// mutation 是一个对象,记录了这一次变化的一些信息
console.log("type:::",mutation.type);
console.log("storeId:::",mutation.storeId);
console.log("payload:::",mutation.payload);
console.log("state:::",state);
// 做其他的事情...
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import { useCounterStore } from '../store/useCounterStore.js';
const store = useCounterStore(); //拿到仓库
// 监听action
// 传递给它的回调函数会在 action 本身之前执行
store.$onAction(
({
name, // action 名称
store, // store 实例,类似 `someStore`
args, // 传递给 action 的参数数组
}) => {
console.log("name:::", name);
console.log("store:::", store);
console.log("args:::", args);
}
);插件选项
- composition 模式下 第三个参数
- 官网介绍
真题解答
题目:在目前的 Vue 应用中,使用状态管理库进行状态管理时有哪些最佳实践?请列举一至两条
参考答案:
在使用 Vue 开发应用时,有关组件的状态管理这一块,有如下的最佳实践:
- 使用专门的状态仓库来管理组件状态,以达到状态逻辑和业务逻辑的分离
- 比起 Vuex,目前更推荐使用 Pinia 来管理仓库的状态
- 尽量都集中使用 actions 中的方法来操作 store 的状态,避免直接操作 store
- 使用 typescript 以便得到更好的类型提示
- 根据不同的功能模块来创建对应的独立的状态仓库
5. Pinia部分源码解析
养成阅读源码的习惯,有如下的好处:
- 阅读源码可以帮助我们扩宽自己的视野,可以看到优秀的程序员是如何书写代码的,从而提升我们自己的编码水平
- 知其然知其所以然。如果你阅读过源码,那么你自然能够知道某一个 API 是如何实现,背后的实现原理是什么,那么你也就能够自然的避免在使用该 API 时可能会遇到的一些 bug,会有一些自己独特的优化心得
- 最后一点就是阅读源码能够冲击大厂,大厂在面试的时候不会考察某个 API 如何使用,没什么意义,因为 API 经常也在变化,一般都是考察 API 背后的原理
阅读源码时的一些注意事项
- 阅读源码基于你已经使用过了该库或者该框架,对里面的 API 已经很熟悉了,是一种自发的行为
- 阅读源码一定要耐心
- 不要陷入于细节,在阅读源码的时候往往需要你站在一个更高的角度
5-1. defineStore 方法
回顾 defineStore 方法的使用。defineStore 方法支持两种变成风格,一种是 option store,另一种是 setup store
option store 风格:
1 | export const useCounterStore = defineStore('counter', { |
option store 风格可以将 id 写到选项里面:
1 | export const useCounterStore = defineStore({ |
setup store 风格:
1 | export const useCounterStore = defineStore('counter', () => { |
defineStore 对应的源码如下:
1 | function defineStore( |
5-2. storeToRefs 方法
首先我们还是回顾该方法的用法:
1 | <script setup> |
源码如下:
1 | function storeToRefs(store) { |
__END__