【Vue3】踩坑点 & 常用技巧大全(Vue3终极详细版)

966 阅读12分钟

1588bc60b6439a9e217137bbb7d56bd.png

Jym好😘,我是珑墨,今天给大家分享  vue3常见坑点和使用技巧  ,嘎嘎的😍,看下面。

1752648724456.png

一、响应式系统全景剖析

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> 语法中,默认不支持直接设置组件的 namepropsemits 等选项,因为这些通常需要在 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 2Vue 3 组合式 API说明
beforeCreatesetup初始化前
createdsetup初始化后
beforeMountonBeforeMount挂载前
mountedonMounted挂载后
beforeUpdateonBeforeUpdate更新前
updatedonUpdated更新后
beforeDestroyonBeforeUnmount卸载前
destroyedonUnmounted卸载后
errorCapturedonErrorCaptured错误捕获
activatedonActivatedkeep-alive 激活
deactivatedonDeactivatedkeep-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-forv-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

十一、实用技巧锦囊(进阶)

  1. useAttrs/useSlots 获取未声明的属性和插槽,适合高阶组件。
  2. 自定义指令:Vue 3 指令钩子变更,需适配新 API。
  3. v-memo:Vue 3.2+ 新增,缓存静态节点,提升性能。
  4. defineAsyncComponent 支持 loading/error 组件和超时处理。
  5. emit 事件校验:defineEmits 支持类型校验,减少低级错误。
  6. 全局 API 变更:Vue 3 全局 API 需通过 app.config.globalProperties 挂载。
  7. 全局注册组件/指令:需通过 app.component、app.directive 注册。
  8. 自定义渲染器:Vue 3 支持自定义渲染器(如 vueuse/headless-ui)。
  9. SSR 支持:Vue 3 SSR API 变更,需适配新写法。
  10. useVModel:vueuse 提供 useVModel,简化 v-model 双向绑定。
  11. defineExpose:暴露方法给父组件,适合表单校验、重置等场景。
  12. useSlots/useAttrs:获取插槽和未声明属性,适合高阶组件。
  13. v-bind="$attrs":传递未声明属性到子组件。
  14. v-on="listeners"Vue 3合并到 listeners":Vue 3 合并到 attrs。
  15. defineOptions:设置组件名、inheritAttrs 等。
  16. defineModel:Vue 3.4+ 新增,简化 v-model 绑定。
  17. useCssVars:动态 CSS 变量绑定。
  18. expose:自定义渲染函数时暴露方法。
  19. useRoute/useRouter:Vue Router 4 组合式 API。
  20. 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 绑定类型不符,需类型一致。


十三、团队协作与大型项目踩坑

  1. 统一响应式风格:团队约定 ref/ reactive 用法,避免混用。
  2. 统一 props/emits 类型声明:用 TypeScript 明确 props/emits 类型。
  3. 组件命名规范:统一 PascalCase,避免 name 冲突。
  4. 全局状态管理:推荐 Pinia,避免 Vuex 2/3 兼容性问题。
  5. 自动导入:用 unplugin-auto-import、unplugin-vue-components 提升开发效率。
  6. 样式隔离:用 scoped、CSS Modules、BEM 命名法。
  7. 代码分割:用异步组件、Suspense 优化首屏加载。
  8. SSR/SSG 支持:用 Nuxt 3、Vite SSR,注意 API 差异。
  9. 国际化:用 vue-i18n@next,注意组合式 API 支持。
  10. 测试:用 Vitest、Vue Test Utils 3,适配新 API。

十四、生态相关踩坑

  1. Vue Router 4:API 变更大,需适配 useRoute/useRouter。
  2. Pinia 替代 Vuex:API 更简洁,支持组合式 API。
  3. 第三方 UI 库:需用 Vue 3 版本(如 Element Plus、Naive UI)。
  4. Vite 替代 Webpack:配置更简单,需适配插件生态。
  5. unplugin-auto-import:自动导入 API,避免手动引入。
  6. unplugin-vue-components:自动注册组件,提升开发效率。
  7. vueuse:大量组合式 API 工具,提升开发效率。

十五、边角与冷门坑点

  1. v-once(只渲染一次)/v-pre:Vue 3 依然支持,适合静态内容优化。
  2. v-html:依然有 XSS 风险,需谨慎使用。
  3. v-memo:缓存静态节点,适合大表格、长列表。
  4. v-is:动态组件,适合表单生成器等场景。
  5. v-slot:作用域插槽,避免 slot 作用域丢失。
  6. v-bind.sync:已废弃,需用 v-model 替代。
  7. attrs 合并:Vue 3合并了 attrs 合并:Vue 3 合并了 listeners 到 $attrs。
  8. inheritAttrs:可用 defineOptions 设置。
  9. 自定义事件名大小写:HTML 属性不区分大小写,建议用短横线。
  10. 组件递归调用:需注册组件自身。
  11. keep-alive include/exclude:需用组件 name 匹配。
  12. 动态 class/style:推荐用对象/数组写法。
  13. v-for 嵌套 v-if:建议用计算属性优化。
  14. v-for 绑定 Map/Set:需用 Array.from 转换。
     <div v-for="item in Array.from(mySet)" :key="item.id">{{ item }}</div>
  1. v-model 修饰符:.number、.trim、.lazy 依然支持。
  2. v-model 绑定数组项:需用 computed 包裹。
  3. v-model 绑定多层对象:需用 computed 包裹。
  4. v-model 绑定自定义组件:需实现 modelValue/update:modelValue。
  5. v-model 绑定原生表单控件:依然支持。
  6. v-model 绑定 checkbox/radio:需注意 true-value/false-value。

十六、性能优化技巧

  1. v-memo:缓存静态节点,提升渲染性能。
  2. shallowRef/shallowReactive:减少深层追踪,提升性能。
  3. 异步组件:按需加载,减少首屏体积。
  4. Suspense:异步加载时显示 loading。
  5. Teleport:弹窗、全局挂载,减少 DOM 层级。
  6. computed 缓存:避免重复计算。
  7. watchEffect 精准依赖:避免无用依赖导致性能浪费。
  8. v-for key 唯一:提升 diff 性能。
  9. 事件节流/防抖:避免高频事件导致性能问题。
  10. 图片懒加载:减少首屏加载压力。

十七、调试与开发工具

  1. Vue Devtools:支持 Vue 3,调试响应式、组件树、事件。

  2. Vite HMR:热更新更快,开发体验提升。

  3. TypeScript 类型提示:用 Volar 插件,支持 <script setup lang="ts">。

  4. eslint-plugin-vue:规范代码风格,避免低级错误。

  5. vite-plugin-inspect:调试 Vite 插件、依赖分析。

  6. 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 顶层用。


十九、官方文档与社区资源推荐


结语

坑一个记一个! 最后送你一个防坑口诀:

	响应式对象不要换,数组操作用方法

	组件通信守规则,单向数据莫要违

	性能优化用利器,虚拟滚动和memo

	组合API是神器,自定义Hook显神威

	Teleport和Suspense,异步加载很省心

	Vite构建速度快,Devtools调试真不赖