【vue3】这些不常用的API,却很实用

最近在重构一个老移动端项目,用Vue3重写的过程中,发现了一些平时不太注意的API,但用起来真的很香。今天分享几个我觉得特别实用的,希望能帮到正在学习Vue3的jym。

1. shallowRef 和 shallowReactive - '大数据'性能提升

先说个场景:你有一个很大的对象,里面嵌套了很多层数据,但你只需要最外层的变化触发更新。这时候用 refreactive 就会把所有嵌套都变成响应式的,性能开销很大。

javascript 复制代码
import { ref, shallowRef, reactive, shallowReactive, triggerRef } from 'vue'

// 普通 ref - 深度响应式
const deepData = ref({
  user: {
    profile: {
      name: '张三',
      address: {
        city: '北京',
        // ... 还有很多层
      }
    }
  }
})

// shallowRef - 浅层响应式,只监听第一层
const shallowData = shallowRef({
  user: {
    profile: {
      name: '李四'
    }
  }
})

// 只有直接替换整个对象才会触发更新
shallowData.value = { user: { profile: { name: '王五' } } } // ✅ 会触发更新

// 修改内部属性不会触发更新
shallowData.value.user.profile.name = '赵六' // ❌ 不会触发更新

// 但可以用 triggerRef 手动触发更新
shallowData.value.user.profile.name = '赵六'
triggerRef(shallowData) // ✅ 手动触发更新,视图会刷新

// shallowReactive - 浅层响应式的 reactive 版本
const shallowState = shallowReactive({
  user: {
    name: '张三',
    age: 20
  }
})

shallowState.user = { name: '李四', age: 25 } // ✅ 会触发更新
shallowState.user.name = '王五' // ❌ 不会触发更新

我在处理表格数据的时候经常用这个,特别是那种几千行的数据,用 shallowRef 能明显感觉到性能提升。shallowReactiveshallowRef 的区别在于,前者直接返回响应式对象,后者返回的是包装后的 ref。

triggerRef 的使用场景 :当你使用 shallowRef 时,如果确实需要修改嵌套属性并触发更新,但又不想替换整个对象(可能很耗性能),就可以先修改属性,然后调用 triggerRef 手动触发更新。这样既享受了浅层响应式的性能优势,又能在需要时精确控制更新时机。

javascript 复制代码
// 实际应用:批量修改后统一触发更新
const tableData = shallowRef([
  { id: 1, name: '张三', status: 'active' },
  { id: 2, name: '李四', status: 'active' },
  // ... 几千条数据
])

// 批量修改多个嵌套属性
tableData.value.forEach(item => {
  item.status = 'inactive' // 修改嵌套属性
  item.updatedAt = Date.now() // 继续修改
})

// 所有修改完成后,统一触发一次更新(性能更好)
triggerRef(tableData) // ✅ 只触发一次更新,而不是每条数据都触发

2. markRaw - 让某些数据"免疫"响应式

有些数据你明确知道不需要响应式,比如第三方库的实例、DOM元素、或者一些配置对象。用 markRaw 标记后,Vue就不会把它变成响应式的。

javascript 复制代码
import { reactive, markRaw } from 'vue'

const state = reactive({
  // 这个第三方库实例不需要响应式
  chart: markRaw(new ChartLibrary()),
  
  // 这个配置对象也不需要
  config: markRaw({
    apiUrl: 'https://api.example.com',
    timeout: 5000
  }),
  
  // 这个需要响应式
  count: 0
})

// 即使把 chart 放到 reactive 里,它也不会变成响应式
// 这样既避免了不必要的性能开销,也防止了某些库因为响应式代理而出错

我之前用 ECharts 的时候就遇到过这个问题,图表实例被响应式代理后各种报错,用 markRaw 一包就解决了。

3. toRaw - 获取原始对象

有时候你需要拿到响应式对象的原始值,比如传给第三方库、或者做深拷贝。toRaw 就是干这个的。

javascript 复制代码
import { reactive, toRaw } from 'vue'

const state = reactive({
  name: 'Vue3',
  version: '3.0'
})

// 获取原始对象,不再是 Proxy
const rawState = toRaw(state)

// 传给需要普通对象的函数
someThirdPartyLibrary.process(rawState)

// 或者做深拷贝
const copy = JSON.parse(JSON.stringify(toRaw(state)))

注意,toRaw 只能拿到一层,如果对象里还有嵌套的响应式对象,那些还是 Proxy。需要递归处理的话,可以配合 markRaw 使用。

4. customRef - 自定义响应式引用

这个API特别适合做防抖、节流,或者需要自定义 get/set 逻辑的场景。

javascript 复制代码
import { customRef } from 'vue'

// 防抖的 ref
function useDebouncedRef(value, delay = 200) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track() // 追踪依赖
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger() // 触发更新
        }, delay)
      }
    }
  })
}

// 使用
const searchText = useDebouncedRef('')

// 用户输入时不会立即更新,等停止输入 200ms 后才更新

我在搜索框里经常用这个,避免用户每输入一个字符就发一次请求。

5. readonly - 创建只读的响应式对象

有时候你想让某些数据是响应式的(能追踪变化),但不允许修改。readonly 就派上用场了。

javascript 复制代码
import { reactive, readonly } from 'vue'

const original = reactive({
  count: 0,
  name: 'Vue3'
})

// 创建只读版本
const readOnlyData = readonly(original)

// 可以读取
console.log(readOnlyData.count) // 0

// 不能修改(开发环境会警告)
readOnlyData.count = 1 // ⚠️ 警告:Set operation on key "count" failed

// 但修改原始对象,只读版本也会更新
original.count = 1
console.log(readOnlyData.count) // 1 ✅

这个在组件间传递 props 的时候很有用,特别是你想确保子组件不会意外修改父组件的数据。

6. getCurrentInstance - 获取当前组件实例

这个API在组合式API里获取组件实例特别方便,虽然官方文档说主要给库开发者用,但实际开发中偶尔也会用到。

javascript 复制代码
import { getCurrentInstance } from 'vue'

export default {
  setup() {
    const instance = getCurrentInstance()
    
    // 获取组件的 props
    console.log(instance.props)
    
    // 获取组件的 attrs
    console.log(instance.attrs)
    
    // 获取组件的 emit
    instance.emit('custom-event')
    
    // 访问全局属性
    const $router = instance.appContext.config.globalProperties.$router
    
    return {}
  }
}

不过要注意,getCurrentInstance 只能在 setup 或生命周期钩子里调用,而且不能缓存到异步回调里用。

7. defineAsyncComponent - 异步组件加载

这个其实不算特别冷门,但很多人可能没深入用过。除了基本的懒加载,它还有很多高级用法。

javascript 复制代码
import { defineAsyncComponent } from 'vue'

// 基础用法
const AsyncComponent = defineAsyncComponent(() => import('./HeavyComponent.vue'))

// 带加载状态和错误处理
const AsyncComponentWithStates = defineAsyncComponent({
  loader: () => import('./HeavyComponent.vue'),
  loadingComponent: LoadingSpinner, // 加载时显示的组件
  errorComponent: ErrorComponent,   // 错误时显示的组件
  delay: 200,                       // 延迟显示 loading,避免闪烁
  timeout: 3000,                    // 超时时间
  onError(error, retry, fail, attempts) {
    // 错误处理逻辑
    if (attempts <= 3) {
      retry() // 重试
    } else {
      fail()  // 放弃
    }
  }
})

我在做路由懒加载和按需加载大组件的时候,这个API帮了不少忙。

8. watchEffect 的立即执行和清理

watchEffect 大家都知道,但它的立即执行特性和清理函数可能很多人没充分利用。

javascript 复制代码
import { watchEffect, ref } from 'vue'

const count = ref(0)

const stop = watchEffect((onInvalidate) => {
  // 这个函数会立即执行一次
  console.log('count:', count.value)
  
  // 清理函数:在下次执行前或停止监听时调用
  onInvalidate(() => {
    console.log('清理之前的副作用')
    // 比如取消请求、清除定时器等
  })
})

// 手动停止监听
stop()

这个特性在处理副作用的时候特别有用,比如监听数据变化自动发送请求,可以在清理函数里取消之前的请求,避免竞态条件。

javascript 复制代码
import { watchEffect, ref } from 'vue'

const userId = ref(1)
let abortController = null

watchEffect((onInvalidate) => {
  // 取消之前的请求
  if (abortController) {
    abortController.abort()
  }
  
  // 创建新的请求
  abortController = new AbortController()
  
  fetch(`/api/user/${userId.value}`, {
    signal: abortController.signal
  })
    .then(res => res.json())
    .then(data => {
      // 处理数据
    })
  
  // 清理函数:在下次执行前或组件卸载时调用
  onInvalidate(() => {
    if (abortController) {
      abortController.abort()
    }
  })
})

这样就能避免快速切换用户ID时,旧的请求覆盖新请求结果的问题。

9. unref、isRef、isReactive - 类型检查工具

这些工具函数在处理可能是响应式也可能不是的数据时特别有用。

javascript 复制代码
import { ref, reactive, unref, isRef, isReactive, isReadonly } from 'vue'

// unref - 如果是 ref 就返回 .value,否则返回原值
function useValue(maybeRef) {
  return unref(maybeRef) // 不用判断是不是 ref,直接 unref 就行
}

const count = ref(10)
const name = 'Vue3'

console.log(useValue(count))  // 10
console.log(useValue(name))   // 'Vue3'

// isRef - 判断是否是 ref
if (isRef(count)) {
  console.log('这是一个 ref')
}

// isReactive - 判断是否是 reactive 对象
const state = reactive({ count: 0 })
if (isReactive(state)) {
  console.log('这是一个 reactive 对象')
}

// isReadonly - 判断是否是 readonly 对象
const readonlyState = readonly(state)
if (isReadonly(readonlyState)) {
  console.log('这是一个 readonly 对象')
}

在写工具函数或者组合式函数的时候,这些检查函数能让你写出更健壮的代码。

10. effectScope 和 onScopeDispose - 批量管理副作用

effectScope 是 Vue3.2 新增的,可以批量管理一组响应式效果(computed、watch、watchEffect等),统一停止或清理。

javascript 复制代码
import { effectScope, ref, watchEffect, computed, onScopeDispose } from 'vue'

function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  // 创建一个作用域
  const scope = effectScope()
  
  scope.run(() => {
    // 在这个作用域内的所有响应式效果都会被统一管理
    watchEffect(() => {
      const handler = (e) => {
        x.value = e.clientX
        y.value = e.clientY
      }
      window.addEventListener('mousemove', handler)
      
      // 清理函数
      onScopeDispose(() => {
        window.removeEventListener('mousemove', handler)
      })
    })
    
    // 也可以有多个 watchEffect、computed 等
    const distance = computed(() => {
      return Math.sqrt(x.value ** 2 + y.value ** 2)
    })
  })
  
  // 返回停止函数
  return {
    x,
    y,
    stop: () => scope.stop() // 一次性停止所有效果
  }
}

这个在写组合式函数的时候特别有用,可以确保所有副作用都能被正确清理,避免内存泄漏。

11. provide/inject 的响应式传递

provide/inject 大家都知道,但很多人不知道它可以传递响应式数据,而且子组件可以直接修改。

javascript 复制代码
// 父组件
import { provide, ref, reactive } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const user = reactive({ name: '张三', age: 20 })
    
    // 提供响应式数据
    provide('count', count)
    provide('user', user)
    
    // 也可以提供修改函数
    provide('increment', () => {
      count.value++
    })
    
    return { count, user }
  }
}

// 子组件(可以是任意深度的子组件)
import { inject, ref } from 'vue'

export default {
  setup() {
    // 注入响应式数据,可以设置默认值
    const count = inject('count', ref(0)) // 如果父组件没有提供,使用默认值
    const user = inject('user', () => ({ name: '默认用户', age: 0 })) // 也可以使用工厂函数
    const increment = inject('increment', () => {
      console.warn('increment 方法未提供')
    })
    
    // 可以直接修改(如果父组件提供的是响应式数据)
    const handleClick = () => {
      count.value++ // ✅ 父组件的 count 也会更新
      user.name = '李四' // ✅ 父组件的 user 也会更新
      // 或者调用父组件提供的方法
      increment()
    }
    
    return { count, user, handleClick }
  }
}

这个在跨层级组件通信的时候特别方便,比 props 一层层传要优雅得多。

12. v-memo - 性能优化的指令

v-memo 是 Vue3.2 新增的指令,可以缓存模板的一部分,只有在依赖项变化时才重新渲染。

vue 复制代码
<template>
  <!-- 只有 item.id 或 selected 变化时才重新渲染这个 div -->
  <div v-for="item in list" :key="item.id" v-memo="[item.id, selected]">
    <p>{{ item.name }}</p>
    <p>{{ item.description }}</p>
    <!-- 复杂的内容 -->
    <ExpensiveComponent :data="item.data" />
  </div>
</template>

<script setup>
import { ref } from 'vue'

const list = ref([
  { id: 1, name: 'Item 1', description: '...', data: {...} },
  { id: 2, name: 'Item 2', description: '...', data: {...} },
  // ...
])

const selected = ref(null)
</script>

这个在处理长列表的时候特别有用,可以大幅提升性能。不过要注意,v-memo 必须和 v-for 一起使用。

13. Suspense - 异步组件的加载状态

Suspense 可以优雅地处理异步组件的加载状态,不需要在每个组件里单独处理。

vue 复制代码
<template>
  <Suspense>
    <template #default>
      <AsyncComponent />
    </template>
    <template #fallback>
      <div class="loading">加载中...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue'

const AsyncComponent = defineAsyncComponent(() => 
  import('./HeavyComponent.vue')
)
</script>

如果异步组件内部有异步操作(比如 setup 里用了 await),Suspense 也会等待:

vue 复制代码
<!-- AsyncComponent.vue -->
<script setup>
// 这个组件内部有异步操作
const data = await fetchData() // Suspense 会等待这个完成
</script>

这个在处理异步数据加载的时候特别优雅,比手动管理 loading 状态要方便。

14. 模板引用的高级用法

模板引用(ref)除了获取 DOM 元素,还有很多高级用法。

vue 复制代码
<template>
  <!-- 在 v-for 中获取多个元素 -->
  <div v-for="item in list" :ref="el => setItemRef(el, item.id)">
    {{ item.name }}
  </div>
  
  <!-- 获取子组件实例 -->
  <ChildComponent ref="childRef" />
</template>

<script setup>
import { ref, onMounted } from 'vue'

const list = ref([
  { id: 1, name: 'Item 1' },
  { id: 2, name: 'Item 2' },
])

// 存储多个元素引用
const itemRefs = ref({})

const setItemRef = (el, id) => {
  if (el) {
    itemRefs.value[id] = el
  }
}

// 子组件引用
const childRef = ref(null)

onMounted(() => {
  // 访问子组件的方法或属性
  console.log(childRef.value.someMethod())
  console.log(childRef.value.someProperty)
  
  // 访问所有列表项的 DOM
  console.log(itemRefs.value)
})
</script>

还可以配合 defineExpose 暴露子组件的特定内容:

vue 复制代码
<!-- ChildComponent.vue -->
<script setup>
import { ref } from 'vue'

const count = ref(0)
const privateData = ref('private') // 这个不会被暴露

const increment = () => {
  count.value++
}

// 只暴露 count 和 increment
defineExpose({
  count,
  increment
})
</script>

15. computed 的 getter/setter 形式

computed 除了只读形式,还可以有 getter 和 setter,实现双向绑定。

javascript 复制代码
import { ref, computed } from 'vue'

const firstName = ref('张')
const lastName = ref('三')

// 可读写的 computed
const fullName = computed({
  get() {
    return `${firstName.value}${lastName.value}`
  },
  set(newValue) {
    // 当设置 fullName 时,自动拆分到 firstName 和 lastName
    const parts = newValue.split('')
    if (parts.length >= 2) {
      firstName.value = parts[0]
      lastName.value = parts.slice(1).join('')
    }
  }
})

// 使用
console.log(fullName.value) // '张三'
fullName.value = '李四' // 自动更新 firstName 和 lastName
console.log(firstName.value) // '李'
console.log(lastName.value) // '四'

这个在处理表单数据转换的时候特别有用。

16. watch 的深度监听和立即执行

watch 的选项参数有很多实用的配置。

javascript 复制代码
import { watch, ref, reactive } from 'vue'

const count = ref(0)
const state = reactive({
  user: {
    name: '张三',
    age: 20
  }
})

// 深度监听对象
watch(
  () => state.user,
  (newVal, oldVal) => {
    console.log('user 变化了', newVal, oldVal)
  },
  { deep: true } // 深度监听嵌套属性
)

// 或者直接监听整个对象
watch(
  state,
  (newVal, oldVal) => {
    // 注意:newVal 和 oldVal 是同一个对象(因为 reactive)
    console.log('state 变化了')
  },
  { deep: true }
)

// 立即执行一次
watch(
  count,
  (newVal, oldVal) => {
    console.log('count:', newVal)
  },
  { immediate: true } // 立即执行一次回调
)

// 监听多个源
watch(
  [count, () => state.user.name],
  ([newCount, newName], [oldCount, oldName]) => {
    console.log('count 或 name 变化了')
  }
)

// 手动实现只监听一次的效果
let stopWatcher = null
stopWatcher = watch(
  count,
  (newVal) => {
    console.log('只执行一次')
    // 执行后立即停止监听
    if (stopWatcher) {
      stopWatcher()
      stopWatcher = null
    }
  }
)

17. nextTick 的多种用法

nextTick 大家都知道,但它的使用场景可能很多人没完全掌握。

javascript 复制代码
import { nextTick, ref } from 'vue'

const count = ref(0)
const message = ref('')

// 等待 DOM 更新后执行
async function handleClick() {
  count.value++
  message.value = 'Count 已更新'
  
  // 此时 DOM 可能还没更新
  console.log(document.querySelector('.count').textContent) // 可能是旧值
  
  // 等待 DOM 更新
  await nextTick()
  console.log(document.querySelector('.count').textContent) // 新值
  
  // 或者用回调形式
  nextTick(() => {
    console.log('DOM 已更新')
  })
}

// 在组件挂载后操作 DOM
import { onMounted } from 'vue'

onMounted(async () => {
  // 即使组件已挂载,某些 DOM 操作可能还需要等待
  await nextTick()
  // 现在可以安全地操作 DOM 了
  document.querySelector('.some-element').focus()
})

18. 响应式 API 的转换工具

Vue3 提供了一些转换工具,可以在不同响应式类型之间转换。

javascript 复制代码
import { ref, reactive, toRef, toRefs, isRef } from 'vue'

const state = reactive({
  name: 'Vue3',
  version: '3.0',
  count: 0
})

// toRef - 将 reactive 对象的某个属性转为 ref
const nameRef = toRef(state, 'name')
nameRef.value = 'Vue 3' // 会同步更新 state.name

// toRefs - 将 reactive 对象的所有属性转为 ref
const { name, version, count } = toRefs(state)
// 现在 name、version、count 都是 ref,可以在模板中直接使用

// 在组合式函数中返回响应式数据时特别有用
function useCounter() {
  const state = reactive({
    count: 0,
    step: 1
  })
  
  const increment = () => {
    state.count += state.step
  }
  
  // 返回 toRefs,让使用者可以解构而不失去响应式
  return {
    ...toRefs(state),
    increment
  }
}

// 使用时可以解构
const { count, step, increment } = useCounter()
// count 和 step 仍然是响应式的

19. 自定义指令的高级用法

自定义指令除了基本的 mountedupdated,还有很多生命周期钩子。

javascript 复制代码
// 自定义指令直接定义对象即可,不需要特殊导入
const vMyDirective = {
  // 元素插入 DOM 前调用
  created(el, binding, vnode) {
    console.log('created')
  },
  
  // 元素插入 DOM 后调用
  mounted(el, binding, vnode, prevVnode) {
    console.log('mounted', binding.value)
    // binding.value - 指令的值
    // binding.oldValue - 之前的值(仅在 update 和 componentUpdated 中可用)
    // binding.arg - 指令参数,如 v-my-directive:foo 中的 'foo'
    // binding.modifiers - 修饰符对象,如 v-my-directive.foo.bar 中的 { foo: true, bar: true }
  },
  
  // 在包含组件的 VNode 更新前调用
  beforeUpdate(el, binding, vnode, prevVnode) {
    console.log('beforeUpdate')
  },
  
  // 在包含组件的 VNode 及其子 VNode 更新后调用
  updated(el, binding, vnode, prevVnode) {
    console.log('updated')
  },
  
  // 在卸载前调用
  beforeUnmount(el, binding, vnode) {
    console.log('beforeUnmount')
  },
  
  // 卸载后调用
  unmounted(el, binding, vnode) {
    console.log('unmounted')
  }
}

// 使用
// <div v-my-directive:arg.modifier="value"></div>

实际应用:实现一个点击外部关闭的指令

javascript 复制代码
const vClickOutside = {
  mounted(el, binding) {
    el._clickOutside = (event) => {
      if (!(el === event.target || el.contains(event.target))) {
        binding.value() // 调用传入的函数
      }
    }
    document.addEventListener('click', el._clickOutside)
  },
  unmounted(el) {
    document.removeEventListener('click', el._clickOutside)
  }
}

20. 组合式 API 的类型推断技巧

在 TypeScript 中使用组合式 API 时,有一些类型推断的技巧。

typescript 复制代码
import { ref, computed, Ref, ComputedRef } from 'vue'

// 明确指定 ref 的类型
const count: Ref<number> = ref(0)
// 或者让 TypeScript 自动推断
const count = ref<number>(0)

// computed 的类型
const doubleCount: ComputedRef<number> = computed(() => count.value * 2)

// 在组合式函数中返回类型
function useCounter(initialValue: number = 0) {
  const count = ref(initialValue)
  
  const increment = () => {
    count.value++
  }
  
  return {
    count: count as Ref<number>, // 或者用 readonly
    increment
  }
}

// 使用泛型
function useAsyncData<T>(url: string) {
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<Error | null>(null)
  
  const fetchData = async () => {
    loading.value = true
    try {
      const response = await fetch(url)
      data.value = await response.json()
    } catch (e) {
      error.value = e as Error
    } finally {
      loading.value = false
    }
  }
  
  return {
    data: readonly(data),
    loading: readonly(loading),
    error: readonly(error),
    fetchData
  }
}

21. 性能优化的组合拳

把这些 API 组合起来,可以实现更强大的性能优化。

javascript 复制代码
import { shallowRef, markRaw, computed, watchEffect } from 'vue'

// 场景:处理大量数据列表
function useLargeList(initialData) {
  // 用 shallowRef 避免深度响应式
  const items = shallowRef(initialData.map(item => ({
    ...item,
    // 某些不需要响应式的数据用 markRaw
    metadata: markRaw(item.metadata)
  })))
  
  // 用 computed 缓存计算结果
  const filteredItems = computed(() => {
    return items.value.filter(item => item.visible)
  })
  
  // 用 watchEffect 处理副作用,自动清理
  const stop = watchEffect((onInvalidate) => {
    // 处理过滤后的数据
    processItems(filteredItems.value)
    
    onInvalidate(() => {
      // 清理工作
      cleanup()
    })
  })
  
  return {
    items,
    filteredItems,
    stop
  }
}

22. 实际项目中的最佳实践

结合我自己的项目经验,分享几个实用的模式:

模式1:响应式数据的初始化

javascript 复制代码
import { ref, reactive, onMounted, readonly } from 'vue'

function useUserData(userId) {
  const user = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const fetchUser = async () => {
    loading.value = true
    error.value = null
    try {
      const response = await fetch(`/api/users/${userId}`)
      user.value = await response.json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }
  
  // 组件挂载时自动获取
  onMounted(fetchUser)
  
  return {
    user: readonly(user),
    loading: readonly(loading),
    error: readonly(error),
    refetch: fetchUser
  }
}

模式2:表单验证

javascript 复制代码
import { ref, computed, reactive, readonly } from 'vue'

function useFormValidation() {
  const form = reactive({
    email: '',
    password: '',
    confirmPassword: ''
  })
  
  const errors = reactive({})
  
  const validateEmail = (email) => {
    return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
  }
  
  // 用 computed 实时验证
  const isValid = computed(() => {
    errors.email = form.email && !validateEmail(form.email) 
      ? '邮箱格式不正确' 
      : ''
    
    errors.password = form.password.length < 6 
      ? '密码至少6位' 
      : ''
    
    errors.confirmPassword = form.password !== form.confirmPassword 
      ? '两次密码不一致' 
      : ''
    
    return !Object.values(errors).some(err => err)
  })
  
  return {
    form,
    errors: readonly(errors),
    isValid
  }
}

模式3:防抖和节流的组合使用

javascript 复制代码
import { customRef, watch } from 'vue'

// 防抖 ref - 基于 customRef 实现
function useDebounceRef(value, delay = 300) {
  let timeout
  return customRef((track, trigger) => {
    return {
      get() {
        track()
        return value
      },
      set(newValue) {
        clearTimeout(timeout)
        timeout = setTimeout(() => {
          value = newValue
          trigger()
        }, delay)
      }
    }
  })
}

// 节流函数 - 配合 watch 使用(这是纯 JS 函数,不是 Vue3 API,但经常和 Vue3 一起用)
function useThrottleFn(fn, delay = 300) {
  let lastTime = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastTime >= delay) {
      fn.apply(this, args)
      lastTime = now
    }
  }
}

// 使用
const searchText = useDebounceRef('')

watch(searchText, (newVal) => {
  // 搜索逻辑,只在用户停止输入 300ms 后执行
  performSearch(newVal)
})

const handleScroll = useThrottleFn(() => {
  // 滚动处理,每 300ms 最多执行一次
  updateScrollPosition()
}, 100)

版本要求说明

部分 API 需要特定版本的 Vue3:

  • Vue 3.2+effectScopeonScopeDisposev-memo
  • Vue 3.0+:其他大部分 API

建议使用 Vue 3.2 或更高版本,以获得完整的 API 支持。

常见陷阱和注意事项

  1. shallowRef/shallowReactive 的陷阱:修改嵌套属性不会触发更新,如果需要更新,要替换整个对象。

  2. toRaw 的局限性:只能获取一层的原始对象,嵌套的响应式对象仍然是 Proxy。

  3. getCurrentInstance 的使用限制 :只能在 setup 或生命周期钩子中调用,不能缓存到异步回调中使用。

  4. inject 的默认值 :如果父组件没有提供值,inject 会返回 undefined,建议总是提供默认值。

  5. watchEffect 的依赖追踪:会自动追踪所有在函数内访问的响应式数据,注意不要访问不需要追踪的数据。

  6. computed 的副作用 :不要在 computed 中执行副作用操作(如修改 DOM、发送请求等),应该使用 watchwatchEffect

总结下

有些API你可能不常用,但在特定场景下真的能解决实际问题。性能优化、边界情况处理、自定义逻辑,这些场景下它们都能派上用场。

不过也要注意,不要过度使用。比如 shallowRef 虽然性能好,但如果你确实需要深度响应式,就不要为了性能而牺牲功能。工具是死的,场景是活的,根据实际需求选择最合适的方案才是王道。。。

相关推荐
小只笨笨狗~2 小时前
css-文字背景渐变色
前端·css·html
BingoGo2 小时前
CSS 也要支持 if 了 !!!CSS if() 函数来了!
前端·css
用户6600676685392 小时前
深入解析JavaScript数组:从内存原理到高效遍历实践
前端·javascript
有点笨的蛋2 小时前
CSS 定位彻底搞懂:五种 position 的真实差异与最佳实践
前端·css
液态不合群2 小时前
数字化转型改变了什么?从技术底层到业务本质的深度重构
前端·人工智能·低代码·重构
qiao若huan喜2 小时前
9、webgl 基本概念 + 复合变换 + 平面内容复习
前端·javascript·信息可视化·webgl
梦幻通灵3 小时前
Edge浏览器好用插件【持续更新】
前端·edge
sTone873753 小时前
Chrome devtools二次开发准备:获取源码和编译
前端·google
创码小奇客3 小时前
Spring Boot依赖排坑指南:冲突、循环依赖全解析+实操方案
后端·面试·架构