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 实现更自然的双向绑定
    比如"全选"这种,同时依赖和影响其他状态的字段。
相关推荐
吃杠碰小鸡3 分钟前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone9 分钟前
C#使用Aspose.Words把 word转成图片
前端·c#·word
Serene_Dream27 分钟前
JVM 并发 GC - 三色标记
jvm·面试
xjt_090128 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农40 分钟前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js