刚开始学 Vue 3,看到 ref、computed、v-model 就有点晕乎乎。
为了练手,我抄着教程写了一个超简单的待办清单,
结果写着写着,发现这个小玩具刚好能把我最不理解的那个家伙------computed------讲清楚。
下面就用我的视角,拆一下这个小 demo,到底在干嘛,以及 computed 为什么值得单拿出来说。
响应式数据:ref 开局
核心状态有两个:
javascript
const title = ref('');
const todos = ref([
{ id: 1, title: '打王者', done: true },
{ id: 2, title: '吃饭', done: true }
]);
title
输入框当前内容,双向绑定在输入框上。todos
一个数组,每一项是一个待办对象:id、title、done。
在模板里通过 v-model、v-for、v-if 等就能把这些数据"长"成界面。
模板怎么把数据"长"出来?
几个关键点:
-
输入框双向绑定
html<input type="text" v-model="title" @keydown.enter="addTodo">v-model="title":输入的内容自动同步到title。- keydown.enter="addTodo":按下回车,就调用 addTodo 新增待办。
-
列表循环 + 勾选状态
html<ul v-if="todos.length"> <li v-for="todo in todos" :key="todo.id"> <input type="checkbox" v-model="todo.done"> <span :class="{ done: todo.done }">{{ todo.title }}</span> </li> </ul> <div v-else>暂无待办事项</div>v-for负责把数组"摊开"成一个个li。- 每条的复选框用
v-model="todo.done",直接双向绑定完成状态。 :class="{ done: todo.done }"决定要不要加上.done这个类,实现中划线 + 灰色。
样式就是很简单的:
css
.done {
text-decoration: line-through;
color: gray;
}
新增待办:一个小小的 addTodo
javascript
const addTodo = () => {
if (!title.value) return;
todos.value.push({
id: Math.random(),
title: title.value,
done: false
});
title.value = '';
};
- 为空就不加:简单的校验。
- 往
todos里push新对象 :新待办默认done: false。 - 清空输入框:体验自然一点。
这里用的是最基础的响应式数组操作:修改 todos.value,界面自然会跟着更新。
真正的主角:computed 计算未完成数量
来看统计那一行:
html
{{ active }} / {{ todos.length }}
前面那个 active,就是一个计算属性:
javascript
const active = computed(() => {
return todos.value.filter(todo => !todo.done).length;
});
这行代码,核心逻辑其实就一句话:
把还没完成的待办筛出来,数一数有多少条。
你可能会问:
"那我为啥不干脆在模板里直接写呢,比如:
html
{{ todos.filter(todo => !todo.done).length }} / {{ todos.length }}
能不能这么写?当然可以。
但计算属性有几个很实际的好处。
computed 有什么好处?
1. 它是"派生数据"的家
像"未完成数量"这种数据:
- 不需要自己单独存一份;
- 完全可以根据
todos推导出来。
这种就叫派生数据 。
computed 天生就是为它们准备的:
javascript
const active = computed(() => {
return todos.value.filter(todo => !todo.done).length;
});
好处是:
- 代码一眼就能看出:
active是"依据todos计算得来"的结果。 - 模板里看到
{{ active }},基本就能猜到意思,不会被一长串过滤逻辑干扰。
2. 自带缓存:只在需要的时候重新算
模板里的表达式,每次渲染都会重新执行。
也就是说,如果写成:
html
{{ todos.filter(todo => !todo.done).length }}
只要组件重新渲染(不管是不是 because todos 变了),它就会再跑一次 filter。
而 computed 则不一样:
- 它会自动追踪依赖 :
todos以及每个todo.done。 - 只有当这些依赖发生变化时,
active才会重新计算。 - 其他不相干的响应式数据(比如
title)变了,并不会让它重算。
在这个小例子里,列表很短,差异你感觉不到。
但在真实项目里:
todos很大;- 统计里用到的逻辑复杂;
- 或者同一个统计在多个地方用到;
这时候 computed 的缓存机制,就能明显减少不必要的重复计算。
3. 模板更干净,逻辑集中在 JS 里
模板里写太多逻辑,阅读成本会明显升高。
想象一下,如果有好几个统计项都长这样:
html
{{ todos.filter(t => !t.done && t.priority === 'high').length }}
项目一大,很快你就会讨厌在模板里翻来翻去的复杂表达式。
把逻辑抽到 computed 里:
javascript
const activeHighPriority = computed(() =>
todos.value.filter(t => !t.done && t.priority === 'high').length
);
模板里只保留结果:
html
{{ activeHighPriority }}
- 模板更像"结构 + 文案";
- 逻辑都待在 JS 里,改起来更顺手,也好测试。
4. 复用方便
如果你有多个地方都要用到"未完成数量",
用模板表达式的话,要把 todos.filter(...).length 复制来复制去。
computed 则只用定义一次:
javascript
const active = computed(...);
模板任何地方都可以直接:
html
{{ active }}
以后改规则(比如不统计某些类型的待办)也只需要改一处逻辑。
computed 的进阶用法:get / set 做"全选"
这个例子里还有一个更高级一点的用法:**带 **
get / set 的计算属性,用来实现"全选":
javascript
const allDone = computed({
get() {
return todos.value.every(todo => todo.done);
},
set(value) {
todos.value.forEach(todo => {
todo.done = value;
});
}
});
再配合模板:
html
全选<input type="checkbox" v-model="allDone">
这里发生了几件很有意思的事:
get:从数据推导视图
- 每当界面需要知道"当前是不是全选状态",就会调用 get()。
every(todo => todo.done)判断是不是所有都完成。- 如果全部完成,
allDone为true,全选框就被勾上。
set:从视图反推数据
- 当你点击"全选"复选框时,因为用了
v-model="allDone",会触发 set(value)。 value是你勾选后的新值(true/false)。- set 里把每一条
todo.done全部改成这个值。
这种写法的妙处在于:
-
模板里看起来就像在绑一个普通的布尔值;
-
实际上背后是一个可以双向联动的"计算属性":
- 列表状态决定"全选"的勾选;
- "全选"的勾选又能反过来更新列表状态。
这也是 computed 非常有魅力的一面:**不只是"算结果",还可以通过 **
set 去"驱动数据变化" 。
小结:一个小待办里,装着 Vue 的几个核心习惯
这个小例子里,其实就体现了几个很值得养成的编码习惯:
- 状态集中在
ref/ 响应式对象里管理
title、todos这样一眼明了。 - 模板只做轻量逻辑,复杂逻辑交给
computed/ 函数
未完成数量用computed,而不是长长的一串模板表达式。 - 把"派生数据"都塞进
computed
既清晰又有缓存,量一大就知道好处。 - 用带
get/set的computed实现更自然的双向绑定
比如"全选"这种,同时依赖和影响其他状态的字段。