1. Ref基础:从入门到精通
Vue3的ref函数可以说是响应式编程的基石。我第一次接触ref时,也被它简洁而强大的特性所吸引。ref的核心作用就是将一个普通的JavaScript值转换为响应式对象,这个对象有一个.value属性指向原始值。
import { ref } from 'vue' // 基本用法 const count = ref(0) console.log(count.value) // 0 // 修改值 count.value++ console.log(count.value) // 1在实际项目中,我经常用ref来处理表单输入。比如下面这个用户登录场景:
const username = ref('') const password = ref('') const handleLogin = () => { if(username.value && password.value) { // 登录逻辑 } }这里有个新手容易踩的坑:在模板中使用ref时不需要.value,但在script中必须使用.value。我刚开始用Vue3时就经常忘记写.value,导致代码不工作。
ref对对象和数组的处理也很智能。它会递归地将所有嵌套属性都转为响应式:
const user = ref({ name: '张三', address: { city: '北京' } }) // 修改嵌套属性 user.value.address.city = '上海' // 这个修改也是响应式的2. 模板引用:操作DOM的利器
模板引用是ref的另一个重要用途。通过给DOM元素添加ref属性,我们可以在组件挂载后直接操作DOM元素。这在集成第三方库或实现特定UI效果时特别有用。
<template> <input ref="inputRef" type="text"> </template> <script setup> import { ref, onMounted } from 'vue' const inputRef = ref(null) onMounted(() => { // 组件挂载后,inputRef.value就是真实的DOM元素 inputRef.value.focus() }) </script>我在一个项目中需要实现图片懒加载,就是通过模板引用结合IntersectionObserver实现的:
const imgRef = ref(null) onMounted(() => { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if(entry.isIntersecting) { entry.target.src = entry.target.dataset.src observer.unobserve(entry.target) } }) }) observer.observe(imgRef.value) })模板引用也可以用在子组件上,这时ref.value获取的是子组件的实例:
<template> <ChildComponent ref="childRef" /> </template> <script setup> const childRef = ref(null) // 调用子组件方法 childRef.value.someMethod() </script>3. shallowRef:性能优化的秘密武器
shallowRef是ref的"轻量版",它只跟踪.value的变化,不会深度转换嵌套对象。这在处理大型对象时能显著提升性能。
import { shallowRef } from 'vue' const bigData = shallowRef({ // 包含大量数据的复杂对象 }) // 直接替换value会触发响应式更新 bigData.value = newValue // 修改嵌套属性不会触发更新 bigData.value.nested.prop = 'new value' // 不会触发更新!我在开发一个数据可视化项目时,遇到了性能问题。数据量很大但很少需要深度更新,换成shallowRef后性能提升了约30%。
如果需要强制更新shallowRef的值,可以使用triggerRef:
import { shallowRef, triggerRef } from 'vue' const state = shallowRef({ count: 0 }) // 修改嵌套属性 state.value.count++ // 手动触发更新 triggerRef(state)4. customRef:打造专属响应式逻辑
customRef允许我们自定义ref的行为,这在需要实现防抖、节流等高级功能时特别有用。
下面是一个防抖ref的实现:
import { customRef } from 'vue' function debouncedRef(value, delay = 500) { let timer return customRef((track, trigger) => { return { get() { track() return value }, set(newValue) { clearTimeout(timer) timer = setTimeout(() => { value = newValue trigger() }, delay) } } }) } // 使用示例 const searchText = debouncedRef('', 300)我在开发搜索功能时就用到了这个自定义ref。用户连续输入时不会立即触发搜索,而是在停止输入300ms后才执行搜索请求,既提升了性能又改善了用户体验。
5. Ref与Reactive的抉择
很多开发者纠结该用ref还是reactive。根据我的经验,ref更适合处理基本类型和需要替换整个对象的情况,而reactive更适合处理不需要替换的复杂对象。
// 适合用ref的场景 const count = ref(0) const user = ref({ name: '张三' }) // 适合用reactive的场景 const form = reactive({ username: '', password: '' })Vue官方推荐优先使用ref,因为它的行为更一致,而且在组合式函数中更容易传递。我在项目中也是主要使用ref,只有在处理紧密相关的多个属性时才会考虑reactive。
6. 实战中的常见陷阱
在使用ref的过程中,我踩过不少坑,这里分享几个常见的:
- 忘记.value:在script中修改ref值时容易忘记写.value
const count = ref(0) count++ // 错误!应该是count.value++- 解构失去响应性:直接解构ref会失去响应性
const state = ref({ count: 0 }) const { count } = state.value // count不是响应式的- 异步更新问题:在同一个事件循环中多次修改ref,Vue会合并更新
const count = ref(0) count.value++ count.value++ console.log(count.value) // 2,但DOM可能只更新一次- 模板引用未挂载:在onMounted之前访问模板引用会是null
const inputRef = ref(null) console.log(inputRef.value) // null,因为组件还没挂载7. 性能优化技巧
经过多个项目的实践,我总结了一些ref性能优化的经验:
- 对于大型不可变数据,使用shallowRef
- 避免在渲染函数中创建新的ref
- 合理使用computed减少不必要的ref更新
- 对于频繁更新的ref,考虑使用customRef实现节流
- 在v-for中使用ref时,考虑使用函数ref避免性能问题
// 不好的做法 <div v-for="item in list" :ref="itemRefs"></div> // 更好的做法 <div v-for="item in list" :ref="el => { if(el) itemRefs[item.id] = el }"></div>8. TypeScript支持
Vue3对TypeScript的支持非常完善,ref也能很好地与TS配合。我们可以为ref指定明确的类型:
const count = ref<number>(0) // 明确指定为number类型 interface User { name: string age: number } const user = ref<User>({ name: '张三', age: 20 })对于模板引用,我们也可以指定具体的元素类型:
const inputRef = ref<HTMLInputElement | null>(null)在子组件引用时,可以使用InstanceType来获取组件实例类型:
const childRef = ref<InstanceType<typeof ChildComponent> | null>(null)9. 组合式函数中的Ref
在编写组合式函数时,ref是最常用的工具之一。它允许我们在函数之间共享响应式状态:
// useCounter.ts export function useCounter(initialValue = 0) { const count = ref(initialValue) const increment = () => count.value++ const decrement = () => count.value-- return { count, increment, decrement } } // 在组件中使用 const { count, increment } = useCounter()我经常用这种模式来封装可复用的业务逻辑,比如表单验证、数据获取等。ref的这种特性让代码组织变得更加灵活。
10. 与其他特性的配合
ref可以很好地与Vue的其他特性配合使用。比如与watch一起使用:
const count = ref(0) watch(count, (newVal, oldVal) => { console.log(`count从${oldVal}变为${newVal}`) })与v-model配合使用:
const text = ref('') <template> <input v-model="text" /> </template>在开发复杂组件时,这种组合使用可以大大简化代码逻辑。