Vue3|Todo List in Action

computed 追踪依赖

需求背景

现在有一个需求,需要展示状态中存储的任务总数,状态中存储的任务列表是一个数组

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
import { defineStore } from 'pinia'
import { nanoid } from 'nanoid'
import type { Task } from '@/types/task'

export const useTaskStore = defineStore('task', {
state: () => ({
tasks: [] as Task[]
}),
actions: {
addTask(title: string, description = '') {
this.tasks.push({
id: nanoid(),
title,
description,
completed: false,
createdAt: new Date().toISOString()
})
},
toggleTask(id: string) {
const task = this.tasks.find(t => t.id === id)
if (task) task.completed = !task.completed
},
deleteTask(id: string) {
this.tasks = this.tasks.filter(t => t.id !== id)
},
removeCompletedTasks(){
this.tasks = this.tasks.filter(t => t.completed === false)
}
},
persist: true
})

错误示范

在展示的时候,很自然的写出了这样的代码

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
<template>
<div style="padding: 2rem">
<h1>Taskmate</h1>
<n-space justify="start">
<n-button-group>
<n-button round type="tertiary">
{{ totalTaskCount > 0 ? "今日任务总数:" + totalTaskCount : '当前尚无任务'}}
</n-button>
</n-button-group>
</n-space>

</div>
</template>

<script setup lang="ts">
import {ref} from 'vue'
import {useTaskStore} from '@/stores/taskStore'

const taskStore = useTaskStore()

const totalTaskCount = ref(0)

totalTaskCount.value = taskStore?.tasks?.length;

</script>

但是后续测试发现,如果修改了 pinia 中的数组长度,界面上展示的个数并不会发生改变,这主要是因为这种方式本质上只是一个快照

totalTaskCount.value = taskStore.tasks.length 是一段同步代码,只会在组件初始化的时候时执行一次;

taskStore.tasks 虽然是响应式的,但你只把它的值赋值了一次,而不是建立一个依赖关系;

所以后续 tasks 增减变化了,totalTaskCount.value 也不会自动变化。

所以上述装模做样的定义了一个 totalTaskCount 本质上只是拍了一个快照,等价于:

1
let snapshot = store.value.length

每次渲染时调用

后续我换成了这样的方式

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
<template>
<div style="padding: 2rem">
<h1>Taskmate</h1>
<n-space justify="start">
<n-button-group>
<n-button round type="tertiary">
{{ getTotalTaskCount() > 0 ? "今日任务总数:" + getTotalTaskCount() : '当前尚无任务'}}
</n-button>
</n-button-group>
</n-space>

</div>
</template>

<script setup lang="ts">
import {ref} from 'vue'
import {useTaskStore} from '@/stores/taskStore'

const taskStore = useTaskStore()

function getTotalTaskCount(){
return taskStore?.tasks?.length;
}

</script>

这样的方式可以解决问题

本质上其实是每次渲染都会调用 getTotalTaskCount(),而 taskStore.tasks 是响应式数据,模板中的函数调用是可以自动响应依赖更新的。(在 Vue 3 中,模板本质是会被编译为 render() 函数,而 render() 函数内部任何访问到的响应式数据,Vue 都会自动追踪它的依赖。)

因此一旦任务变化:

  • taskStore.tasks 变化被 Vue 侦测到;
  • 触发响应式更新机制;
  • 重新调用 render
  • 所以按钮上的任务数量就能更新。

computed最佳实践

其实最佳实践思路是利用 Vue3 组合式 API 的 computed 函数来实现追踪依赖

我们现在是希望某个变量能够追踪 taskStore.task 这个响应式状态的,因此可以这么定义

const totalTaskCount = computed(() => taskStore.tasks.length)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div style="padding: 2rem">
<h1>Taskmate</h1>
<n-space justify="start">
<n-button-group>
<n-button round type="tertiary">
{{ totalTaskCount > 0 ? "今日任务总数:" + totalTaskCount : '当前尚无任务'}}
</n-button>
</n-button-group>
</n-space>

</div>
</template>

<script setup lang="ts">
import {ref} from 'vue'
import {useTaskStore} from '@/stores/taskStore'

const taskStore = useTaskStore()

const totalTaskCount = computed(() => taskStore.tasks.length)

</script>

通过这样的写法,当任务变化,totalTaskCount 会自动更新

循环渲染组件

在我们首页需要遍历 taskStore 中数组渲染所有任务的时候,这个需求可以很完美的使用 v-for 进行解决

1
2
3
4
5
6
7
8
9
<n-space vertical :size="16" style="margin: 1rem 0">
<TaskItem
v-for="task in filteredData"
:key="task.id"
:task="task"
@edit="openEdit"
@delete="deleteTask"
/>
</n-space>

这里 v-for 是用于遍历数组或对象,渲染多个组件

:key 是性能优化和组件 diff 的关键,必须是唯一值(如 task.id)。


Vue3|Todo List in Action
http://example.com/2025/06/21/Vue3-Todo-List-in-Action/
作者
Noctis64
发布于
2025年6月21日
许可协议