Skip to main content

源码要点

面试 Vue 源码时,考察的核心是对 Vue 底层原理的理解,包括响应式系统、虚拟 DOM、模板编译、组件生命周期等核心机制。以下是必须掌握的关键知识点,结合 Vue 2 和 Vue 3 的核心差异进行整理:

一、响应式系统原理(Vue 2 vs Vue 3)

响应式是 Vue 的核心特性,也是面试高频考点,需掌握 Vue 2 和 Vue 3 的实现差异。

1. Vue 2 响应式实现

  • 核心 APIObject.defineProperty(劫持对象属性的 getter/setter)。
  • 实现流程
    • 通过 Observer 类将数据(data)递归转为响应式对象(遍历属性,用 Object.defineProperty 重写 getset)。
    • getter 中通过 Dep(依赖管理器)收集依赖(Watcher),setter 中触发 Dep 通知所有依赖更新。
  • 关键角色
    • Observer:将数据转为响应式(数组通过重写原型方法实现响应式,如 pushsplice)。
    • Dep:每个响应式属性对应一个 Dep,维护依赖列表(Watcher),提供 depend(收集)和 notify(通知)方法。
    • Watcher:依赖的具体载体(组件渲染、watchcomputed 都会创建 Watcher),接收更新通知后执行回调(如重新渲染组件)。
  • 局限性
    • 无法监听对象新增/删除的属性(需用 Vue.set/$set 手动处理)。
    • 无法监听数组索引修改和长度变化(需通过重写的数组方法触发更新)。

2. Vue 3 响应式实现

  • 核心 APIProxy(代理整个对象,而非单个属性)+ Reflect(反射操作)。
  • 实现流程
    • 通过 reactive 函数创建对象的 Proxy 代理,拦截 get(读取)、set(修改)、deleteProperty(删除)等操作。
    • get 时收集依赖(track 函数),set/deleteProperty 时触发依赖更新(trigger 函数)。
  • 改进点
    • 天然支持对象新增/删除属性、数组索引修改和长度变化。
    • 无需递归初始化为响应式(Proxy 懒代理,访问深层属性时才递归代理)。
  • 关键 API 差异
    • reactive:处理对象/数组,返回 Proxy 代理。
    • ref:处理基本类型,通过 .value 访问,底层用 reactive 包装成 { value: ... }

二、虚拟 DOM 与 Diff 算法

虚拟 DOM 是 Vue 实现跨平台渲染和高效更新的核心,Diff 算法是优化渲染性能的关键。

1. 虚拟 DOM 原理

  • 定义:用 JavaScript 对象(VNode)描述真实 DOM 结构(如 tagpropschildren 等属性)。
  • 作用
    • 跨平台:同一 VNode 可渲染为 DOM(浏览器)、原生组件(React Native)等。
    • 减少 DOM 操作:通过 Diff 算法计算最小更新量,批量执行 DOM 操作。
  • VNode 结构(简化):
    {
    tag: 'div', // 标签名
    props: { class: 'box' }, // 属性
    children: [/* 子 VNode */], // 子节点
    key: '1', // 用于 Diff 算法
    el: null, // 对应真实 DOM 元素
    // 其他标识:静态节点标记、组件类型等
    }

2. Diff 算法核心逻辑

  • 核心目标:对比新旧 VNode 树,找出差异并更新真实 DOM,尽可能复用现有节点。
  • Vue 2 Diff 特点
    • 同级比较:只对比同一层级的节点,不跨层级比较(降低复杂度,从 O(n³) 优化为 O(n))。
    • key 的作用:作为节点唯一标识,帮助 Diff 算法快速找到可复用节点(无 key 时可能导致节点错误复用,如列表渲染)。
    • 流程
      1. 先比较节点是否相同(tagkey 一致)。
      2. 不同则直接替换节点;相同则递归比较 propschildren
  • Vue 3 Diff 优化
    • 静态标记:编译时标记静态节点(如纯文本、无动态绑定的节点),Diff 时直接跳过。
    • 最长递增子序列:列表 Diff 时,通过该算法减少节点移动次数(Vue 2 是暴力对比,移动次数更多)。

三、模板编译过程

Vue 的模板(template)需要编译为渲染函数(render)才能执行,编译过程是连接模板和虚拟 DOM 的桥梁。

1. 编译三阶段

  • 解析(Parse):将模板字符串解析为抽象语法树(AST)。

    • 核心:通过正则匹配标签、属性、文本等,递归构建 AST 节点(如元素节点、文本节点、注释节点)。
    • 处理:解析指令(v-ifv-for)、插值({{ }})、事件(@click)等。
  • 优化(Optimize):标记 AST 中的静态节点和静态根节点,避免 Diff 时重复处理。

    • 静态节点:不包含动态绑定(如 <div>静态文本</div>),渲染后永远不变。
    • 静态根节点:包含静态子节点的非叶子节点,可整体跳过 Diff。
  • 生成(Generate):将优化后的 AST 转换为渲染函数(render 函数)。

    • 渲染函数执行后返回 VNode 树,例如:
      // 模板 <div>{{ message }}</div> 编译为:
      function render(h) {
      return h('div', this.message);
      }

2. Vue 3 编译时优化

Vue 3 强化了编译时优化,通过编译阶段的分析减少运行时 Diff 开销:

  • PatchFlag:为动态节点添加标记(如文本动态、属性动态),Diff 时只处理带标记的节点。
  • hoistStatic:将静态节点的创建逻辑提升到渲染函数外,避免每次渲染重新创建。
  • cacheHandler:缓存事件处理函数,避免每次渲染创建新函数导致的不必要更新。

四、组件化与生命周期

组件是 Vue 的核心概念,需理解组件的初始化流程和生命周期钩子的触发时机。

1. 组件初始化流程(Vue 2)

  • 整体流程new Vue() → 初始化(init)→ 编译($mount)→ 渲染(render)→ 挂载(mount)。
  • 关键步骤
    1. 初始化阶段initState(初始化 datapropscomputedwatch)、initEvents(初始化事件)。
    2. 挂载阶段$mount 调用 compileToFunctions 编译模板为 render 函数,执行 render 生成 VNode,再通过 patch 函数将 VNode 转为真实 DOM 并挂载到页面。
    3. 更新阶段:数据变化触发 setterDep.notifyWatcher.update → 重新执行 render 生成新 VNodepatch 对比新旧 VNode 并更新 DOM。

2. 生命周期钩子实现

  • 原理:生命周期钩子本质是在组件初始化的特定阶段执行的回调函数,由 Vue 内部在对应时机触发。
  • Vue 2 关键钩子触发时机
    • beforeCreate:初始化 dataprops 之前(无法访问 this.data)。
    • createddataprops 初始化完成(可访问数据,但 DOM 未挂载)。
    • beforeMountrender 函数已生成,但未挂载到 DOM。
    • mounted:真实 DOM 挂载完成(可访问 DOM 元素)。
    • beforeUpdate:数据更新后,DOM 重新渲染前。
    • updated:DOM 重新渲染完成。
    • beforeDestroy:组件销毁前(可做清理工作,如清除定时器)。
    • destroyed:组件销毁完成(所有事件监听、子组件均已移除)。
  • Vue 3 生命周期变化
    • 组合式 API 中用 onMountedonUpdated 等函数替代选项式钩子,内部通过注册回调到组件实例实现。

五、computed 与 watch 原理

computedwatch 是处理数据依赖和变化的核心 API,需理解其实现差异。

1. computed 原理

  • 核心特性:缓存(依赖不变时,多次访问返回缓存结果)、响应式(依赖变化时自动重新计算)。
  • 实现
    • computed 本质是一个特殊的 WatcherComputedWatcher),其 lazy 选项为 true(延迟计算)。
    • 首次访问 computed 属性时,触发 getter 并执行计算函数,收集依赖(依赖的响应式数据)。
    • 依赖变化时,标记 computed 为“脏”(dirty: true),下次访问时重新计算并更新缓存。

2. watch 原理

  • 核心特性:监听数据变化并执行副作用(支持异步操作),可配置 deep(深度监听)、immediate(立即执行)。
  • 实现
    • 每个 watch 对应一个 WatcherUserWatcher),初始化时会读取一次监听的属性,触发 getter 收集依赖。
    • 当监听的属性变化时,Watcher 触发回调函数(handler)。
    • 深度监听:通过 traverse 函数递归遍历对象,收集所有子属性的依赖,确保深层变化能被捕获。
    • 立即执行immediate: true 时,初始化后直接执行一次 handler

六、全局 API 与实例方法

关键 API 的实现原理能体现对源码的理解深度。

1. Vue.set / this.$set(Vue 2)

  • 作用:为响应式对象新增属性并触发更新(解决 Object.defineProperty 无法监听新增属性的问题)。
  • 原理
    1. 若目标是数组,调用重写的 splice 方法(会触发 setter)。
    2. 若目标是对象,判断属性是否已存在:存在则直接赋值;不存在则用 Object.defineProperty 为该属性添加 getter/setter,并触发 Dep.notify 通知更新。

2. nextTick 原理

  • 作用:在下次 DOM 更新循环结束后执行回调(常用于修改数据后获取更新后的 DOM)。
  • 原理
    • 基于 JavaScript 的事件循环(Event Loop),优先使用微任务(Promise.thenMutationObserver),降级使用宏任务(setImmediatesetTimeout)。
    • Vue 内部维护一个回调队列,nextTick 将回调加入队列,DOM 更新后(flushSchedulerQueue)批量执行队列中的回调。

3. v-model 原理

  • 作用:表单元素的双向绑定(语法糖)。
  • 原理
    • 对普通表单元素(如 input):等价于 :value="xxx" + @input="xxx = $event.target.value"
    • 对组件:等价于 :value="xxx" + @input="xxx = $event",子组件通过 $emit('input', newValue) 触发更新。
  • Vue 3 变化:支持自定义 modelValueupdate:modelValue 事件,更灵活(如 <Child v-model:title="title" />)。

七、Vue 3 重要新特性原理

Vue 3 基于 Composition API 重构,需掌握核心变化的实现。

1. Composition API 实现

  • setup 执行时机:在 beforeCreate 之前执行,此时组件实例未完全初始化(thisundefined)。
  • 响应式集成setup 中通过 ref/reactive 创建响应式数据,返回后暴露给模板和其他选项。
  • 生命周期钩子onMounted 等函数内部通过 getCurrentInstance 获取当前组件实例,将钩子回调注册到实例的生命周期队列中。

2. 模块化设计

Vue 3 采用 monorepo 架构,将核心功能拆分为独立模块:

  • @vue/reactivity:响应式系统(reactiverefeffect 等)。
  • @vue/compiler-core:模板编译核心逻辑。
  • @vue/runtime-core:运行时核心(虚拟 DOM、组件渲染等)。
  • 各模块解耦,可单独使用(如用 @vue/reactivity 作为独立的响应式库)。

八、常见面试题延伸

  • 为什么 data 必须是函数?:避免组件复用时代理同一个对象(引用类型共享),函数每次返回新对象确保实例独立。
  • v-if 和 v-show 的区别?v-if 是动态创建/销毁节点(编译时条件渲染),v-show 是通过 display: none 控制显示(始终渲染节点)。
  • Vue 2 中数组为什么不能通过索引修改?Object.defineProperty 无法监听数组索引的 setter,需通过重写 pushsplice 等方法触发更新。
  • Vue 3 性能提升在哪里?Proxy 响应式优化、编译时静态标记、Diff 算法优化(最长递增子序列)、按需引入等。

总结

面试 Vue 源码的核心是理解“数据驱动视图”的完整链路:从响应式数据的创建(Observer/Proxy),到模板编译为渲染函数,再到虚拟 DOM 的生成与 Diff 算法,最终通过生命周期管理组件的渲染与更新。需重点掌握 Vue 2 和 Vue 3 的实现差异,尤其是响应式系统和编译优化的演进。