2.1 ref 让数字和文字变成"活的"

16 阅读9分钟

所有代码可直接复制到 Vue3 + <script setup> 环境中运行


阶段1:创建数字

目标:创建第一个 ref

操作

// 导包
import { ref } from 'vue'
// 操作代码
let pingGuo = ref(5)

模板(<template>)里写

{{ pingGuo }}

结果: 页面显示 5

错误示例

let pingGuo = 5
// 后果:页面显示5,但永远不会动

傻瓜口令:"ref 装数字"


阶段2:修改数字

目标:修改 ref 的值

操作

// 导包
import { ref } from 'vue'
// 操作代码
let pingGuo = ref(5)
pingGuo.value = 10

模板(<template>)里写

{{ pingGuo }}

结果: 页面数字从 5 变成 10

错误示例

pingGuo = 10
// 后果:页面还是5,不会动

傻瓜口令:"ref 改要 .value"


阶段3:再试一次

目标:巩固修改操作

操作

// 导包
import { ref } from 'vue'
// 操作代码
let xiangJiao = ref(2)
xiangJiao.value = 8

模板(<template>)里写

{{ xiangJiao }}

结果: 页面数字从 2 变成 8

错误示例

xiangJiao = 8
// 后果:页面还是2,不会动

傻瓜口令:"ref 改要 .value"


阶段4:包装文字

目标:ref 也能装文字

操作

// 导包
import { ref } from 'vue'
// 操作代码
let mingZi = ref('强强')
mingZi.value = '壮壮'

模板(<template>)里写

{{ mingZi }}

结果: 页面文字从 强强 变成 壮壮

错误示例

mingZi = '壮壮'
// 后果:页面还是强强,不会动

傻瓜口令:"ref 装文字,也能用"


阶段5:包装开关

目标:ref 也能装开关

操作

// 导包
import { ref } from 'vue'
// 操作代码
let kaiGuan = ref(true)
kaiGuan.value = false

模板(<template>)里写

{{ kaiGuan }}

结果: 页面显示 false

错误示例

kaiGuan = false
// 后果:页面还是 true,不会动

傻瓜口令:"ref 装开关,也能用"


阶段6:包装对象

目标:ref 也能装盒子

操作

// 导包
import { ref } from 'vue'
// 操作代码
let heZi = ref({ name: '苹果' })
heZi.value = { name: '香蕉' }

模板(<template>)里写

{{ heZi }}

结果: 页面显示 { "name": "香蕉" }

错误示例

heZi = { name: '香蕉' }
// 后果:页面还是苹果,不会动

傻瓜口令:"ref 装盒子,整体换"


阶段7:ref 包深层盒子

目标:修改盒子里的东西

操作

// 导包
import { ref } from 'vue'
// 操作代码
let heZi = ref({ name: '苹果' })
heZi.value.name = '香蕉'

模板(<template>)里写

{{ heZi.name }}

结果: 页面文字从 苹果 变成 香蕉

错误示例

heZi.name = '香蕉'
// 后果:忘写 .value,报错或者不更新

傻瓜口令:"ref 包盒子,里面也能改"


阶段8:ref 列表加项

目标:往列表里加东西

操作

// 导包
import { ref } from 'vue'
// 操作代码
let paiDui = ref(['猫', '狗'])
paiDui.value.push('鸟')

模板(<template>)里写

{{ paiDui }}

结果: 页面显示 [ "猫", "狗", "鸟" ]

错误示例

paiDui.push('鸟')
// 后果:忘写 .value,报错

傻瓜口令:"ref 包列表,加项用 push"


阶段9:ref 列表删项

目标:删掉最后那个

操作

// 导包
import { ref } from 'vue'
// 操作代码
let paiDui = ref(['猫', '狗'])
paiDui.value.pop()

模板(<template>)里写

{{ paiDui }}

结果: 页面只剩下 [ "猫" ]

错误示例

paiDui.pop()
// 后果:忘写 .value,报错

傻瓜口令:"ref 包列表,删项用 pop"


阶段10:让 ref 自动算

目标:自动算出结果

操作

// 导包
import { ref, computed } from 'vue'
// 操作代码
let shuZi = ref(2)
let chengGuo = computed(() => shuZi.value + 2)

模板(<template>)里写

{{ chengGuo }}

结果: 页面显示 4

错误示例

let chengGuo = computed(() => shuZi + 2)
// 后果:计算里忘写 .value,结果不对

傻瓜口令:"computed 帮 ref 自动算"


阶段11:盯着 ref 变

目标:数据变了打日志

操作

// 导包
import { ref, watch } from 'vue'
// 操作代码
let shuZi = ref(0)
watch(shuZi, () => console.log('变了'))
shuZi.value = 1

模板(<template>)里写

{{ shuZi }}

结果: 打开控制台(F12),看到输出 "变了"

错误示例

watch(shuZi.value, ...)
// 后果:watch 只需要名字,不要 .value

傻瓜口令:"watch 盯着 ref,变了就报告"


阶段12:ref 进函数

目标:把 ref 传给函数

操作

// 导包
import { ref } from 'vue'
// 操作代码
function jiaYi(n) {
    n.value = n.value + 1
}
let shuZi = ref(0)
jiaYi(shuZi)

模板(<template>)里写

{{ shuZi }}

结果: 页面显示 1

错误示例

function jiaYi(n) { n = n + 1 }
// 后果:没有改 .value,外面不会变

傻瓜口令:"ref 进函数,里面也能改"


阶段13:ref 给标签起名

目标:给 input 起名字

操作

// 导包
import { ref } from 'vue'
// 操作代码
let shuRuKuang = ref(null)

模板(<template>)里写

<input ref="shuRuKuang" value="点点我" />

结果: 页面显示一个输入框

错误示例

let abc = ref(null)
// <input ref="shuRuKuang" />
// 后果:名字对不上,抓不到

傻瓜口令:"ref 给标签,起个好名字"


阶段14:操作抓到的标签

目标:让输入框自动聚焦

操作

// 导包
import { ref, onMounted } from 'vue'
// 操作代码
let shuRuKuang = ref(null)
onMounted(() => {
    shuRuKuang.value.focus()
})

模板(<template>)里写

<input ref="shuRuKuang" />

结果: 页面刷新后,输入框已经处于打字状态(有光标)

错误示例

shuRuKuang.focus()
// 后果:忘写 .value,报错

傻瓜口令:"ref 抓标签,点点能聚焦"


阶段15:ref 对战 reactive

目标:对比两者的区别

对比表格

场景refreactive
包装内容万能(数字/对象)只能是对象
访问方式必须 .value直接访问
替换整体可以不可以

操作

// 导包
import { ref, reactive } from 'vue'
// 操作代码
let refNum = ref(10)
let reactiveObj = reactive({ n: 20 })
// 修改
refNum.value = 11
reactiveObj.n = 21

模板(<template>)里写

{{ refNum }} - {{ reactiveObj.n }}

结果: 页面显示 11 - 21

傻瓜口令:"ref 啥都能装,reactive 装对象"


阶段16:牵一根线 (toRef)

目标:把对象属性变 ref

操作

// 导包
import { reactive, toRef } from 'vue'
// 操作代码
let ren = reactive({ age: 10 })
let ageRef = toRef(ren, 'age')

ageRef.value = 11 // 改这个

模板(<template>)里写

{{ ren.age }}

结果: ren.age 也变成了 11,说明连着线

错误示例

let age = ren.age // 这样只是复制数字
age = 12 // ren.age 不会变

傻瓜口令:"toRef 牵根线,两头一起变"


阶段17:全部拆开 (toRefs)

目标:解构对象不丢响应

操作

// 导包
import { reactive, toRefs } from 'vue'
// 操作代码
let ren = reactive({ name: '强强', age: 10 })
// 拆开变成两个 ref
let { name, age } = toRefs(ren)

age.value = 20

模板(<template>)里写

{{ ren.age }}

结果: ren.age 也变成 20,说明都连着线

错误示例

let { age } = ren // 这样拆开就断了
age = 20 // ren.age 不会变

傻瓜口令:"toRefs 全拆开,还是连着线"


阶段18:查身份 (isRef)

目标:检查是否是 ref

操作

// 导包
import { ref, isRef } from 'vue'
// 操作代码
let a = ref(1)
let b = 1
console.log('a是吗?', isRef(a))
console.log('b是吗?', isRef(b))

模板(<template>)里写

看控制台

结果: 控制台打印:a是吗?true,b是吗?false

傻瓜口令:"isRef 查身份,看它是谁"


阶段19:只管外壳 (shallowRef)

目标:只监听外层变化

操作

// 导包
import { shallowRef } from 'vue'
// 操作代码
let s = shallowRef({ n: 1 })

// 动作1:改里面(无效)
function changeInner() { s.value.n = 2 }

// 动作2:改外壳(有效)
function changeOuter() { s.value = { n: 100 } }

模板(<template>)里写

{{ s }} 
<button @click="changeInner">改里面</button>
<button @click="changeOuter">改外壳</button>

结果: 点“改里面”页面不动;点“改外壳”页面变100

傻瓜口令:"shallowRef 这一点,只管最外层"


阶段20:强制更新 (triggerRef)

目标:强制 shallowRef 更新

操作

// 导包
import { shallowRef, triggerRef } from 'vue'
// 操作代码
let s = shallowRef({ n: 1 })

function forceChange() { 
    s.value.n = 50
    triggerRef(s) // 踢一脚
}

模板(<template>)里写

{{ s }}
<button @click="forceChange">强制改</button>

结果: 点击后页面变成 50

傻瓜口令:"triggerRef 踢一脚,强制刷新它"


阶段21:L2 总结

目标:总结什么时候用什么

对比表格

只是想存个数据用 ref
想存一堆且不用替换用 reactive
想把 reactive 拆开用 toRefs
数据巨大只需换整体用 shallowRef

操作: (无新增代码,复习上述概念)

傻瓜口令:"基本用 ref,对象看情况"


阶段22:值拿出来就死

目标:理解赋值断连

错误场景:把 value 拿出来给别的变量

错误代码

// 错误代码
import { ref } from 'vue'
let a = ref(10)
let b = a.value // b 只是个数字 10
b = 20

错误后果: a 还是 10,根本没变

正确代码

// 正确代码
import { ref } from 'vue'
let a = ref(10)
a.value = 20 // 必须改 .value

解决方法: 永远操作源头 ref,别操作拿出来的临时变量

傻瓜口令:"值拿出来,就断了线"


阶段23:ref 进对象自动脱

目标:对象里不用写 .value

错误场景:在 reactive 对象里多写了 .value

错误代码

// 错误代码
import { ref, reactive } from 'vue'
let r = ref(10)
let obj = reactive({ r })
obj.r.value = 20 // 错!不需要 .value

错误后果: obj.r.value 是 undefined,报错

正确代码

// 正确代码
obj.r = 20 // 自动就把 r 改了

解决方法: 记住:ref 只要进到 reactive 对象里,外壳就自动脱了

傻瓜口令:"ref 进对象,外壳自动脱"


阶段24:ref 进数组不脱

目标:数组里必须写 .value

错误场景:在数组里忘了写 .value

错误代码

// 错误代码
import { ref, reactive } from 'vue'
let r = ref(10)
let arr = reactive([r])
arr[0] = 20 // 错!这把 ref 覆盖了

错误后果: r 还是 10,arr[0] 变成了普通数字 20,断连了

正确代码

// 正确代码
arr[0].value = 20 // 必须加 .value

解决方法: 记住:数组特殊,ref 进去不脱壳

傻瓜口令:"ref 进数组,外壳还得留"


阶段25:watch 改自己

目标:避免死循环

错误场景:监听变化时又去改它,无限循环

错误代码

// 错误代码
import { ref, watch } from 'vue'
let n = ref(0)
watch(n, () => {
    n.value++ // 无限+1,卡死
})
n.value = 1

错误后果: 浏览器卡死,页面崩溃

正确代码

// 正确代码
watch(n, () => {
    if(n.value < 5) n.value++ // 加个刹车
})

解决方法: 只要在 watch 里改自己,必须加 if 判断停止条件

傻瓜口令:"watch 改自己,必须加条件"


阶段26:直接打印一大坨

目标:调试看 .value

错误场景:console.log(ref) 看不到值

错误代码

// 错误代码
import { ref } from 'vue'
let n = ref(10)
console.log(n)

错误后果: 控制台显示 RefImpl 一大堆属性,找不到 10 在哪

正确代码

// 正确代码
console.log(n.value) // 显示 10

解决方法: 调试打印记得加 .value

傻瓜口令:"直接看 ref,是一大坨"


阶段27:人走茶凉

目标:组件销毁清定时器

错误场景:组件关了,定时器还在跑

错误代码

// 错误代码
import { onMounted } from 'vue'
onMounted(() => {
    setInterval(() => console.log('跑'), 1000)
})
// 没写 onUnmounted

错误后果: 组件关了,后台还在一直打印,这叫内存泄漏

正确代码

// 正确代码
import { onUnmounted } from 'vue'
let t = null
onMounted(() => t = setInterval(...))
onUnmounted(() => clearInterval(t))

解决方法: 有始有终,一定要清理

傻瓜口令:"人走茶凉,别再改数"


阶段28:名字起一样

目标:变量名别和 ref 重名

错误场景:数据变量和标签 ref 重名

错误代码

// 错误代码
import { ref } from 'vue'
let count = ref(10) 

Template: <div ref="count">

错误后果: count 变成了那个 div 标签,10 丢了

正确代码

// 正确代码
let countData = ref(10)
// <div ref="countEl">

解决方法: 数据叫 xxData,标签叫 xxEl,区分开

傻瓜口令:"名字起一样,数字变标签"


阶段29:L3 总结

目标:Ref 避坑指南

总结

  1. 拿出来就废:不要把 .value 赋值给别人用
  2. 进出有别:进对象脱,进数组不脱
  3. 名字别撞:Variable 和 DOM Ref 别重名
  4. 遇事不决:先检查是不是忘了 .value

傻瓜口令:"遇事不决,先查 .value"