Vue3 避坑指南:15个高频问题速解,新手也能少走弯路

0 阅读8分钟

用 Vue3 开发的同学都有过这样的经历:

明明代码看起来没毛病,却报奇奇怪怪的错;跟着教程写能跑,自己写就出问题;上线后莫名出现bug,排查半天找不到原因……

其实这些问题大多不是你技术不行,而是 Vue3 和 Vue2 有差异、组合式API有坑点,很多人踩过的坑,你完全可以直接避开。

今天整理了 15个 Vue3 高频踩坑问题,涵盖 setup 语法、响应式、路由、Pinia、组件通信等核心场景,每个问题都附「问题现象+原因+解决方案」,复制代码就能解决,新手必看!

一、setup 语法相关(最基础也最容易踩坑)

1. 问题:setup 中打印 ref 变量,看不到具体值

现象:用 ref 定义变量,console.log 打印出来是一个对象,看不到里面的 value 值。

// 错误示例
const count = ref(0)
console.log(count) // 打印 { value: 0 },不是直接的 0

原因:ref 用于包装简单类型,会返回一个响应式代理对象,必须通过 .value 访问值。

// 正确做法
const count = ref(0)
console.log(count.value) // 0

补充:模板中使用 ref 变量时,Vue 会自动解包,不需要写 .value。

2. 问题:setup 中使用生命周期钩子,报错“onMounted is not defined”

现象:直接在 setup 中写 onMounted,控制台报错,提示未定义。

// 错误示例
setup() {
  onMounted(() => {
    console.log('页面挂载完成')
  })
}

原因:Vue3 的生命周期钩子需要从 vue 中手动引入,不能直接使用。

// 正确做法
import { onMounted } from 'vue'

setup() {
  onMounted(() => {
    console.log('页面挂载完成')
  })
}

3. 问题:setup 中无法使用 this

现象:在 setup 中打印 this,结果是 undefined,无法访问组件实例。

原因:Vue3 的 setup 是在组件实例创建之前执行的,此时 this 还未绑定,自然无法使用。

解决方案:放弃使用 this,通过 defineProps、defineEmits、ref/reactive 等 API 实现对应功能:

// 正确做法(以获取props为例)
const props = defineProps({
  name: String
})
console.log(props.name) // 替代 this.name

二、响应式相关(最容易出错,影响功能稳定性)

4. 问题:reactive 定义的对象,新增属性不响应

现象:用 reactive 定义对象后,新增的属性修改时,页面不更新。

// 错误示例
const user = reactive({
  name: '张三'
})
// 新增属性,页面不响应
user.age = 25

原因:reactive 只能对初始定义的属性做响应式处理,新增的属性不会自动变成响应式。

// 正确做法(两种方式)
// 方式1:初始定义时,提前声明属性(推荐)
const user = reactive({
  name: '张三',
  age: undefined
})
user.age = 25

// 方式2:用 Vue3 提供的 toRefs 或 defineProps 扩展
import { toRefs } from 'vue'
const user = reactive({
  name: '张三'
})
const userRefs = toRefs(user)
userRefs.age = ref(25)

5. 问题:ref 定义的数组,修改元素不响应

现象:用 ref 定义数组,直接修改数组元素或长度,页面不更新。

// 错误示例
const list = ref([1, 2, 3])
// 修改元素,页面不响应
list.value[0] = 10
// 修改长度,页面不响应
list.value.length = 0

原因:ref 包装的数组,直接修改索引或长度,Vue 无法检测到变化。

// 正确做法(用数组方法或重新赋值)
// 方式1:使用数组方法(push、pop、splice 等)
list.value.splice(0, 1, 10) // 修改第一个元素
list.value.splice(0) // 清空数组

// 方式2:重新赋值(推荐,简洁)
list.value = [10, 2, 3]
list.value = []

6. 问题:解构 reactive 对象后,失去响应式

现象:解构 reactive 定义的对象,解构后的变量修改时,页面不更新。

// 错误示例
const user = reactive({
  name: '张三',
  age: 25
})
// 解构后,name 失去响应式
const { name } = user
name = '李四' // 页面不更新

原因:reactive 的响应式是基于代理对象的,解构后会变成普通变量,失去代理特性。

// 正确做法:用 toRefs 解构
import { toRefs } from 'vue'

const user = reactive({
  name: '张三',
  age: 25
})
// 解构后仍保持响应式
const { name, age } = toRefs(user)
name.value = '李四' // 页面更新

三、路由相关(后台系统高频踩坑)

7. 问题:setup 中无法使用 this.router/this.router / this.route

现象:在 setup 中使用 this.$router 跳转,报错“Cannot read property 'push' of undefined”。

原因:setup 中没有 this,自然无法访问 routerrouter 和 route。

// 正确做法:使用 useRouter 和 useRoute 钩子
import { useRouter, useRoute } from 'vue-router'

setup() {
  const router = useRouter() // 对应 this.$router
  const route = useRoute() // 对应 this.$route

  // 跳转路由
  const goHome = () => {
    router.push('/home')
  }

  // 获取路由参数
  console.log(route.params.id)
}

8. 问题:路由懒加载报错“Failed to resolve component”

现象:配置路由懒加载后,页面跳转时报错,提示无法解析组件。

// 错误示例
const routes = [
  {
    path: '/home',
    component: () => import('@/views/Home') // 缺少 .vue 后缀
  }
]

原因:Vite 对文件后缀要求严格,路由懒加载时,必须写全组件文件后缀(.vue)。

// 正确做法
const routes = [
  {
    path: '/home',
    component: () => import('@/views/Home.vue') // 补全 .vue 后缀
  }
]

9. 问题:路由守卫中,无法获取 Pinia 状态

现象:在 router.beforeEach 中,使用 useUserStore 获取用户状态,返回 undefined。

// 错误示例
router.beforeEach((to, from, next) => {
  const userStore = useUserStore()
  console.log(userStore.token) // undefined
})

原因:路由守卫执行时,Pinia 实例还未创建,无法获取 store。

// 正确做法:在路由守卫中延迟获取,或提前创建 Pinia 实例
// main.js 中提前创建 Pinia
import { createPinia } from 'pinia'
const pinia = createPinia()
app.use(pinia)

// 路由守卫中正常使用
router.beforeEach((to, from, next) => {
  const userStore = useUserStore()
  if (!userStore.token && to.path !== '/login') {
    next('/login')
  } else {
    next()
  }
})

四、Pinia 相关(状态管理高频坑)

10. 问题:Pinia 中修改状态,页面不更新

现象:在 Pinia 的 action 中修改 state,页面没有同步更新。

// 错误示例
export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: {
      name: '张三'
    }
  }),
  actions: {
    updateName() {
      // 直接替换对象,失去响应式
      this.userInfo = { name: '李四' }
    }
  }
})

原因:直接替换 Pinia 中的对象,会破坏响应式代理,导致页面不更新。

// 正确做法:修改对象属性,而非替换整个对象
export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: {
      name: '张三'
    }
  }),
  actions: {
    updateName() {
      this.userInfo.name = '李四' // 直接修改属性,保持响应式
    }
  }
})

11. 问题:Pinia 持久化,刷新页面状态丢失

现象:Pinia 中存储的 token、用户信息,刷新页面后就消失了。

原因:Pinia 的状态默认存储在内存中,刷新页面会清空内存。

// 正确做法:使用 pinia-plugin-persistedstate 实现持久化
// 1. 安装依赖
npm install pinia-plugin-persistedstate

// 2. main.js 配置
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)

// 3. 单个 store 配置持久化
export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userInfo: null
  }),
  persist: true // 开启持久化,默认存储在 localStorage
})

五、组件通信与其他高频坑

12. 问题:子组件向父组件传值,父组件接收不到

现象:子组件用 emit 触发事件,父组件绑定事件后,无法获取传过来的值。

// 子组件(错误示例)
const emit = defineEmits(['change'])
const handleChange = () => {
  emit('change') // 未传递参数
}

// 父组件
<Child @change="handleChange" />
const handleChange = (val) => {
  console.log(val) // undefined
}

原因:子组件 emit 事件时,没有传递参数,父组件自然接收不到。

// 正确做法:emit 时传递参数
// 子组件
const emit = defineEmits(['change'])
const handleChange = () => {
  emit('change', '传递的值') // 传递参数
}

// 父组件
<Child @change="handleChange" />
const handleChange = (val) => {
  console.log(val) // 传递的值
}

13. 问题:父组件调用子组件方法,报错“xxx is not a function”

现象:父组件用 ref 获取子组件实例,调用子组件方法时,提示方法不存在。

// 子组件(错误示例)
const resetForm = () => {
  console.log('重置表单')
}

// 父组件
<Child ref="childRef" />
const childRef = ref(null)
const handleReset = () => {
  childRef.value.resetForm() // 报错
}

原因:子组件的方法没有通过 defineExpose 暴露,父组件无法访问。

// 正确做法:子组件用 defineExpose 暴露方法
// 子组件
const resetForm = () => {
  console.log('重置表单')
}
// 暴露方法
defineExpose({ resetForm })

// 父组件
<Child ref="childRef" />
const childRef = ref(null)
const handleReset = () => {
  childRef.value?.resetForm() // 安全调用,避免报错
}

14. 问题:v-for 循环中,使用 v-model 绑定值错乱

现象:v-for 循环渲染输入框,修改其中一个输入框的值,其他输入框的值也跟着变。

<!-- 错误示例 -->
<div v-for="(item, index) in list" :key="index">
  <el-input v-model="item.value" />
</div>

<script setup>
const list = ref([{}]) // 初始值为一个空对象
</script>

原因:v-for 循环时,若数组元素是同一个引用(如空对象),修改一个会影响所有。

<!-- 正确做法:循环时创建独立对象 -->
<div v-for="(item, index) in list" :key="index">
  <el-input v-model="item.value" />
</div>

<script setup>
// 初始值为独立对象,避免引用共享
const list = ref([{ value: '' }])
// 新增时,也创建独立对象
const addItem = () => {
  list.value.push({ value: '' })
}
</script>

15. 问题:Vite 打包后,页面空白、报错“Cannot find module”

现象:开发环境正常运行,打包后打开 dist 文件夹下的 index.html,页面空白,控制台报错。

原因:Vite 打包默认的公共路径是 /,若部署在非根目录(如服务器子目录),会导致资源路径错误。

// 正确做法:修改 vite.config.js 中的 base 配置
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  base: './' // 改为相对路径,适配所有部署场景
})

写在最后

Vue3 的坑,大多集中在「响应式原理」和「API 用法差异」上。很多时候不是你写得不对,而是忽略了 Vue3 的设计逻辑。

以上 15 个问题,覆盖了 Vue3 开发中 80% 的高频踩坑场景,建议收藏起来,遇到问题时直接对照查找,能节省大量排查时间。

其实只要掌握了响应式原理、setup 语法、路由和 Pinia 的核心用法,很多坑都能提前避开。


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!