Vue 3 做 todos , ref 能看懂,computed 终于也懂了

刚开始学 Vue 3,看到 refcomputedv-model 就有点晕乎乎。

为了练手,我抄着教程写了一个超简单的待办清单

结果写着写着,发现这个小玩具刚好能把我最不理解的那个家伙------computed------讲清楚。

下面就用我的视角,拆一下这个小 demo,到底在干嘛,以及 computed 为什么值得单拿出来说。

响应式数据:ref 开局

核心状态有两个:

javascript 复制代码
const title = ref('');
const todos = ref([
  { id: 1, title: '打王者', done: true },
  { id: 2, title: '吃饭',   done: true }
]);
  • title
    输入框当前内容,双向绑定在输入框上。
  • todos
    一个数组,每一项是一个待办对象:idtitledone

在模板里通过 v-modelv-forv-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 = '';
};
  • 为空就不加:简单的校验。
  • todospush 新对象 :新待办默认 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) 判断是不是所有都完成。
  • 如果全部完成,allDonetrue,全选框就被勾上。

set:从视图反推数据

  • 当你点击"全选"复选框时,因为用了 v-model="allDone",会触发 set(value)。
  • value 是你勾选后的新值(true / false)。
  • set 里把每一条 todo.done 全部改成这个值。

这种写法的妙处在于:

  • 模板里看起来就像在绑一个普通的布尔值;

  • 实际上背后是一个可以双向联动的"计算属性":

    • 列表状态决定"全选"的勾选;
    • "全选"的勾选又能反过来更新列表状态。

这也是 computed 非常有魅力的一面:**不只是"算结果",还可以通过 **

set 去"驱动数据变化"

小结:一个小待办里,装着 Vue 的几个核心习惯

这个小例子里,其实就体现了几个很值得养成的编码习惯:

  • 状态集中在 ref / 响应式对象里管理
    titletodos 这样一眼明了。
  • 模板只做轻量逻辑,复杂逻辑交给 computed / 函数
    未完成数量用 computed,而不是长长的一串模板表达式。
  • 把"派生数据"都塞进 computed
    既清晰又有缓存,量一大就知道好处。
  • 用带 get/setcomputed 实现更自然的双向绑定
    比如"全选"这种,同时依赖和影响其他状态的字段。
相关推荐
bug总结5 小时前
vue+A*算法+canvas解决自动寻路方案
前端·vue.js·算法
cindershade5 小时前
JavaScript 事件循环机制详解及项目中的应用
前端·javascript
王霸天5 小时前
🚀 告别“变形”与“留白”:前端可视化大屏适配的终极方案(附源码)
前端·javascript
LYFlied5 小时前
Vue版本演进:Vue3、Vue2.7与Vue2全面对比
前端·javascript·vue.js
PieroPC5 小时前
Nicegui 组件放在页面中间
前端·后端
踏浪无痕5 小时前
自定义 ClassLoader 动态加载:不重启就能加载新代码?
后端·面试·架构
踏浪无痕5 小时前
别重蹈我们的覆辙:脚本引擎选错的两年代价
后端·面试·架构
Airene5 小时前
Vite 8 发布 beta 版本了,升级体验一下 Rolldown
前端·vite
TT哇5 小时前
【每日八股】面经常考
java·面试