阶段1:自动算盘
目标:创建一个 computed(计算属性)
操作:
// 导包
import { ref, computed } from 'vue'
// 操作代码
let count = ref(1)
// 定义一个计算属性:总是等于 count 的 2 倍
let double = computed(() => count.value * 2)
模板(<template>)里写:
<div>原价: {{ count }}</div>
<div>双倍: {{ double }}</div>
结果: 页面显示 1 和 2
傻瓜口令:"computed 是算盘,专门算结果"
阶段2:它是活的
目标:修改依赖,computed 自动变
操作: 接上一阶段代码
function test() {
count.value = 5 // 我只改了 count
}
模板(<template>)里写:
<button @click="test">改为5</button>
结果: 点按钮,double 自动立刻变成了 10。 不用你手动去改 double,它自己盯着 count 呢。
傻瓜口令:"源头变了它就变,反应超灵敏"
阶段3:多头联动
目标:一个 computed 依赖多个数据
操作:
// 导包
import { ref, computed } from 'vue'
// 操作代码
let x = ref(10)
let y = ref(20)
// 依赖 x 和 y
let he = computed(() => x.value + y.value)
模板(<template>)里写:
{{ he }}
结果: 页面显示 30。 不管 x 变了,还是 y 变了,he 都会自动跟着变。
傻瓜口令:"多个数据一起算,谁变它都变"
阶段4:它会偷懒 (缓存)
目标:验证缓存特性(它很聪明,不会做无用功)
操作:
import { ref, computed } from 'vue'
let count = ref(1)
let lazyNum = computed(() => {
console.log('💦 辛苦计算中...')
return count.value + 100
})
模板(<template>)里写:
<!-- 用了三次 -->
{{ lazyNum }}
{{ lazyNum }}
{{ lazyNum }}
结果: 虽然页面用了3次,但控制台里只打印了1次“辛苦计算中”。 因为它发现 count 没变,就直接把上次算好的答案背出来了。
傻瓜口令:"没变就不算,直接报答案"
阶段5:身份揭秘
目标:验证 computed 返回的是 Ref
操作:
import { ref, computed } from 'vue'
let count = ref(1)
let c = computed(() => count.value + 1)
// 在 script 里取值
console.log(c.value) // 必须点 .value
结果: computed 产生的也是一个 Ref 盒子,在 js 里操作记得拆包。
傻瓜口令:"computed 也是盒,取值点 value"
阶段6:禁止修改
目标:试图给 computed 赋值(演示错误)
操作:
import { ref, computed } from 'vue'
let count = ref(1)
let c = computed(() => count.value + 1)
// ❌ 错误操作
c.value = 999
结果:
控制台红色/黄色警告:computed value is readonly。
因为它是个“结果”,你不能强行修改结果,只能去修改“源头”(count)。
傻瓜口令:"它是计算器,不能写数字"
阶段7:能读也能写
目标:Writable Computed(可写计算属性)
操作:
// 导包
import { ref, computed } from 'vue'
// 操作代码
let count = ref(1)
let double = computed({
// 读的时候:乘2
get: () => count.value * 2,
// 写的时候:除2(反向控制 count)
set: (val) => count.value = val / 2
})
// 以前只能读,现在可以写!
double.value = 10
模板(<template>)里写:
count: {{ count }}
结果: 把 double 改成 10,count 自动变成了 5
傻瓜口令:"要想可写加 set,反向控制源数据"
阶段8:绑定输入框
目标:v-model 绑定 computed
操作: 接上一阶段代码
// 必须是可写的 computed (有set)
模板(<template>)里写:
输入双倍值: <input v-model="double">
<p>原始值 count: {{ count }}</p>
结果: 你输入 100,count 自动变 50。 你输入 20,count 自动变 10。
傻瓜口令:"输入框里双向绑,数据变动自动响"
阶段9:自动筛子
目标:列表过滤
操作:
import { ref, computed } from 'vue'
let list = ref([10, 20, 5, 8, 100])
// 自动筛选所有大于 10 的数
let bigList = computed(() => {
return list.value.filter(n => n > 10)
})
模板(<template>)里写:
{{ bigList }}
结果: 显示 [20, 100]。 如果你往 list 里再加个数,bigList 也会自动更新。
傻瓜口令:"列表筛选不用急,computed 帮你理"
阶段10:自动排队
目标:列表排序
操作:
import { ref, computed } from 'vue'
let list = ref([3, 1, 5, 2])
let sortedList = computed(() => {
// [...list.value] 是为了复制一份,不要修改原数组
return [...list.value].sort()
})
模板(<template>)里写:
{{ sortedList }}
结果: 显示 [1, 2, 3, 5]
傻瓜口令:"自动排队很整齐,不用动手去搬运"
阶段11:连环计
目标:链式依赖(Computed 依赖 Computed)
操作:
import { ref, computed } from 'vue'
let price = ref(100)
let num = ref(2)
// 总价 = 单价 * 数量 (200)
let total = computed(() => price.value * num.value)
// 打折价 = 总价 * 0.8 (160)
// 这里依赖了上面的 total
let discount = computed(() => total.value * 0.8)
模板(<template>)里写:
打折后: {{ discount }}
结果: 改 price 或 num,discount 都会自动算对
傻瓜口令:"一环扣一环,全都不用管"
阶段12:变装大师
目标:返回样式对象 (Style)
操作:
import { ref, computed } from 'vue'
let color = ref('red')
// 返回一个样式对象
let myStyle = computed(() => {
return { color: color.value, fontSize: '20px' }
})
模板(<template>)里写:
<div :style="myStyle">我是变色龙</div>
<button @click="color = 'blue'">变蓝</button>
结果: 点按钮,字变蓝
傻瓜口令:"样式装在盒子里,根据心情自动换"
阶段13:贴标签
目标:返回类名对象 (Class)
操作:
import { ref, computed } from 'vue'
let isError = ref(false)
// 返回类名对象:key是类名,value是开关
let myClass = computed(() => {
return {
'error-box': isError.value,
'normal-box': !isError.value
}
})
模板(<template>)里写:
<div :class="myClass">盒子</div>
<button @click="isError = !isError">切换状态</button>
结果: class 会在这个 div 上自动切换
傻瓜口令:"标签自动贴上去,样式开关很随意"
阶段14:开关裁判
目标:返回布尔值(逻辑判断)
操作:
import { ref, computed } from 'vue'
let score = ref(59)
// 自动判断是否及格
let isPass = computed(() => score.value >= 60)
模板(<template>)里写:
{{ score }}分 - 及格了吗?{{ isPass }}
结果: 59分显示 false,改成 60分显示 true
傻瓜口令:"是错是对自动判,裁判就看 computed"
阶段15:赛跑比赛 (vs Method)
目标:对比 Method(方法)和 Computed(计算属性)的性能
操作:
// 导包
import { ref, computed } from 'vue'
// 操作代码
let count = ref(1)
// 选手1:普通方法 (没缓存)
function getNumFunc() {
console.log('🏃♂️ Method 跑了一次步')
return count.value + 100
}
// 选手2:计算属性 (有缓存)
let getNumComp = computed(() => {
console.log('🛌 Computed 翻了个身')
return count.value + 100
})
模板(<template>)里写:
方法: {{ getNumFunc() }} {{ getNumFunc() }}
计算: {{ getNumComp }} {{ getNumComp }}
结果: 控制台里: Method 跑了两次(每次调用都跑)。 Computed 只动了一次(第二次直接背答案)。
傻瓜口令:"Method 每次跑断腿,Computed 躺着背答案"
阶段16:二手加工 (Props)
目标:基于 Reactive 对象(模拟 Props)进行二次计算
操作:
import { reactive, computed } from 'vue'
// 模拟父组件传来的 props (它是 reactive 的)
const props = reactive({ width: 10, height: 20 })
// 我们不改 props,而是算一个新的面积
const area = computed(() => props.width * props.height)
模板(<template>)里写:
尺寸: {{ props.width }} x {{ props.height }}
面积: {{ area }}
<button @click="props.width++">加宽</button>
结果: 点击加宽,面积自动更新。这是组件开发中最常用的模式。
傻瓜口令:"父传子,子再算,响应链路不断线"
阶段17:万能接口 (传参)
目标:实现带参数的计算(“我想算 id 为 5 的那个人的分”)
操作:
import { ref, computed } from 'vue'
const scores = ref({ xiaoming: 90, xiaohong: 100 })
// 技巧:Computed 返回一个函数!
const getScore = computed(() => {
return (name) => {
return scores.value[name] + '分'
}
})
模板(<template>)里写:
小明: {{ getScore('xiaoming') }}
小红: {{ getScore('xiaohong') }}
结果: 显示对应分数。 注意:这种写法会失去具体值的缓存优势,但保留了响应性。
傻瓜口令:"返回一个函数,就能接收参数"
阶段18:侦探与算盘 (vs Watch)
目标:区分 Computed (算盘) 和 Watch (侦探) 的职责
操作:
import { ref, computed, watch } from 'vue'
let money = ref(100)
// 算盘:负责算账 (无副作用)
let double = computed(() => money.value * 2)
// 侦探:负责报警 (有副作用:打印、发请求)
watch(money, (newVal) => {
console.log('🚨 警告:金额变动!通知银行...', newVal)
})
模板(<template>)里写:
资产: {{ double }}
<button @click="money += 100">发财</button>
结果: computed 默默更新界面,watch 在控制台大喊大叫。
傻瓜口令:"计算用 computed,做事用 watch"
阶段19:监控摄像头
目标:使用 onTrigger 调试 Computed 为什么变了
操作:
import { ref, computed } from 'vue'
let count = ref(0)
let double = computed(() => count.value * 2, {
// 调试钩子
onTrigger(e) {
console.log('谁动了我?', e)
}
})
模板(<template>)里写:
{{ double }}
<button @click="count++">动一下</button>
结果: 控制台会显示详细信息:哪个 key 变了,新旧值是多少。
傻瓜口令:"装上监控摄像头,谁动数据看个够"
阶段20:急性子 (No Async)
目标:演示 Computed 不能包含异步操作
操作:
import { ref, computed } from 'vue'
// ❌ 错误示范:async 函数
let badComputer = computed(async () => {
return 100 // 这里的 return 其实是 return Promise
})
模板(<template>)里写:
结果: {{ badComputer }}
结果:
页面显示 [object Promise]。
因为 Computed 必须立刻、同步拿到结果。它等不了异步请求。
注:异步计算需配合 Watch 或 VueUse 的 computedAsync。
傻瓜口令:"计算不能这等那等,必须立刻返回"
阶段21:结构化输出
目标:返回对象,简化复杂的模板逻辑
操作:
import { reactive, computed } from 'vue'
let user = reactive({ age: 20, vip: true, banned: false })
// ❌ 模板里不想写这么长: v-if="user.age >= 18 && user.vip && !user.banned"
// ✅ 封装到 computed
let canEnter = computed(() => {
return user.age >= 18 && user.vip && !user.banned
})
模板(<template>)里写:
<div v-if="canEnter">欢迎光临 VIP 俱乐部</div>
<div v-else>禁止入内</div>
结果: 模板极其清爽,逻辑清晰。
傻瓜口令:"复杂逻辑打包回,模板清爽不扎堆"
阶段22:禁止捣乱 (No Side Effects)
目标:不要在 computed 里做额外操作(副作用)
操作:
// 导包
import { ref, computed } from 'vue'
// 操作代码
let count = ref(1)
let double = computed(() => {
// ❌ 错误行为:这里不应该去修改 DOM 或者 alert
document.title = '偷改标题'
return count.value * 2
})
后果: Computed 应该是一个“纯粹”的算盘,只负责接收输入,返回输出。 如果你在里面搞破坏(副作用),代码会变得极其难以预测和调试。
傻瓜口令:"只管算账,别搞破坏"
阶段23:禁止自吃 (No Mutation)
目标:不要修改依赖项(死循环陷阱)
操作:
import { ref, computed } from 'vue'
let num = ref(1)
// ❌ 危险操作
let danger = computed(() => {
// 我依赖 num,我又改了 num
num.value++
return num.value
})
模板(<template>)里写:
{{ danger }}
结果:
控制台报错:Maximum recursive updates exceeded(递归更新次数过多)。
因为:计算 -> 改num -> 触发计算 -> 改num -> 触发计算 ... 💥
傻瓜口令:"算账别改账,小心死循环"
阶段24:只认熟人 (Non-reactive)
目标:识别非响应式陷阱
操作:
import { computed } from 'vue'
// ❌ Date.now() 不是响应式的
let now = computed(() => {
return Date.now()
})
模板(<template>)里写:
当前时间戳: {{ now }}
<button @click="">刷新没用</button>
结果: 无论怎么点按钮,这个时间戳永远不会变。 因为 computed 发现里面没有它认识的 reactive/ref 朋友,所以它决定永远不重新算。
傻瓜口令:"外人它不理,只认响应式"
阶段25:势利眼 (Conditional)
目标:理解动态依赖收集(没用到的不收集)
操作:
import { ref, computed } from 'vue'
let open = ref(false)
let msg = ref('秘密信息')
let content = computed(() => {
// 如果 open 是 false,Vue 根本不会去读 msg.value
// 所以 msg 变了也不会触发重算(节省性能)
return open.value ? msg.value : '请先打开'
})
模板(<template>)里写:
{{ content }}
结果:
open 为 false 时,你改 msg 改出花来,content 并不会重新计算(不会打印日志)。只有当你需要用到 msg 时,它才会理你。
傻瓜口令:"没用到的数据,变了也不算"
阶段26:别摸空气 (DOM Access)
目标:避免在 computed 访问未挂载的 DOM
操作:
import { ref, computed, onMounted } from 'vue'
const box = ref(null)
const width = computed(() => {
// 第一次执行时,DOM 还没画上去,box.value 是 null
// ❌ 直接 box.value.offsetWidth 会报错
// ✅ 必须判空
return box.value?.offsetWidth || 0
})
模板(<template>)里写:
<div ref="box" style="width: 100px">盒子</div>
宽度: {{ width }}
结果:
加上 ?. 保护后,先显示 0,挂载后变成 100。无需报错。
傻瓜口令:"界面没画好,千万别去摸"
阶段27:不要太累 (Performance)
目标:避免耗时计算
原理: Computed 是同步的。如果你的计算公式里面写了一个“循环一亿次”,那么每次数据变动,你的页面就会卡顿一下。 解决方法:如果是极其庞大的计算,考虑 WebWorker 或 优化算法。
傻瓜口令:"计算太复杂,页面会卡吧"
阶段28:异步替身 (Solutions)
目标:解决异步需求(Computed 做不到的事)
场景:
“我想根据 userId 变化,去服务器 API 获取用户信息。”
Computed 做不到(不支持 async)。
正确做法:
使用 watch 或 watchEffect。
watch(userId, async (newId) => {
userInfo.value = await fetchUser(newId) // 这里是异步
})
傻瓜口令:"想要异步算,出门找 Watch"
阶段29:Computed 终极守则
目标:三条铁律
- 纯粹 (Pure): 别改 DOM,别发请求,别改依赖。
- 同步 (Sync): 必须立刻返回,不能 await。
- 响应式 (Reactive): 必须依赖 Ref/Reactive,否则不更新。
傻瓜口令:"纯粹、同步、响应式,三条铁律"