Jym好😘,我是珑墨,今天给大家分享 vue3常见坑点和使用技巧 ,嘎嘎的😍,看下面。
一、响应式系统全景剖析
1. ref、reactive、shallowRef、shallowReactive、readonly、markRaw
- ref:适合基本类型,也可包裹对象。对象时模板自动解包,JS 里需 .value。
- reactive:适合对象和数组,不能包裹基本类型(会失效)。
- shallowRef/shallowReactive:只对第一层做响应式,深层属性变动不会触发更新。适合大对象、性能敏感场景。
// shallowRef 与 shallowReactive
import { shallowRef, shallowReactive } from 'vue';
const obj = shallowRef({ a: 1 });
obj.value.a = 2; // 不会触发响应式更新,只有 obj.value = 新对象 才会触发
const arr = shallowReactive([{ a: 1 }]);
arr[0].a = 2; // 不会触发响应式更新,只有 arr[0] = 新对象 才会触发
- readonly:只读响应式,防止数据被修改。注意只读是浅层的。
import { reactive, readonly } from 'vue';
const state = reactive({ count: 0 });
const roState = readonly(state);
roState.count++; // 警告:只读属性不能修改
- markRaw:标记对象为“永远不响应式”,适合第三方库实例、DOM 节点等。
import { markRaw, reactive } from 'vue';
const dom = markRaw(document.createElement('div'));
const state = reactive({ dom }); // dom 不会被代理
常见坑:
- reactive 不能嵌套 ref,会丢失响应性。
- shallowRef 适合大对象频繁整体替换,避免深层追踪性能损耗。
- readonly 只读限制是浅层的,深层对象仍可被修改。
- markRaw 后对象彻底不响应式,慎用。
state.foo = 2; // foo 变成普通值,响应性丢失
2. 响应式丢失的场景
- 解构 reactive 对象属性,响应性丢失。
// 用 toRefs、toRef 保持响应性。
import { reactive, toRefs } from 'vue';
const state = reactive({ count: 0 });
const { count } = state; // 这样 count 不是响应式的
const { count: reactiveCount } = toRefs(state); // 这样才是响应式的
- 直接赋值替换 reactive 对象,响应性丢失。
// 替换对象用 ref 包裹整个对象。
import { reactive } from 'vue';
let state = reactive({ a: 1 });
state = { a: 2 }; // 这样 state 不是响应式的了
// 正确做法:用 ref 包裹整个对象
const state = ref({ a: 1 });
state.value = { a: 2 };
- 通过 Object.assign、Array.prototype.map 等方法操作,响应性可能丢失。
- 传递响应式对象到第三方库,可能被“脱壳”。
//传递到第三方库前用 toRaw。
import { reactive, toRaw } from 'vue';
const state = reactive({ a: 1 });
thirdPartyLib(toRaw(state)); // 传递原始对象,避免 Proxy 问题
3. Proxy 限制
- 不能代理已被代理的对象。
// 注意:不能代理已被代理的对象、DOM 节点、class 实例、Symbol 属性。
reactive(document.createElement('div')); // 不会代理 DOM 节点
- 不能代理 DOM 节点。
- 不能代理 class 实例(会丢失方法)。
- 不能代理 Symbol 属性。
二、组合式 API 的“易混点”
1. setup 的参数和返回值
export default {
setup(props, { emit, slots, attrs, expose }) {
// props: 响应式的props
// emit: 触发自定义事件
// slots: 插槽
// attrs: 未声明的属性
// expose: 暴露方法给父组件
return {
// 返回的属性会暴露给模板
}
}
}
- setup(props, { emit, slots, attrs, expose })
- 返回对象的属性会暴露给模板。
- 返回渲染函数时,模板失效。
- setup 里没有 this,不要再用 this.xxx。
2. defineProps、defineEmits、defineExpose、defineOptions
<script setup>
const props = defineProps<{ msg: string }>();
const emit = defineEmits<{
(e: 'update', value: string): void
}>();
function updateValue(val: string) {
emit('update', val);
}
defineExpose({ updateValue }); // 暴露给父组件
defineOptions({ props: { title: String, active: Boolean }, emits: ['click', 'update'] })
</script>
- defineProps 只能在
<script setup> 顶层调用。 - defineEmits 只能在
<script setup> 顶层调用。 - defineExpose 用于暴露方法给父组件。
- defineOptions 用于设置组件名、继承属性等(⚠️注意:
defineOptions并不是 Vue 官方 API,而是由 Volar 插件 提供的编译时辅助宏,用于增强<script setup>的功能。在<script setup>语法中,默认不支持直接设置组件的 name、props、emits等选项,因为这些通常需要在defineComponent()中定义。而defineOptions提供了一种简洁的方式来声明这些信息)。
3. script setup 的“语法糖陷阱”
<script setup>
// 不能用 export default
// 不能用 this
// 不能用 data、methods、computed 等选项式 API
</script>
-
不能在
<script setup> 里用 export default。 -
不能用 this。
-
不能用 data、methods、computed 等选项式 API。
-
不能在 <script setup> 里用 name 选项,需用 defineOptions。
三、生命周期钩子全对照
| Vue 2 | Vue 3 组合式 API | 说明 |
|---|---|---|
| beforeCreate | setup | 初始化前 |
| created | setup | 初始化后 |
| beforeMount | onBeforeMount | 挂载前 |
| mounted | onMounted | 挂载后 |
| beforeUpdate | onBeforeUpdate | 更新前 |
| updated | onUpdated | 更新后 |
| beforeDestroy | onBeforeUnmount | 卸载前 |
| destroyed | onUnmounted | 卸载后 |
| errorCaptured | onErrorCaptured | 错误捕获 |
| activated | onActivated | keep-alive 激活 |
| deactivated | onDeactivated | keep-alive 失活 |
常见坑:
- 生命周期钩子只能在 setup 顶层调用,不能在回调、条件语句中调用。
- onUnmounted 适合做清理定时器、事件监听等。
- onErrorCaptured 只能捕获子组件错误,不能捕获自身 setup 错误。
四、watch、watchEffect、computed 的“易错点”
1. watch
import { ref, watch } from 'vue';
const count = ref(0);
watch(count, (newVal, oldVal) => {
console.log('count changed:', newVal, oldVal);
}, { immediate: true }); // 立即执行一次
// 监听对象
const obj = ref({ a: 1 });
watch(obj, () => { /* ... */ }, { deep: true }); // 深度监听
-
默认惰性,需 immediate: true 立即执行。
-
监听对象默认浅层,需 deep: true 深度监听。
-
监听多个值用数组。
-
监听 ref 时自动解包,无需 .value。
-
监听 props 时,props 变动才会触发,props 内部对象变动不会触发。
2. watchEffect
import { ref, watchEffect } from 'vue';
const count = ref(0);
watchEffect(() => {
console.log('count is', count.value);
// 自动收集依赖,count 变动时自动执行
});
- 自动收集依赖,依赖变动自动执行。
- 不能精确控制依赖,易死循环。
- 返回清理函数用于副作用清理。
- ⚠️不要在 watchEffect 里修改依赖数据,否则死循环。
3. computed
import { ref, computed } from 'vue';
const count = ref(0);
const double = computed(() => count.value * 2);
const plus = computed({
get: () => count.value + 1,
set: val => { count.value = val - 1 }
});
- 只读 computed 用法:const val = computed(() => ...)
- 可写 computed 用法:const val = computed({ get, set })
- computed 依赖的响应式数据变动才会重新计算。
- ⚠️computed 不能异步返回 Promise,否则模板渲染异常。
五、模板语法与指令的“新特性”
1. v-model 多值绑定
// 父组件:
<MyInput v-model:title="title" v-model:content="content" />
//子组件实现:
<script setup>
const props = defineProps(['title', 'content']);
const emit = defineEmits(['update:title', 'update:content']);
</script>
- 支持多个 v-model,需自定义 prop 和事件名。
- 默认 modelValue 和 update:modelValue。
2. v-for 和 v-if 的优先级
<!-- 错误用法 -->
<div v-for="item in list" v-if="item.show">{{ item.name }}</div>
<!-- 正确用法 -->
<div v-for="item in list.filter(i => i.show)">{{ item.name }}</div>
- v-for 优先于 v-if,避免同元素同时用。
- 建议用计算属性过滤后再 v-for。
3. v-bind、v-on 的新写法
<MyComp v-bind="attrs" v-on="listeners" />
- 支持 v-bind="object" 批量绑定属性。
- 支持 v-on="object" 批量绑定事件。
4. Fragment、多个根节点
<template>
<header>头部</header>
<main>内容</main>
<footer>底部</footer>
</template>
- Vue 3 支持组件返回多个根节点,无需外层 div。
5. v-memo
<div v-memo="[props.id]">
<!-- 只有 props.id 变动才会重新渲染 -->
</div>
- Vue 3.2+ 新增,缓存静态节点,提升性能。
6. v-is 动态组件
Vue 3 推荐使用 <component :is="xxx"> 来实现动态组件切换, v-is 是一个非常实用的指令,主要用于动态渲染组件。它解决了 <component> 标签中不能使用 v-for、v-if 等指令的问题,并且可以更灵活地绑定动态组件。
`v-is` 是 `<component :is="...">` 的高级替代方案,适用于需要在动态组件上使用 `v-if`、`v-for` 等指令的场景。
// 如果你希望在这个组件上使用 `v-if`、`v-for` 或其他指令时,会遇到限制。这时就可以使用 `v-is` 指令来替代。
<template> <component :is="currentComponent" />
</template>
<script setup>
import Home from './components/Home.vue'
import About from './components/About.vue'
const currentComponent = ref(Home)
</script>
支持 v-for 动态创建多个组件(⚠️ 注意:你不能在 <component> 上直接使用 v-for,但可以通过 v-is 绕过这个限制。)
<template>
<div v-for="item in components" :key="item.name" v-is="item.component"></div>
</template>
<script setup>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
const components = [
{ name: 'A', component: ComponentA },
{ name: 'B', component: ComponentB }
]
</script>
支持 v-if 判断渲染
<div v-if="showComponent" v-is="currentComponent"></div>
支持异步加载组件(懒加载)
<template> <div v-is="asyncComponent" /> </template> <script setup> const asyncComponent = defineAsyncComponent(() => import('./MyComponent.vue')) </script>
六、TypeScript 深度集成
1. props 类型推断
<script setup lang="ts">
const props = defineProps<{ foo: string, bar?: number }>();
</script>
- defineProps<{ foo: string }>() 自动推断类型。
- 复杂类型需手动声明。
2. emits 类型推断
<script setup lang="ts">
const emit = defineEmits<{
(e: 'change', value: string): void
}>();
</script>
- defineEmits<{ (e: 'change', value: string): void }>()
3. 组件实例类型
- defineExpose 暴露方法,父组件用 ref 获取实例类型。
4. 泛型组件
- <script setup lang="ts" generic="T"> 支持泛型组件(需 3.3+)。
<script setup lang="ts" generic="T">
const props = defineProps<{ value: T }>();
</script>
七、异步组件、Suspense、Teleport
1. 异步组件
import { defineAsyncComponent } from 'vue';
const AsyncComp = defineAsyncComponent({
loader: () => import('./Comp.vue'),
loadingComponent: Loading,
errorComponent: Error,
delay: 200,
timeout: 3000
});
- 支持 loading/error 组件和超时处理。
2. Suspense
-
它是一个 Vue 内置组件;
-
可以包裹带有异步操作的组件(如异步加载的子组件、异步获取的数据);
-
提供了两个插槽:
#default:主内容(异步部分)#fallback:加载期间显示的内容(可选)
💡 这种方式替代了 Vue 2 中需要手动管理 loading 状态的做法。
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>加载中... ⏳</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
</script>
3. Teleport
<Teleport to="body">
<div>我是弹窗</div>
</Teleport>
- 实现弹窗、全局挂载等场景,让节点挂到任意位置。
八、依赖注入 provide/inject 的“响应性陷阱”
import { provide, inject, ref } from 'vue';
// 父组件
const count = ref(0);
provide('count', count);
// 子组件
const count = inject('count');
console.log(count.value); // 响应式
- 直接 provide 普通值,inject 后不是响应式。
- 需用 ref/reactive 包裹后 provide,inject 后才响应式。
- inject 默认值只在 provide 未提供时生效,provide 为 undefined 时不会用默认值。
九、常见性能陷阱
- 大量响应式数据建议用 shallowReactive。
- 频繁变动大对象用 shallowRef。
import { shallowReactive, shallowRef } from 'vue';
const bigObj = shallowReactive({ /* ... */ }); // 只追踪第一层
const arr = shallowRef([/* ... */]); // 只追踪整体变动
- computed、watchEffect 依赖要精准,避免无用依赖导致性能浪费。
- v-for 建议加唯一 key,避免 diff 性能问题。
- v-if/v-for 嵌套建议用计算属性优化。
十、社区高频“神坑”补充
| 坑点描述 | 解决方案 |
|---|---|
| reactive 不能包裹 ref | 只用 ref 或只用 reactive,不要混用 |
| 组件 emits 未声明 | 用 defineEmits 明确声明 |
| v-for key 用 index | 尽量用唯一 id,避免性能问题 |
| watchEffect 死循环 | 依赖收集要精准,避免副作用 |
| 依赖注入 provide/inject 响应性丢失 | 用 ref/reactive 包裹注入值 |
| 组件未注册 | Vue 3 需手动注册组件或用自动导入 |
| 事件冒泡丢失 | 事件修饰符 .stop、.prevent 用法变动 |
| v-if/v-for 嵌套导致渲染异常 | 用计算属性优化 |
| 组件 name 不唯一导致 keep-alive 缓存异常 | 明确设置组件 name: ( defineOptions({ name: 'accountList' })) 或者 export default {name:'accountList'} 或者检查router配置的routerName是否正确 |
| 组件 props 默认值为对象/数组 | 必须用工厂函数返回新对象/数组 |
| 依赖循环导致响应式失效 | 拆分依赖,避免循环引用 |
| 组件样式穿透 | 用 :deep() 代替 /deep/ 或 >>> |
| 事件监听未解绑 | 在 onUnmounted 里解绑 |
| 组件未销毁导致内存泄漏 | 检查定时器、事件监听、全局状态 |
| v-model 绑定对象属性丢失响应 | 用 computed 包裹 |
| 组件 slot 作用域丢失 | 用 v-slot 明确声明 |
| 组件 props 校验失效 | 用 defineProps 明确类型 |
| 组件 emits 校验失效 | 用 defineEmits 明确类型 |
| 组件样式污染 | 用 scoped 或 CSS Modules |
十一、实用技巧锦囊(进阶)
- useAttrs/useSlots 获取未声明的属性和插槽,适合高阶组件。
- 自定义指令:Vue 3 指令钩子变更,需适配新 API。
- v-memo:Vue 3.2+ 新增,缓存静态节点,提升性能。
- defineAsyncComponent 支持 loading/error 组件和超时处理。
- emit 事件校验:defineEmits 支持类型校验,减少低级错误。
- 全局 API 变更:Vue 3 全局 API 需通过 app.config.globalProperties 挂载。
- 全局注册组件/指令:需通过 app.component、app.directive 注册。
- 自定义渲染器:Vue 3 支持自定义渲染器(如 vueuse/headless-ui)。
- SSR 支持:Vue 3 SSR API 变更,需适配新写法。
- useVModel:vueuse 提供 useVModel,简化 v-model 双向绑定。
- defineExpose:暴露方法给父组件,适合表单校验、重置等场景。
- useSlots/useAttrs:获取插槽和未声明属性,适合高阶组件。
- v-bind="$attrs":传递未声明属性到子组件。
- v-on="attrs。
- defineOptions:设置组件名、inheritAttrs 等。
- defineModel:Vue 3.4+ 新增,简化 v-model 绑定。
- useCssVars:动态 CSS 变量绑定。
- expose:自定义渲染函数时暴露方法。
- useRoute/useRouter:Vue Router 4 组合式 API。
- useStore:Pinia 组合式 API 替代 Vuex。
十二、常见报错与调试技巧
[Vue warn]: Avoid mutating a prop directly
// 错误
props.value = 1;
// 正确
emit('update:value', 1);
解决:props 只读,需用中间变量或 emit 更新。
[Vue warn]: Extraneous non-props attributes
<input v-bind="$attrs" />
解决:用 v-bind="$attrs" 传递未声明属性。
[Vue warn]: Failed to resolve component
解决:检查组件注册和导入路径。
响应式失效
解决:检查是否解构、赋值、嵌套 ref/reactive。
[Vue warn]: Invalid prop
解决:props 类型声明与传入类型不符。
[Vue warn]: Missing required prop
解决:props 必填未传。
[Vue warn]: Unknown custom element
解决:组件未注册或拼写错误。
[Vue warn]: v-for key duplicate
解决:key 不唯一,需用唯一 id。
[Vue warn]: v-model binding type error
解决:v-model 绑定类型不符,需类型一致。
十三、团队协作与大型项目踩坑
- 统一响应式风格:团队约定 ref/ reactive 用法,避免混用。
- 统一 props/emits 类型声明:用 TypeScript 明确 props/emits 类型。
- 组件命名规范:统一 PascalCase,避免 name 冲突。
- 全局状态管理:推荐 Pinia,避免 Vuex 2/3 兼容性问题。
- 自动导入:用 unplugin-auto-import、unplugin-vue-components 提升开发效率。
- 样式隔离:用 scoped、CSS Modules、BEM 命名法。
- 代码分割:用异步组件、Suspense 优化首屏加载。
- SSR/SSG 支持:用 Nuxt 3、Vite SSR,注意 API 差异。
- 国际化:用 vue-i18n@next,注意组合式 API 支持。
- 测试:用 Vitest、Vue Test Utils 3,适配新 API。
十四、生态相关踩坑
- Vue Router 4:API 变更大,需适配 useRoute/useRouter。
- Pinia 替代 Vuex:API 更简洁,支持组合式 API。
- 第三方 UI 库:需用 Vue 3 版本(如 Element Plus、Naive UI)。
- Vite 替代 Webpack:配置更简单,需适配插件生态。
- unplugin-auto-import:自动导入 API,避免手动引入。
- unplugin-vue-components:自动注册组件,提升开发效率。
- vueuse:大量组合式 API 工具,提升开发效率。
十五、边角与冷门坑点
- v-once(只渲染一次)/v-pre:Vue 3 依然支持,适合静态内容优化。
- v-html:依然有 XSS 风险,需谨慎使用。
- v-memo:缓存静态节点,适合大表格、长列表。
- v-is:动态组件,适合表单生成器等场景。
- v-slot:作用域插槽,避免 slot 作用域丢失。
- v-bind.sync:已废弃,需用 v-model 替代。
- listeners 到 $attrs。
- inheritAttrs:可用 defineOptions 设置。
- 自定义事件名大小写:HTML 属性不区分大小写,建议用短横线。
- 组件递归调用:需注册组件自身。
- keep-alive include/exclude:需用组件 name 匹配。
- 动态 class/style:推荐用对象/数组写法。
- v-for 嵌套 v-if:建议用计算属性优化。
- v-for 绑定 Map/Set:需用 Array.from 转换。
<div v-for="item in Array.from(mySet)" :key="item.id">{{ item }}</div>
- v-model 修饰符:.number、.trim、.lazy 依然支持。
- v-model 绑定数组项:需用 computed 包裹。
- v-model 绑定多层对象:需用 computed 包裹。
- v-model 绑定自定义组件:需实现 modelValue/update:modelValue。
- v-model 绑定原生表单控件:依然支持。
- v-model 绑定 checkbox/radio:需注意 true-value/false-value。
十六、性能优化技巧
- v-memo:缓存静态节点,提升渲染性能。
- shallowRef/shallowReactive:减少深层追踪,提升性能。
- 异步组件:按需加载,减少首屏体积。
- Suspense:异步加载时显示 loading。
- Teleport:弹窗、全局挂载,减少 DOM 层级。
- computed 缓存:避免重复计算。
- watchEffect 精准依赖:避免无用依赖导致性能浪费。
- v-for key 唯一:提升 diff 性能。
- 事件节流/防抖:避免高频事件导致性能问题。
- 图片懒加载:减少首屏加载压力。
十七、调试与开发工具
-
Vue Devtools:支持 Vue 3,调试响应式、组件树、事件。
-
Vite HMR:热更新更快,开发体验提升。
-
TypeScript 类型提示:用 Volar 插件,支持 <script setup lang="ts">。
-
eslint-plugin-vue:规范代码风格,避免低级错误。
-
vite-plugin-inspect:调试 Vite 插件、依赖分析。
-
vite-plugin-vue-devtools:增强 Vue 3 调试能力。
十八、常见报错与解决方案
[Vue warn]: Avoid mutating a prop directly
解决:props 只读,需用中间变量或 emit 更新。
[Vue warn]: Extraneous non-props attributes
解决:用 v-bind="$attrs" 传递未声明属性。
[Vue warn]: Failed to resolve component
解决:检查组件注册和导入路径。
[Vue warn]: Invalid prop
解决:props 类型声明与传入类型不符。
[Vue warn]: Missing required prop
解决:props 必填未传。
[Vue warn]: Unknown custom element
解决:组件未注册或拼写错误。
[Vue warn]: v-for key duplicate
解决:key 不唯一,需用唯一 id。
[Vue warn]: v-model binding type error
解决:v-model 绑定类型不符,需类型一致。
[Vue warn]: inject() can only be used inside setup or functional components
解决:inject 只能在 setup 或函数式组件中用。
[Vue warn]: onXXX() can only be used inside setup
解决:生命周期钩子只能在 setup 顶层用。
十九、官方文档与社区资源推荐
- Vue 3 官方文档 cn.vuejs.org/
- Vue 3 RFCs github.com/vuejs/rfcs
- VueUse 组合式 API 工具库 vueuse.org/
- Pinia 状态管理 pinia.vuejs.org/
- Vite 官方文档 vitejs.dev/
- Element Plus element-plus.org/
- Naive UI www.naiveui.com/
结语
坑一个记一个! 最后送你一个防坑口诀:
响应式对象不要换,数组操作用方法
组件通信守规则,单向数据莫要违
性能优化用利器,虚拟滚动和memo
组合API是神器,自定义Hook显神威
Teleport和Suspense,异步加载很省心
Vite构建速度快,Devtools调试真不赖