很多人拿到 Vue3 的学习资料,第一反应就是刷文档、看视频,结果越学越乱,几天之后连ref和reactive的区别都开始犯迷糊。问题往往不在你不够努力,而是缺少一条清晰的主线。这篇内容我想把手头带新人时最常用的一套 Vue3 学习路线拆开来讲,从“为什么要这么学”到“每个阶段练什么”,尽量说透。无论你是刚接触前端的零基础小白,还是使用旧版框架多年、想平滑过渡的老手,这篇内容都能给你一个可以照着执行的完整思路。
先说结论:学 Vue3 最忌讳的就是一头扎进语法细节里。框架层面的东西更新太快,今天记了明天就可能过时,只有把“设计思路”和“依赖的基础”弄明白,后面的一切学习才走得稳。所以这篇路线我会按照“基础补全 → 核心语法 → 生态进阶 → 实战整合”四个阶段来拆,每个阶段会讲清楚学什么、怎么练、注意什么,也会把我见过的高频坑一并列出,希望能帮你少走一点弯路。
1. 先把 Vue3 到底“新”在哪搞清楚
1.1 组合式 API 不是换了个写法那么简单
Vue3 最直观的变化就是组合式 API 全面上位。很多教程开篇就甩代码,告诉你“以后要这么写”,但如果你不清楚这套设计要解决什么问题,学起来会非常别扭,总觉得“旧写法也不是不能用”。
传统的选项式 API 把代码按照data、computed、methods、watch这样的选项来组织。它的好处是结构清晰,新人上手快,坏处是一旦组件的逻辑变多,同一块业务相关的代码会被拆散到各处。比如一个“用户列表”模块,它的数据在data里,过滤逻辑在computed里,加载函数在methods里,监听器在watch里,当这几个部分各有一百行代码时,你想改一个关联性功能,得在文件上下反复跳转。
组合式 API 的做法,是把同一业务逻辑的所有代码放在一起,用一个函数来封装:
// 用户列表相关的所有逻辑,从数据到方法全部集中在这里 export function useUserList() { const users = ref<UserItem[]>([]) const keyword = ref('') const filteredUsers = computed(() => users.value.filter(u => u.name.includes(keyword.value)) ) async function loadUsers() { const data = await api.getUsers() users.value = data } return { users, keyword, filteredUsers, loadUsers } }组件内部只需要:
const { users, keyword, filteredUsers, loadUsers } = useUserList()这样的设计,本质上是把“关注点分离”从“文件内部”推向了“逻辑单元内部”。你学习组合式 API 的时候,不要把注意力只放在setup怎么写、ref怎么用,而是要理解它的核心意图:让逻辑可以被抽取、复用、组合。这样才能解释为什么 Vue3 鼓励你用自定义 Hook(也就是以use开头的函数)来组织代码,而不是把所有东西平铺在一个大组件里。
1.2 响应式原理是绕不开的内功
Vue3 的响应式系统几乎是新手学习路上的第一个分水岭,它直接决定了你踩坑数量的多少。Vue2 用的是Object.defineProperty对数据进行劫持,本身有诸多限制:新增属性无法被拦截、数组的索引操作响应不到、delete操作也无法触发更新。Vue3 全面换成了Proxy代理,可以拦截对象层面的所有操作,所以新增属性和删除属性都能被追踪到。
这也是为什么你在 Vue3 里会看到ref和reactive两套 API 并存——它们本质上都是对响应式数据的包装,只是使用方式不同:
| API | 适用场景 | 访问方式 | 注意事项 |
|---|---|---|---|
ref | 基本类型、对象都可以 | 通过.value访问 | 模板中自动解包,不需要写.value |
reactive | 只接受对象类型 | 直接属性访问 | 对 “解构赋值” 不友好,解构后会丢失响应式 |
这里新手最常犯的错,就是写let count = reactive(0)。reactive底层依赖Proxy,它没法对基本类型数据做代理,所以这行代码会直接报错。还有人对ref包裹对象时的行为感到困惑:其实ref接收对象后,内部会转用reactive来处理这个对象,所以你修改obj.value.name时仍然是响应式的。
理解响应式原理还有一个直接好处,就是能解释“为什么数据变了视图没更新”这类经典问题。大多数情况下,问题出在直接赋值给响应式对象的某个不存在的属性、在reactive对象上解构取出了普通变量、或者在异步回调里丢失了this。这几个场景,几乎每个新手都得经历一遍才能长记性。
2. 动手之前,先理清学习路线图
2.1 前置基础决定了你的天花板
很多人问“零基础能不能直接学 Vue3”,我的回答是:能,但会走得非常痛苦。Vue3 是框架,它帮你解决的问题是“数据和视图的同步”,但框架本身不会帮你解决 JavaScript 语言层面的问题。比如你在模板里写了{{ list.filter(x => x.age > 18).map(x => x.name) }},如果看不懂这个表达式的拆解顺序,出了问题根本不知道从哪里排查。
我建议在进入 Vue3 核心语法之前,按下述清单自查一遍,基础不牢就花一两周补齐,这属于必做功课后,不是可有可无:
- ES6 模块化:
import和export的各种写法,默认导出和命名导出的区别 - 箭头函数与普通函数的
this指向差异 - 解构赋值、展开运算符在对象和数组上的行为
Promise的基本用法,async / await写法的执行顺序- 数组方法:
map、filter、reduce、forEach,这个在模板表达式里非常常用 - 原型链和
this绑定逻辑,因为它直接影响你对函数调用上下文的理解
有一条实践经验可以分享:与其抽出两周时间专门磨 JavaScript 基础,不如一边看 Vue3 文档一边补。当你第一次在watch里写异步逻辑时发现回调顺序不对,再去回头查事件循环的机制,这时候的体会远比单独看书深刻。这其实是“项目驱动学习”的一种表现,但前提是你得知道自己的知识缺口在哪里。
2.2 一条值得参考的分阶段路线
我把整个 Vue3 学习旅程拆成五个阶段,每个阶段对应不同的能力目标:
- 基础速通阶段(约1周):了解组件、模板语法、数据绑定、事件处理、条件渲染和列表渲染。目标是能写一个由两三个组件组成的静态页面。
- 核心语法深挖阶段(约2周):全面掌握组合式 API、生命周期钩子、
computed、watch、组件通信。目标是能独立实现一个待办事项应用,并完成数据的增删改查。 - 生态进阶阶段(约2周):学 Vue Router、Pinia 和常用 UI 组件库。目标是做一个多页面、带登录鉴权和全局状态管理的完整应用。
- 工程化实战阶段(约3周):上手 Vite 项目结构、配置代理、接入 TypeScript、制定代码规范和自动化部署。目标是完成一个可供展示的中小型后台管理系统。
- 深入源码阶段(可选):阅读响应式系统和渲染器源码,理解
createApp的完整执行过程,写一个简单的构建工具链。
每个阶段之间不要跳步,尤其是不要从第一阶段直接跳到第四阶段。我见过太多人刚学会v-for就急着用脚手架去搭后台项目,结果目录结构看不懂、组件通信一塌糊涂,最后项目变成一坨巨大的单文件组件堆叠。
这五个阶段没有硬性的“几周必须学完”,因人而异。如果你每天能花两小时,前三个阶段大概一个半月可以吃透;如果时间充裕每天五小时,三周能冲到实战阶段。重要的不是速度,而是每一个阶段都有可见的产出物,这一点下面会详细说。
3. 核心语法逐个击破的实操要点
3.1 组件通信方式的“继承”与“突变”
组件通信一直是新手最容易懵的地方。Vue2 时代,大家习惯用$emit和$on;Vue3 里$on被移除了,父子通信的方式集中在props和emit上,跨层级则推荐provide / inject,全局共享状态交给状态管理库。
学习通信方式时,最重要的是先建立“数据流单向”的心智模型。子组件只能通知父组件“我这边发生了什么”,而不应该直接修改父组件传进来的数据。
<!-- 子组件 --> <script setup> const props = defineProps({ modelValue: String }) const emit = defineEmits(['update:modelValue']) function onInput(value) { emit('update:modelValue', value) } </script>这个模式就是v-model的底层实现。你记住“props 是上游的数据快照,emit 是下游的事件上报”这句话,后面学v-model自定义组件、表单组件的封装都顺理成章。
provide / inject适合祖孙组件共享数据。注意它有一个非响应式特点,默认情况下provide传出去的值不会自动保持响应式,如果你希望下游感知变化,需要显式地传一个ref或者reactive对象进去。新手经常在这里踩坑,以为provide传个对象过去,子组件读到的数据能自动更新,结果页面一动不动。
// 父组件 const userInfo = reactive({ name: '张三', age: 30 }) provide('userInfo', userInfo) // 后代组件 const userInfo = inject('userInfo') // 这样传入的是一个响应式对象,才能保证数据变化时视图同步更新通信方式的选择原则很简单:父子之间用 props/emit,祖孙之间用 provide/inject,跨模块共享用 Pinia,别一上来就全局状态管理——状态管理是有代价的,免得将来维护心智负担太重。
3.2 computed 和 watch:“何时用谁”的实用判断
computed和watch是 Vue3 中容易被混淆的一对搭档。粗暴的理解是:computed用于“根据已有数据推导出新数据”,它必须有返回值,而且依赖的响应式数据变化时会自动重新计算;watch则更侧重于“数据变化后执行副作用操作”,比如请求接口、写入缓存、操作 DOM。
从一个真实例子来看。假设你要做一个搜索框,输入关键字后实时过滤列表:
// computed 正确姿势 const filteredList = computed(() => { return list.value.filter(item => item.name.includes(keyword.value)) }) // watch 错误示例 const filteredList = ref([]) watch(keyword, (val) => { filteredList.value = list.value.filter(item => item.name.includes(val)) })第二种写法不是不行,但它把“计算逻辑”拆到了副作用里,导致filteredList初始值还得手动给一次,而且你多写了一个中间变量。所以能用computed解决的问题,不要在watch里重复算。
watch真正的舞台是异步场景。比如关键字变化后,你要去请求服务端数据:
watch(keyword, async (newKeyword) => { loading.value = true try { const res = await fetchSearchResult(newKeyword) list.value = res } catch (e) { // 处理错误 } finally { loading.value = false } }, { immediate: true })这里还有一个细节:watch监听的keyword是ref的话,在回调里拿到的新值已经是解包后的值,不需要再写.value。但如果监听的是reactive的某个属性,需要像写路径一样传一个 getter 函数(即() => obj.keyword)。
3.3 生命周期:从“新旧对照”到“场景感知”
Vue3 的生命周期比 Vue2 多了一个onBeforeUnmount和onUnmounted之分的onUnmounted,但其实真正需要你花心思的,是理解“为什么要有onMounted里发请求、onUnmounted里清理定时器”这种约定。
生命周期不是一个需要背时间点顺序的清单,而是一组“你在什么时候可以做什么操作”的约束。举例来说:
setup执行阶段:组件实例刚创建,此时能访问 props、能声明响应式数据,但模板尚未渲染,不能操作 DOMonMounted阶段:模板渲染完毕,可以获取 DOM 节点,适合初始化第三方库、绑定事件监听onUnmounted阶段:组件即将销毁,适合清理定时器、取消订阅、移除全局事件监听
onMounted(() => { window.addEventListener('resize', handleResize) }) onUnmounted(() => { window.removeEventListener('resize', handleResize) })很多新手忽略清理操作,导致组件销毁后定时器还在跑、事件监听还挂在window上,从而出现内存泄漏或重复触发。在开发聊天、实时看板这类频繁创建销毁组件的场景时,这几乎是一个必现的问题。
4. 生态和工程化,决定你能不能做“真项目”
4.1 Vue Router 和 Pinia 的两条快车道
Vue Router 本身不复杂,难在你要理解它是如何在单页应用里模拟“页面切换”的。核心就三件事:路由配置、导航守卫、动态路由。入门时先把前三者跑通,做项目时再补充“路由懒加载”来优化首屏体积。
const routes = [ { path: '/', component: () => import('@/views/HomeView.vue') }, { path: '/login', component: () => import('@/views/LoginView.vue') } ]component: () => import(...)这种写法就是路由懒加载,按需加载页面代码,避免首屏把所有页面的 JS 全部下载下来。我看一个项目“专不专业”,往往先看路由表是不是全都懒加载,这是成本最低的性能优化项。
导航守卫是用的最多的一个功能点,典型场景是登录鉴权:
router.beforeEach((to, from) => { const token = localStorage.getItem('token') if (!token && to.path !== '/login') { return { path: '/login' } } })只要两三行代码,就实现了“未登录用户只能进入登录页”的效果。关键在于理解守卫里的返回值:return 一个地址,就可以重定向到那个路由;如果 return false,会取消当前的导航。
Pinia 相比 Vuex 的改进在于——它砍掉了mutations这一层概念。你不需要再为“同步修改数据”和“异步提交”分别写不同的函数,状态管理变成了纯粹的“定义状态 + 操作状态”:
// stores/user.ts import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ token: '', userInfo: {} }), actions: { async login(username, password) { const { token, userInfo } = await api.login(username, password) this.token = token this.userInfo = userInfo return token } } })它的风格更贴近普通 JavaScript 的思维,也是我推荐新手直接学 Pinia 的原因——学 Vuex 的额外概念成本不低,而大多数项目最终只需要 Pinia 的这些简单能力。
4.2 提升实战效率的工程化配置要点
在配置上,新手最容易遇到的三个问题:跨域代理、路径别名、环境变量。
用 Vite 搭项目后,开发环境请求后端接口一般会遇到跨域问题。解决方式是配置server.proxy:
// vite.config.ts export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } })这样前端请求/api/user时,Vite 会把请求转发到http://localhost:8080/user,浏览器端看到的是同源请求,绕开了跨域限制。注意rewrite是否要剥离/api前缀,取决于后端的接口路径定义,这需要跟前端协商一致。
路径别名最常用的配置是让@指向src目录:
import path from 'path' resolve: { alias: { '@': path.resolve(__dirname, './src') } }配置之后,你在组件里写import userApi from '@/api/user',再也不用写一长串../../../的相对路径。这能显著降低后期移动文件时的改造成本,属于早期越早配置越省力的东西。
还有一个容易忽略的环节是代码规范,具体到实操就是 ESLint 加 Prettier。如果你不是一个人写代码,建议项目初始化时就把 lint 规则定下来,避免不同成员提交的代码风格互相污染。等代码量大了再想靠人工对齐风格,代价极为高昂。
5. 实战项目的推进策略与避坑清单
5.1 从“复制”到“创造”的项目进阶路径
学完语法和生态之后,一定要进入实战阶段,不然知识是散的。但实战也不能瞎打,我建议按照“三步走”来:
第一步是照猫画虎。找一个完整的开源项目(后台管理模板是比较理想的选择),把项目拉下来,读它的目录结构、路由配置、状态管理写法,尝试改几个页面。这个阶段你要回答的问题不是“怎么实现这个功能”,而是“为什么要这样组织代码”。
第二步是“搭积木改造”。拿到一个空白的脚手架,自己实现一个基础的后台管理布局:侧边栏菜单、头部导航、标签页缓存、路由守卫。每个功能都从零写一遍,遇到问题去查文档,查不到再翻开源项目,这样印象才深。
第三步才是从零做“某跨平台系统”这类完整项目。我建议选一个你日常会用的工具作为灵感,比如个人记账、待办管理、一个小型知识库或一款内容管理后台,用两个月时间把它做完整,包括登录鉴权、列表筛选、权限控制、暗黑模式这样的细节。
有一个重要的判断标准:项目做完之后,它必须“能部署给别人用”。如果你只在本机npm run dev看一眼就算完,那这个实践基本没闭环——你从来没经历过打包、部署、解决线上路径问题的阶段,工程化能力就始终缺一块。
5.2 高频问题排查速查表
以下是我在指导新人和自己排查问题时,遇到频率极高的几个问题和对应解法:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
ref变量在模板里显示不出来 | 模板中忘记使用.value,或写成了{{ refObj }} | 模板里ref会自动解包,直接用变量名即可 |
reactive对象解构后界面不更新 | reactive解构出的字段变成普通值 | 用toRefs进行解构 |
props修改后子组件不刷新 | 父组件传给子组件的是普通对象,没有响应式 | 确认传的是ref/reactive包裹后的响应式数据 |
| 动态添加的属性不触发视图更新 | 使用了普通对象而不是reactive | 改用reactive,或将字段在初始 state 中声明 |
watch监听不到深层对象变化 | 监听的对象没有开启deep | 向watch的第三个参数传{ deep: true } |
| 组件销毁后定时器仍然执行 | 忘记在onUnmounted里清理定时器 | 监听onUnmounted并清除定时器 |
| 路由切换后页面滚动位置不变 | 未配置滚动行为 | 在router.scrollBehavior里返回{ top: 0 } |
值得补充一个容易被忽略的点:watch的deep: true虽然方便,但在数据量非常庞大的对象上会有明显性能开销。更好的做法是尽可能监听具体路径,比如写() => obj.someDeepField,而不是直接对整个对象开启深度监听。
5.3 调试工具和思维方式的进阶建议
调试 Vue3 项目最强工具,依然是官方提供的 DevTools。但多数新人只会看一个Components面板,实际上它还有两个高频用法:
第一,在组件的data或setup面板中直接修改ref/reactive的值,验证界面变化逻辑,可以省去频繁刷新页面的时间。第二,追踪“状态变更的来源”——当你在页面上点击操作导致数据变化时,DevTools 会把这次变化记录在时间线里,你能清楚地看到是哪个组件触发了修改。这比在代码里打一堆console.log高效得多。
除此之外,推荐学会使用 Vue Router 的开发工具——安装扩展后,路由跳转会在浏览器侧边栏显示当前路由信息、参数和守卫执行顺序。排查“为什么没跳转成功”“为什么守卫死循环”这类问题时非常直观。
6. 关于学习方法和避坑心态的几句心得
最后再补一条经验吧。学习 Vue3 时最影响进度的,其实不是技术难点,而是“明明代码看着没问题,但结果完全不对”时产生的挫败感。这种时刻我的习惯是:不要盯着代码反复看,先打开 DevTools 的报错面板,把错误信息完整读一遍。大多数运行时错误,报错信息里都直接写明了所在组件、文件行号和数据字段,定位之后问题通常就解决了一半。
还有一点想反复强调:教程和文档永远不是按“你的项目场景”来写的。所以看完一个知识点,不要急着学下一个,先在你的项目里找一个能“用上它的地方”,把代码拆开写一遍。你真正能掌握的 Vue3 知识,不是浏览过的章节,而是亲手敲完并跑通的那部分代码。
如果你能按照上面这条路线,一步一个产出地稳扎稳打,两到三个月之后,基本可以具备独立上手中小型业务系统的能力。那份“我能真的做出来”的信心,才是整个学习过程中最值钱的东西。