Vue 进阶 & 源码解读
Vue 进阶原理与源码实现解析
本笔记按课程大纲展开,覆盖 Vue 进阶(模板化 / 组件化) 与 Vue 源码(演变 / 优化 / 响应式原理) 两大部分
Part 1. Vue 进阶
Vue 的两大核心特征:模板化(Template) 与 组件化(Component)。进阶能力本质上就是对这两者的"二次加工"。
一、特征一 — 模板化
模板化的进阶 = 对模板结构和模板数据分别做二次加工,再加上脱离模板字符串的 JSX 写法。
1.1 模板结构的二次加工 — 插槽(slot)
插槽是组件对外开放的"内容占位符",让父组件决定子组件的部分渲染结构,是组件复用的核心手段。
默认插槽(Default Slot)
子组件预留一个匿名 <slot>,父组件标签内部的内容会被填充进去。
<!-- 子组件 Card.vue -->
<template>
<div class="card">
<slot>默认内容(父组件未传入时显示)</slot> <!-- 兜底内容 -->
</div>
</template>
<!-- 父组件使用 -->
<Card>
<p>这段会替换掉子组件 slot 的兜底内容</p>
</Card>
💡 提示:
<slot>标签内的内容是"后备内容(fallback)",只有父组件不传时才显示。
具名插槽(Named Slot)
一个组件需要多个插槽时,用 name 区分;父组件用 v-slot:名字(简写 #名字)指定。
<!-- 子组件 Layout.vue -->
<template>
<header><slot name="header"></slot></header>
<main><slot></slot></main> <!-- 匿名插槽,name 默认为 default -->
<footer><slot name="footer"></slot></footer>
</template>
<!-- 父组件使用 -->
<Layout>
<template #header><h1>标题</h1></template> <!-- #header 是 v-slot:header 的简写 -->
<p>主体内容会进入默认插槽</p>
<template #footer><small>版权信息</small></template>
</Layout>
作用域插槽(Scoped Slot)
作用域插槽解决的核心问题:子组件想把自己内部的数据"暴露"给父组件,由父组件决定如何渲染。数据从子流向父,渲染结构却由父决定。
<!-- 子组件 List.vue:把每一项 item 通过 slot 传出去 -->
<template>
<ul>
<li v-for="(item, i) in list" :key="i">
<slot :row="item" :index="i"></slot> <!-- 绑定数据,向上"暴露" -->
</li>
</ul>
</template>
<!-- 父组件:通过 slotProps 接收子组件暴露的数据 -->
<List :list="users">
<template #default="{ row, index }"> <!-- 解构子组件传出的 row / index -->
<span>{{ index }} - {{ row.name }}</span>
</template>
</List>
⚠️ 注意: 作用域插槽是"渲染逻辑反转"——子组件提供数据与循环,父组件提供每项的展示模板。
三种插槽对比:
| 插槽类型 | 解决的问题 | 数据流向 | 父组件语法 |
|---|---|---|---|
| 默认插槽 | 父决定子的内容 | 父 → 子 | 直接写在组件标签内 |
| 具名插槽 | 多个内容区域分别填充 | 父 → 子 | <template #name> |
| 作用域插槽 | 父用子的数据决定渲染 | 子 → 父(数据) | <template #name="props"> |
1.2 模板数据的二次加工
模板里直接写复杂表达式不可维护,需要把数据加工"抽离"出去。分两类:监听类(响应数据变化)与 通道类(对数据做转换)。
监听类 — computed + watch
- computed(计算属性):基于依赖派生出新值,有缓存,依赖不变不重新计算。
- watch(侦听器):监听某个数据变化后执行副作用(异步、请求、操作 DOM 等)。
export default {
data() {
return { firstName: '张', lastName: '三', keyword: '' }
},
computed: {
// 缓存:firstName / lastName 不变时,多次访问 fullName 不会重新计算
fullName() {
return this.firstName + this.lastName
}
},
watch: {
// 监听 keyword 变化,执行异步副作用(如防抖搜索)
keyword(newVal, oldVal) {
this.debouncedSearch(newVal)
}
}
}
computed vs watch 对比(高频面试题):
| 维度 | computed | watch |
|---|---|---|
| 用途 | 派生新值(一个值由多个值算出) | 监听变化后做副作用 |
| 缓存 | 有缓存,依赖不变不重算 | 无缓存,每次变化都触发 |
| 返回值 | 必须有 return |
无返回值,执行逻辑 |
| 异步 | 不支持异步 | 支持异步 |
| 多对一/一对多 | 多个依赖 → 一个结果 | 一个数据 → 多个操作 |
| 适用场景 | 模板里需要的衍生数据 | 数据变化触发请求、动画、存储 |
💡 提示: "能用 computed 就别用 watch"——computed 更声明式、有缓存、更易测试。
通道类 — 函数、组装器、过滤器
把数据"过一道加工通道"再渲染:
- 函数(methods):每次渲染都执行,无缓存,可传参,常用于事件回调与一次性转换。
- 组装器(getter/computed 式组装):把多份数据组装为视图需要的结构,带缓存。
- 过滤器(filter):Vue2 中专门做文本格式化(
{{ price | toYuan }})。
// Vue2 过滤器:格式化金额
filters: {
toYuan(value) {
// 把分转换为元并保留两位小数
return '¥' + (value / 100).toFixed(2)
}
}
// 模板:{{ amount | toYuan }}
🚨 警告: Vue3 已彻底移除 filter(过滤器)。请改用 computed 或 methods 替代,例如
{{ toYuan(amount) }}。
1.3 脚本化模板书写 — JSX
模板(template)本质会被编译成 render 函数。JSX 让你直接写 render,用 JS 的完整能力组织视图,灵活度远高于模板语法。
// 用 render 函数 / JSX 替代 template
export default {
data() {
return { list: ['a', 'b', 'c'] }
},
render() {
// JS 控制流(map / 三元 / 变量)比模板指令更自由
return (
<ul>
{this.list.map((item, i) => (
<li key={i}>{item.toUpperCase()}</li>
))}
</ul>
)
}
}
模板 vs JSX:
| 维度 | template | JSX |
|---|---|---|
| 本质 | 编译成 render 函数 | 直接就是 render |
| 灵活度 | 受指令语法限制 | 完整 JS 能力(循环/条件/递归) |
| 编译优化 | 可做静态分析/PatchFlag | 优化空间小(动态性强) |
| 上手 | 直观、易读 | 需熟悉 JS,结构灵活但难审查 |
⚠️ 注意: template 因为结构固定,编译器能做更多优化(见 Part2 第 6 节);JSX 灵活但牺牲了编译优化空间。
二、特征二 — 组件化
组件化的进阶 = 各种复用与扩展手段:传统传参、逻辑混入、继承、构造扩展、插件、高级引用。
2.1 传统组件化方式
最基础的方式:components 注册 + props 传入 + emit 传出,父子单向数据流。
// 父传子:props;子传父:$emit
export default {
components: { ChildComp }, // 局部注册子组件
data() { return { msg: 'hi' } },
methods: {
onChildEvent(payload) { // 接收子组件抛出的事件
console.log(payload)
}
}
}
// 模板:<ChildComp :msg="msg" @notify="onChildEvent" />
2.2 混入 MIXIN 方式
mixin 把一段可复用的组件配置(data / methods / 生命周期等)"混入"到多个组件。
// 定义一个混入:可复用的逻辑
const logMixin = {
data() { return { count: 0 } },
created() {
console.log('mixin created 先于组件 created 执行')
},
methods: {
log() { console.log(this.count) }
}
}
export default {
mixins: [logMixin], // 混入复用
created() {
console.log('组件自身 created')
}
}
应用场景
- 多个组件共享同一段逻辑(如埋点、表单校验、轮询、滚动加载)。
- 组件库内部的通用行为抽取。
优缺点
| 优点 | 缺点 |
|---|---|
| 逻辑复用,减少重复代码 | 来源不清:属性/方法不知来自哪个 mixin |
| 多个 mixin 可叠加 | 命名冲突风险高 |
| 写法简单 | 隐式依赖,组件与 mixin 强耦合却不可见 |
🚨 警告: mixin 的"隐式来源"问题正是 Vue3 推出 Composition API(组合式函数)替代它的根本原因。
合并策略(重点)
mixin 与组件同名配置如何合并,是面试常考点:
| 选项类型 | 合并策略 |
|---|---|
| data | 递归合并,同名以组件自身优先 |
| 生命周期钩子 | 合并为数组都执行,mixin 的先执行,组件的后执行 |
| methods / components / directives | 合并为对象,同名组件覆盖 mixin |
2.3 继承拓展 EXTENDS 方式
extends 与 mixin 类似,但只继承一个组件配置(更像"基类"),优先级低于组件自身、低于 mixins。
const BaseComp = {
data() { return { type: 'base' } },
created() { console.log('base created') }
}
export default {
extends: BaseComp, // 继承单个基础组件
data() { return { type: 'child' } } // 同名 data,组件自身优先
}
合并策略
合并规则与 mixin 完全一致(data 递归合并、钩子拼数组、方法覆盖)。优先级顺序(从低到高):
extends < mixins < 组件自身
2.4 整体拓展类 EXTEND 方式
Vue.extend(options)(Vue2)返回一个组件构造器(子类),可用 new 实例化并手动挂载,常用于命令式 / 编程式创建组件(如全局弹窗、Toast)。
// Vue2:创建一个"组件构造器"
const ToastConstructor = Vue.extend({
template: '<div class="toast">{{ text }}</div>',
data() { return { text: '' } }
})
// 编程式实例化并挂载到 body(命令式调用弹窗的核心)
const instance = new ToastConstructor()
instance.text = '操作成功'
instance.$mount() // 手动挂载,生成真实 DOM
document.body.appendChild(instance.$el) // 插入页面
💡 提示: Vue3 中改用
createApp(Component).mount(container)来达到同样的编程式挂载效果。
2.5 插件使用方式 USE 方式
Vue.use(plugin)(Vue2)/ app.use(plugin)(Vue3)注册插件,插件本质是带 install 方法的对象或函数。
应用场景
- 全局注册组件、指令、原型方法(
$http、$toast)。 - 接入第三方库(Vue Router、Vuex/Pinia、ElementPlus)。
手写插件
// 插件 = 一个带 install 方法的对象(或一个函数)
const MyPlugin = {
// install 接收 app(或 Vue) 与可选 options
install(app, options) {
// 1. 注册全局组件
app.component('MyButton', MyButton)
// 2. 注册全局指令
app.directive('focus', { mounted(el) { el.focus() } })
// 3. 挂载全局方法(Vue3 用 globalProperties)
app.config.globalProperties.$toast = (msg) => console.log(msg)
}
}
// 使用
app.use(MyPlugin, { /* options */ })
⚠️ 注意:
use内部会对同一插件去重——同一个插件多次 use 只会 install 一次。
2.6 组件的高级引用方式
递归组件
组件在自身模板里引用自己,常用于树形结构(菜单、评论、文件目录)。必须有 name 且有终止条件。
<!-- TreeNode.vue -->
<template>
<li>
{{ node.label }}
<ul v-if="node.children"> <!-- 终止条件:无 children 不再递归 -->
<TreeNode
v-for="child in node.children"
:key="child.id"
:node="child"
/>
</ul>
</li>
</template>
<script>
export default { name: 'TreeNode', props: ['node'] } // name 用于自引用
</script>
🚨 警告: 递归组件没有终止条件会导致无限递归、栈溢出。
动态组件
<component :is="..."> 根据变量动态切换渲染哪个组件,常配合 <keep-alive> 缓存。
<!-- is 的值变化即切换组件,keep-alive 缓存被切走的组件状态 -->
<keep-alive>
<component :is="currentTab"></component>
</keep-alive>
<!-- currentTab 可以是组件名字符串,或组件对象 -->
异步组件
按需加载,用到时才请求对应代码块,是路由懒加载/性能优化的基础。
// Vue3:defineAsyncComponent + 动态 import
import { defineAsyncComponent } from 'vue'
const AsyncModal = defineAsyncComponent(() =>
import('./Modal.vue') // 打包时被拆为独立 chunk,运行时按需加载
)
// Vue2 简易写法:
// components: { AsyncModal: () => import('./Modal.vue') }
三种高级引用对比:
| 方式 | 核心语法 | 典型场景 |
|---|---|---|
| 递归组件 | 模板内引用自身 + name |
树形菜单、评论嵌套 |
| 动态组件 | <component :is> |
Tab 切换、表单渲染器 |
| 异步组件 | defineAsyncComponent(() => import()) |
路由懒加载、大组件按需加载 |
Part 2. Vue 源码
三、演变
3.1 从 Vue2.x 到 Vue3.x:提升、整体思路与前向兼容
Vue3 的整体思路:用 TS 重写 + monorepo 拆包 + Proxy 响应式 + 编译期优化 + 组合式 API,在更小体积下获得更好性能与可维护性。
核心提升一览:
| 维度 | Vue2 | Vue3 |
|---|---|---|
| 响应式 | Object.defineProperty |
Proxy |
| 代码组织 | Options API | Composition API(组合式) |
| 语言 | Flow(部分) | TypeScript 全量重写 |
| 架构 | 单体 | monorepo 分包 |
| 体积 | 较大,无法 tree-shaking | 支持 tree-shaking,按需更小 |
| 新能力 | — | Fragment / Teleport / Suspense |
| 根节点 | 必须单根 | 支持多根(Fragment) |
前向兼容: Vue3 兼容了大部分 Vue2 语法(Options API 仍可用),并提供 Migration Build(迁移构建) 帮助旧项目渐进升级。
四、结构上的优化 — monorepo + TS
4.1 原子结构的赋能:拆分与统一
Vue3 用 monorepo(pnpm/yarn workspaces)把框架拆为多个原子化、职责单一的包,既可统一开发,又能独立发布与独立使用。
| 包 | 职责 |
|---|---|
@vue/reactivity |
响应式系统(可脱离 Vue 独立使用) |
@vue/runtime-core |
与平台无关的运行时核心(vnode / 组件 / 调度) |
@vue/runtime-dom |
浏览器 DOM 平台的运行时(操作真实 DOM) |
@vue/compiler-core |
与平台无关的编译核心(template → AST → render) |
@vue/compiler-dom |
DOM 平台的编译器 |
@vue/shared |
各包共享的工具函数 |
💡 提示:
@vue/reactivity的原子化让响应式可以单独用于非 Vue 场景,这正是"拆分与统一"的赋能价值。
五、性能上优化
5.1 移除使用率较低的 API
精简 API 既减小体积又利于 tree-shaking:移除/调整了 filter(过滤器)、$on/$off/$once(事件总线)、inline-template、按键码修饰符(@keyup.13)等。
5.2 tree-shaking ⇒ 打包产物优化
Vue3 全面 ESM 化,全局 API 改为具名导入,没用到的就不会被打进包。
// Vue2:所有能力都挂在 Vue 上,整体引入,无法摇树
// Vue.nextTick(...) Vue.set(...)
// Vue3:按需具名导入,未使用的 API 在打包时被剔除
import { nextTick, h, computed } from 'vue'
5.3 数据劫持优化:Object.defineProperty ⇒ Proxy
这是 Vue3 响应式的核心升级,高频面试题。
| 维度 | Object.defineProperty(Vue2) | Proxy(Vue3) |
|---|---|---|
| 拦截粒度 | 逐个属性劫持 | 拦截整个对象 |
| 新增/删除属性 | 监听不到,需 Vue.set/delete |
原生支持 |
| 数组 | 需重写 7 个变异方法,索引赋值/length 不可监听 | 索引与 length 均可监听 |
| 初始化 | 递归深度遍历所有属性 | 惰性代理:访问到才递归 |
| 拦截能力 | 仅 get/set | 13 种 trap(has/deleteProperty 等) |
🚨 警告: Vue2 中给对象新增属性、或用索引改数组(
arr[0] = x)不会触发更新,这正是Object.defineProperty的固有缺陷,Proxy 从根本上解决。
5.4 更多底层优化
- 静态提升(hoistStatic):静态节点只创建一次,复用同一 vnode。
- 事件缓存(cacheHandlers):内联事件函数缓存,避免每次渲染新建。
- diff 优化:基于 PatchFlag 的靶向更新(见第 6 节)。
六、编译优化
6.1 compile 阶段静态模板分析 ⇒ 分析树 ⇔ PatchFlag
Vue3 编译器在 compile 阶段对模板做静态分析,给动态节点打上 PatchFlag(更新标记),运行时只 diff 真正会变化的部分,跳过静态内容。
<!-- 模板 -->
<div>
<p>静态文本,永不变</p> <!-- 被静态提升,diff 时直接跳过 -->
<p>{{ msg }}</p> <!-- 动态文本,打上 PatchFlag: TEXT -->
</div>
// 编译产物(简化示意):动态节点带 patchFlag 数字标记
_createElementVNode("p", null, _toDisplayString(msg), 1 /* TEXT */)
// 运行时按 flag 只比对 textContent,无需全量 diff 属性/children
- Block Tree(区块树):把动态节点收集进 block,使 diff 从"逐层遍历"变成"只遍历动态节点数组"。
- PatchFlag:用位运算标记节点是 TEXT / CLASS / STYLE / PROPS 哪种动态,实现靶向更新。
💡 提示: 这正是 template 比 JSX 更易优化的原因——结构固定,编译器能在编译期把"哪里会变"算清楚。
七、代码游览
7.1 Vue2 & Vue3 核心源码主流程
Vue2 主流程:
new Vue(options)
→ _init() // 初始化:合并配置、initState(响应式 data/props/computed/watch)
→ $mount() // 挂载入口
→ compile(template) // 模板编译为 render 函数(runtime+compiler 版本)
→ render() → VNode // 执行 render 生成虚拟 DOM
→ patch(oldVNode, vnode) // diff 后更新真实 DOM
Vue3 主流程:
createApp(RootComponent)
→ mount(container)
→ createVNode(根组件)
→ render(vnode, container)
→ patch(n1, n2) // 基于 block + PatchFlag 的靶向 diff
├─ 组件:setup() → 响应式 effect → 重新执行 render
└─ 元素:按 patchFlag 精准更新
| 阶段 | Vue2 | Vue3 |
|---|---|---|
| 入口 | new Vue() |
createApp() |
| 响应式初始化 | initState + defineReactive |
reactive() + effect |
| 渲染触发 | Watcher |
ReactiveEffect |
| 更新 | 全量 diff | block + PatchFlag 靶向 diff |
八、整体回顾:Vue2 与 Vue3 对比
| 主题 | Vue2 | Vue3 |
|---|---|---|
| 响应式原理 | Object.defineProperty |
Proxy + Reflect |
| 逻辑复用 | mixin / extends | Composition API(组合式函数) |
| 代码组织 | Options API | Options + Composition API |
| 类型支持 | 较弱 | TS 重写,类型完善 |
| 体积/摇树 | 不可 tree-shaking | 可 tree-shaking |
| 模板根节点 | 单根 | 多根(Fragment) |
| 新特性 | — | Teleport / Suspense / <script setup> |
| 过滤器 filter | 支持 | 移除 |
事件总线 $on |
支持 | 移除(用第三方或 mitt) |
| 性能 | 全量 diff | 编译优化 + 靶向更新,更快 |
九、手写 Vue 响应式原理
应用场景
理解 reactive / ref / effect 的底层,是面试与排查"为什么没更新"问题的关键。下面对比 Vue2 与 Vue3 两种实现思路。
Vue2 思路:Object.defineProperty
function defineReactive(obj, key, val) {
// 闭包内的依赖收集(简化)
const dep = []
Object.defineProperty(obj, key, {
get() {
// 读取时收集依赖(谁用到了我)
if (activeEffect) dep.push(activeEffect)
return val
},
set(newVal) {
if (newVal === val) return // 值未变则不触发
val = newVal
// 赋值时通知所有依赖重新执行
dep.forEach(effect => effect())
}
})
}
function observe(obj) {
// 递归遍历每个属性,逐个劫持(这正是 Vue2 的性能与新增属性痛点)
Object.keys(obj).forEach(key => defineReactive(obj, key, obj[key]))
}
Vue3 思路:Proxy + Reflect + effect
let activeEffect = null
// 注册副作用:执行时建立"数据 → effect"的依赖关系
function effect(fn) {
activeEffect = fn
fn() // 首次执行,触发 get 完成依赖收集
activeEffect = null
}
const targetMap = new WeakMap() // 存储:对象 → (key → effect 集合)
function track(target, key) {
// 依赖收集:把当前 effect 记到 target.key 名下
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
let dep = depsMap.get(key)
if (!dep) depsMap.set(key, (dep = new Set()))
dep.add(activeEffect)
}
function trigger(target, key) {
// 派发更新:取出该 key 的所有 effect 重新执行
const depsMap = targetMap.get(target)
depsMap?.get(key)?.forEach(effect => effect())
}
function reactive(target) {
return new Proxy(target, {
get(obj, key, receiver) {
track(obj, key) // 读 → 收集依赖
const res = Reflect.get(obj, key, receiver)
// 惰性递归:访问到对象类型才继续代理(解决深层遍历开销)
return typeof res === 'object' && res !== null ? reactive(res) : res
},
set(obj, key, value, receiver) {
const result = Reflect.set(obj, key, value, receiver)
trigger(obj, key) // 写 → 派发更新
return result
}
})
}
// 使用示例
const state = reactive({ count: 0 })
effect(() => {
console.log('count 变了:', state.count) // 自动建立依赖
})
state.count++ // 触发 set → trigger → 重新执行 effect,打印新值
两种实现核心差异:
| 维度 | Vue2 defineReactive |
Vue3 reactive |
|---|---|---|
| 拦截 | 逐属性 get/set | 整对象 Proxy |
| 深层对象 | 初始化即递归 | 访问时才递归(惰性) |
| 新增属性 | 不响应 | 自动响应 |
| 依赖存储 | 闭包 dep | WeakMap + Map + Set |
💡 提示:
Reflect配合 Proxy 使用,能正确处理receiver(this 指向),保证继承场景下 getter/setter 行为正确。
📌 复习要点回顾: 插槽三类(默认/具名/作用域)、computed vs watch、mixin 合并策略与优先级、
extend/use编程式与插件、defineProperty → Proxy的四大改进、PatchFlag 靶向更新、以及手写响应式的track/trigger闭环——这些是 Vue 进阶与源码方向的高频考点。