vue3新增的api - 完整解决方案与实战教程
在Vue 2时代,我们习惯用Options API组织代码,逻辑分散在data、methods、computed、watch中。当项目升级到Vue 3后,团队开始尝试Composition API中的新API,比如ref、reactive、toRefs、watchEffect、shallowRef、markRaw等。但很多开发者直接照搬Vue 2的思维,结果踩坑不断:响应式丢失、watchEffect无限循环、shallowRef与reactive混用导致视图不更新、模板ref与响应式ref混淆、以及setup中访问this为undefined。这些痛点轻则导致页面数据不刷新,重则引发内存泄漏和难以定位的性能问题。本文从真实业务场景出发,逐一拆解Vue 3新增API的常见陷阱,并给出可直接落地的完整代码方案。
问题现象:升级Vue 3后,数据变了视图不动
典型现象包括:
- 使用
reactive定义的对象,整体替换后视图不更新。 - 从
reactive对象中解构出来的属性,修改后页面无变化。 watchEffect内部修改了依赖自身的数据,导致无限递归报错。- 用
shallowRef包裹复杂对象,修改深层属性后视图不刷新。 - 在
setup中通过this.$refs获取DOM,结果报错undefined。
原因分析:Vue 3响应式系统的底层逻辑变了
Vue 3使用Proxy替代Object.defineProperty,响应式追踪基于代理对象的属性访问。以下是最容易出错的几个原因:
- reactive解构丢失响应性:解构相当于把属性值复制给新变量,新变量不再经过Proxy,因此失去追踪能力。必须用
toRefs或toRef保持引用。 - 整体替换reactive对象:
state = reactive({...})后直接state = newObj会切断原代理,应使用Object.assign或ref。 - watchEffect自动收集依赖:如果回调中修改了被读取的响应式数据,会再次触发回调,形成死循环。需用
watch显式指定源,或加条件判断。 - shallowRef只追踪.value整体替换:修改
shallowRef内部对象的属性不会触发更新,必须整体替换.value。 - 模板ref与响应式ref命名冲突:Vue 3中
ref既用于响应式数据又用于模板引用,若变量名与模板ref属性同名,setup中返回的ref会覆盖模板引用。
解决方案(附完整代码)
下面通过一个“用户列表+搜索+分页”的实战组件,演示如何正确使用Vue 3新增API并避开上述坑。
<template>
<div>
<!-- 模板ref:注意变量名不要与响应式ref重名 -->
<input ref="searchInputRef" v-model="keyword" />
<ul>
<li v-for="user in userList" :key="user.id">{{ user.name }}</li>
</ul>
<button @click="loadMore">加载更多</button>
</div>
</template>
<script setup>
import { ref, reactive, toRefs, watch, watchEffect, shallowRef, markRaw, onMounted } from 'vue'
// 1. 基础响应式:ref用于基本类型,reactive用于对象
const keyword = ref('')
const page = ref(1)
// 2. reactive对象解构必须用toRefs,否则丢失响应性
const state = reactive({
loading: false,
total: 0,
list: []
})
// 正确:解构后仍是ref,模板中自动解包
const { loading, total, list } = toRefs(state)
// 3. 整体替换reactive对象:用Object.assign或ref
// 错误示范:state = reactive({ ... }) 会切断原代理
// 正确示范:
function resetState() {
Object.assign(state, {
loading: false,
total: 0,
list: []
})
}
// 4. shallowRef:只追踪.value整体替换,适合大数组性能优化
const bigData = shallowRef([])
// 错误:bigData.value.push(item) 不会触发更新
// 正确:bigData.value = [...bigData.value, item]
// 5. markRaw:标记永不响应式的对象,避免性能开销
const staticConfig = markRaw({
api: '/api/user',
pageSize: 20
})
// 6. watchEffect:自动收集依赖,但避免修改依赖自身
watchEffect(() => {
// 只读取keyword,不修改它,否则死循环
console.log('keyword changed:', keyword.value)
// 若需要修改,用watch显式监听
})
// 7. watch:显式指定源,可拿到新旧值,适合异步请求
watch(page, async (newPage) => {
state.loading = true
try {
const res = await fetch(`${staticConfig.api}?page=${newPage}&size=${staticConfig.pageSize}`)
const data = await res.json()
// 注意:这里直接修改state.list,因为state是reactive
state.list = data.list
state.total = data.total
} finally {
state.loading = false
}
})
// 8. 模板ref:必须与响应式ref区分命名
const searchInputRef = ref(null)
onMounted(() => {
// 正确获取DOM
searchInputRef.value?.focus()
})
// 9. 模拟加载更多:用shallowRef整体替换
function loadMore() {
const newItem = { id: Date.now(), name: `User ${Date.now()}` }
// 错误:bigData.value.push(newItem)
bigData.value = [...bigData.value, newItem]
}
// 10. 暴露给模板
const userList = list
</script>
排查步骤与最佳实践:
- 检查所有从
reactive解构的地方,是否用了toRefs或toRef。 - 避免直接给
reactive变量重新赋值,改用Object.assign或改用ref。 - 使用
watchEffect时,确保回调内不修改被读取的响应式数据;如需修改,改用watch并指定源。 - 对于大型只读数据或第三方类实例,使用
shallowRef或markRaw提升性能。 - 模板ref变量名不要与响应式ref重名,建议加
Ref后缀,如searchInputRef。 - 在
setup中不能使用this,所有逻辑通过导入的API完成。
掌握这些新增API的边界条件后,Vue 3的Composition API能带来更清晰的逻辑复用和更好的性能。建议在团队内建立代码规范,强制使用toRefs解构、禁止直接替换reactive对象,并配合ESLint插件eslint-plugin-vue的vue/no-ref-as-operand等规则,从工具层面规避低级错误。