本系列文章是本人研究 Pinia 的相关记录。

Pinia 是近年比较热门的 Vue 相关状态管理库,我对其的学习步骤如下:

  1. 使用 pinia.js 实现一个简单的 TodoList 功能;
  2. 观察 pinia.js 的 api 和数据结构,自己实现一个伪 pinia,并将 TodoList 的依赖转为伪 pinia;
  3. 对比 pinia.js 源码和自己的实现。

(一)使用 pinia.js 实现一个简单的 TodoList 功能

首先在 main.js 中引入:

1
2
3
4
// src/main.js
import { createPinia } from 'pinia'

createApp(App).use(createPinia()).mount('#app')

在 src/store 目录下新建 todoList.js

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// src/store/todoList.js
import { defineStore } from 'pinia'

export default defineStore('todoList', {
state: () => ({
todos: [], // id: number, text: string, isFinished: boolean
filter: 'all', // 'all' 'finished' 'unfinished'
nextId: 0
}),
actions: {
addTodo(text) {
this.todos.push({
id: this.nextId++,
text,
isFinished: false
})
},
toggleTodo(id) {
this.todos = this.todos.map((todo) => {
if (todo.id === id) {
todo.isFinished = !todo.isFinished
}

return todo
})
},
removeTodo(id) {
this.todos = this.todos.filter((todo) => todo.id !== id)
}
},
getters: {
finishedTodos(state) {
return state.todos.filter((todo) => todo.isFinished)
},
unFinishedTodos(state) {
return state.todos.filter((todo) => !todo.isFinished)
},
filteredTodos(state) {
switch (this.filter) {
case 'finished':
return this.finishedTodos
case 'unfinished':
return this.unFinishedTodos
default:
return this.todos
}
}
}
})

在 src/store 目录下新建 index.js

1
2
3
import useTodoListStore from './todoList'

export { useTodoListStore }

如此,就能在 src/App.vue 中引入:

1
2
3
4
5
6
// src/App.vue
import { useTodoListStore } from './store'

const todoListStore = useTodoListStore()

console.log(todoListStore)

todoListStore 打印出来观察:

todoListStore

  1. 这是一个 Proxy 对象;
  2. $dispose$id$onAction$patch$reset$subscribe 等一些由 pinia 定义的属性以及一些继承的属性;
  3. state 中定义的 filternextIdtodos 都是 ObjectRefImpl 类型的对象,都成为了 todoListStore 的属性;
  4. actions 中定义的 addTodoremoveTodotoggleTodo 等方法都成为了 todoListStore 的属性;
  5. getters 中定义的 filteredTodosfinishedTodosunfinishedTodos 都是 ComputedRefImpl 类型的对象,都成为了 todoListStore 的属性。

观察完后将 src/App.vue 的代码改成:

1
2
3
4
5
6
7
<template>
<todo-list></todo-list>
</template>

<script setup>
import TodoList from '@/components/TodoList/index.vue'
</script>

下面来具体写 TodoList 组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- src/components/TodoList/index.vue -->
<template>
<div>
<todo-tab></todo-tab>
<todo-form></todo-form>
<todos></todos>
</div>
</template>

<script setup>
import TodoTab from './TodoTab.vue'
import TodoForm from './TodoForm.vue'
import Todos from './Todos.vue'
</script>
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
35
36
37
38
39
40
41
42
<!-- src/components/TodoList/TodoTab.vue -->
<template>
<div>
<a
href="javascript:;"
:class="{ active: todoListStore.filter === 'all' }"
@click="setFilter('all')"
>All</a
>
<a
href="javascript:;"
:class="{ active: todoListStore.filter === 'finished' }"
@click="setFilter('finished')"
>Finished</a
>
<a
href="javascript:;"
:class="{ active: todoListStore.filter === 'unfinished' }"
@click="setFilter('unfinished')"
>Unfinished</a
>
</div>
</template>

<script setup>
import { useTodoListStore } from '@/store'
const todoListStore = useTodoListStore()

const setFilter = (filter) => {
todoListStore.$patch({ filter })
}
</script>

<style scoped>
a {
margin-right: 15px;
}
.active {
text-decoration: none;
color: #000;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- src/components/TodoList/TodoForm.vue -->
<template>
<div>
<input
placeholder="Input something..."
type="text"
v-model="inputRef"
/>
<button @click="addTodo">Add Todo</button>
</div>
</template>

<script setup>
import { ref } from 'vue'
import { useTodoListStore } from '@/store'

const todoListStore = useTodoListStore()
const inputRef = ref('')

const addTodo = () => {
todoListStore.addTodo(inputRef.value)
inputRef.value = ''
}
</script>
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
<!-- src/components/TodoList/Todos.vue -->
<template>
<div>
<div v-for="item of todoListStore.filteredTodos" :key="item.id">
<input
type="checkbox"
:checked="item.isFinished"
@click="todoListStore.toggleTodo(item.id)"
/>
<span :class="{ finished: item.isFinished }"
>{{item.text}}</span
>
<button @click="todoListStore.removeTodo(item.id)">
Delete
</button>
</div>
</div>
</template>

<script setup>
import { useTodoListStore } from '@/store'

const todoListStore = useTodoListStore()
</script>

<style scoped>
.finished {
text-decoration: line-through;
}
</style>

本小节完,下一小节开始自己实现 pinia 相关的功能。