← Frontend

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 进阶与源码方向的高频考点。