Vue 源码学习指南

概述

本文聚焦 Vue.js 3 核心源码(基于 vuejs/core 仓库)解析,结合 Vue 2 源码对比,涵盖响应式原理、diff 算法、项目工程化体系三大核心模块,包含详细注释源码与完整原理拆解,兼顾学习参考与查阅价值,读者可通过本文全面掌握 Vue 3 核心功能的实现逻辑与设计思路。

响应式原理部分

一、流程图

二、结合源码对流程图的讲解

响应式原理是 Vue 框架的核心,Vue 2 和 Vue 3 的实现思路本质都是「数据劫持/代理 + 依赖收集 + 触发更新」,但底层实现方式差异巨大,Vue 3 基于 Proxy 重构后,解决了 Vue 2 的诸多缺陷,同时提升了性能和扩展性。以下结合双方带详细注释的核心源码,逐环节拆解流程。

(一)Vue 2 响应式核心实现(对比参考,带详细注释)

Vue 2 响应式核心依赖 Object.defineProperty 劫持对象属性,核心逻辑集中在 src/core/observer 目录,以下是简化后的核心源码(保留关键逻辑,添加详细注释):

// Vue 2 核心:Observer 类(负责劫持数据,实现响应式)
class Observer {
  constructor(value) {
    this.value = value;
    // 为对象添加 __ob__ 属性,标记已被劫持(避免重复劫持)
    def(value, '__ob__', this);
    // 区分对象和数组,数组特殊处理(重写原型方法)
    if (Array.isArray(value)) {
      // 重写数组的 7 个可改变数组的方法(push/pop/shift/unshift/splice/sort/reverse)
      const augment = hasProto ? protoAugment : copyAugment;
      augment(value, arrayMethods, arrayKeys);
      // 数组嵌套对象,仍需递归劫持
      this.observeArray(value);
    } else {
      // 对象:遍历所有属性,逐个劫持
      this.walk(value);
    }
  }

  // 遍历对象所有属性,执行 defineReactive 劫持
  walk(obj) {
    const keys = Object.keys(obj);
    for (let i = 0; i< keys.length; i++) {
      defineReactive(obj, keys[i]);
    }
  }

  // 遍历数组,对数组中的对象进行递归劫持
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i]);
    }
  }
}

// 核心函数:为单个属性添加 get/set 劫持,实现依赖收集和触发更新
function defineReactive(obj, key, val) {
  // 递归劫持嵌套对象(val 可能是对象)
  let childOb = observe(val);
  // Dep 实例:管理当前属性的所有依赖(Watcher)
  const dep = new Dep();

  // 劫持当前属性的 get/set
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // get 钩子:依赖收集(当属性被访问时,收集当前活跃的 Watcher)
    get() {
      // 关键:Dep.target 是当前活跃的 Watcher(全局唯一,对应组件/计算属性/侦听器)
      if (Dep.target) {
        // 收集依赖:将 Watcher 加入 Dep 实例的依赖列表
        dep.depend();
        // 嵌套对象的依赖也需要收集(如 obj.a.b,访问 b 时,a 的 Dep 也需收集)
        if (childOb) {
          childOb.dep.depend();
        }
      }
      return val;
    },
    // set 钩子:触发更新(当属性被修改时,通知所有依赖的 Watcher)
    set(newVal) {
      // 值未变化(含 NaN 特殊情况),不触发更新
      if (newVal === val || (newVal !== newVal && val !== val)) {
        return;
      }
      val = newVal;
      // 新值如果是对象,需要递归劫持
      childOb = observe(newVal);
      // 通知所有依赖的 Watcher 执行更新,触发视图渲染或回调
      dep.notify();
    }
  });
}

// Dep 类:管理依赖(Watcher 集合),负责收集和通知依赖
class Dep {
  constructor() {
    this.subs = []; // 存储当前属性的所有 Watcher 实例
  }

  // 收集依赖:将当前活跃的 Watcher 加入 subs 列表
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this); // Watcher 内部会调用 dep.addSub(this),完成双向绑定
    }
  }

  // 新增 Watcher 到依赖列表
  addSub(sub) {
    this.subs.push(sub);
  }

  // 触发更新:遍历所有 Watcher,执行 update 方法
  notify() {
    const subs = this.subs.slice(); // 浅拷贝,避免遍历中修改数组导致异常
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update(); // Watcher 执行更新逻辑(视图更新或回调执行)
    }
  }
}

// 入口函数:将数据转为响应式,对外暴露的核心方法
function observe(value) {
  // 非对象/数组,不劫持(Object.defineProperty 无法代理基本类型)
  if (!isObject(value) || value instanceof VNode) {
    return;
  }
  let ob;
  // 已经被劫持过(有 __ob__ 属性),直接返回,避免重复劫持
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else {
    // 未被劫持,创建 Observer 实例,开始劫持流程
    ob = new Observer(value);
  }
  return ob;
}

Vue 2 响应式流程对应流程图拆解:

  1. 调用 observe 函数传入目标数据,判断数据类型,仅对象/数组会进入劫持流程,创建 Observer 实例;

  2. 若为对象,通过 walk 方法遍历所有属性,调用 defineReactive 为每个属性添加 get/set 钩子;若为数组,重写 7 个可改变数组的原型方法,同时通过 observeArray 递归劫持数组中的嵌套对象;

  3. 当属性被访问(触发 get 钩子)时,执行依赖收集:Dep 类收集当前活跃的 Watcher,将其加入依赖列表,同时递归收集嵌套对象的依赖;

  4. 当属性被修改(触发 set 钩子)时,执行更新触发:Dep 类调用 notify 方法,遍历所有依赖的 Watcher,执行 update 方法,最终触发视图更新或回调函数执行;

  5. 核心局限:无法劫持对象新增/删除的属性(需借助 Vue.set/Vue.delete)、无法劫持数组索引和长度变更、嵌套对象需一次性递归劫持(存在性能损耗)、无法代理基本类型数据。

(二)Vue 3 响应式核心实现(vuejs/core 仓库,带详细注释)

Vue 3 彻底重构响应式系统,核心逻辑集中在 packages/reactivity 目录,基于 ES6 Proxy 和 Reflect 实现,解决 Vue 2 所有局限,同时实现模块解耦,可独立复用。以下是简化后的核心源码(保留关键逻辑,添加详细注释):

// 全局常量:响应式标记(用于区分响应式对象、只读对象等)
export const enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  RAW = '__v_raw',
  PROXY = '__v_proxy'
}

// 全局常量:依赖收集/触发的操作类型(规范操作,提升可维护性)
export const enum TrackOpTypes {
  GET = 'get',
  HAS = 'has',
  ITERATE = 'iterate'
}

export const enum TriggerOpTypes {
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear'
}

// 全局变量:管理当前活跃的 Effect(替代 Vue 2 的 Watcher,统一管理副作用)
let activeEffect: ReactiveEffect | undefined;
// 全局依赖映射表:target → depsMap(属性 → 依赖集合),存储所有依赖关系
const targetMap = new WeakMap<Target, DepMap>();
// 响应式对象缓存:存储已代理的对象,避免重复代理
const reactiveMap = new WeakMap<Target, any>();
// 只读响应式对象缓存
const readonlyMap = new WeakMap<Target, any>();

// Vue 3 核心:reactive 函数(创建响应式对象,对外暴露的入口)
export function reactive(target: object) {
  // 若是只读响应式对象,直接返回(避免重复代理)
  if (isReadonly(target)) {
    return target;
  }
  // 调用 createReactiveObject 创建 Proxy 实例,完成代理
  return createReactiveObject(
    target,
    false, // isReadonly:是否为只读响应式
    mutableHandlers, // 可变对象的代理处理器(包含 get/set/deleteProperty 等钩子)
    mutableCollectionHandlers, // 集合对象(Map/Set/WeakMap/WeakSet)的代理处理器
    reactiveMap // 响应式对象缓存,用于复用已代理对象
  );
}

// 核心函数:创建 Proxy 实例,实现数据代理的核心逻辑
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // Proxy 只能代理对象,非对象直接返回(基本类型由 ref 处理)
  if (!isObject(target)) {
    return target;
  }
  // 若目标对象已被代理(存在 PROXY 标记),直接返回代理实例
  if (target[ReactiveFlags.PROXY]) {
    return target[ReactiveFlags.PROXY];
  }
  // 若目标对象已在缓存中,直接返回缓存的代理实例,避免重复代理
  const existingProxy = proxyMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  // 确定代理处理器:集合对象(Map/Set 等)使用 collectionHandlers,普通对象使用 baseHandlers
  const handlers = isCollection(target) ? collectionHandlers : baseHandlers;
  // 创建 Proxy 实例,绑定代理处理器,实现对目标对象的代理
  const proxy = new Proxy(target, handlers);
  // 缓存代理实例,标记目标对象已被代理
  proxyMap.set(target, proxy);
  target[ReactiveFlags.PROXY] = proxy;
  return proxy;
}

// 核心:可变对象的代理处理器(baseHandlers),包含 get/set/deleteProperty 等核心钩子
const mutableHandlers: ProxyHandlerVue 3 响应式流程对应流程图拆解:1.  调用 reactive 函数传入目标对象,内部调用 createReactiveObject 创建 Proxy 实例,同时通过 reactiveMap 缓存代理实例,避免重复代理;2.  Proxy 实例通过 mutableHandlers 处理对象的 getsetdeleteProperty 等操作,替代 Vue 2  Object.defineProperty,天然支持对象新增/删除属性、数组索引/长度变更;3.  当属性被访问(触发 get 钩子)时,调用 track 函数进行依赖收集:以「目标对象(target+ 属性(key+ 操作类型(TrackOpTypes)」为键,将当前活跃的 ReactiveEffect(替代 Vue 2  Watcher)存入 targetMap,完成依赖关联;4.  当属性被修改(set)或删除(deleteProperty)时,调用 trigger 函数触发更新:从 targetMap 中取出对应属性的所有 Effect,执行其 run 方法,触发副作用(视图更新、回调执行等);5.  核心优化:嵌套对象采用惰性代理(访问时才递归创建 Proxy),减少初始化性能损耗;用 WeakMap 存储依赖和缓存,避免内存泄漏;支持基本类型响应式(通过 ref 实现,本文聚焦核心 reactive 逻辑);模块解耦,reactivity 包可独立复用。三、关键编码技巧和学习到的内容1.  数据代理/劫持的工程化设计:Vue 3 将响应式能力拆分为独立的 reactivity 包,实现框架无关,可单独集成到其他项目;Vue 2 响应式与核心代码耦合度高,复用性差。通过枚举(ReactiveFlagsTrackOpTypes 等)规范操作类型,提升代码可维护性和可读性。2.  性能优化思路:Vue 3 采用惰性代理替代 Vue 2 的一次性递归劫持,减少初始化时的性能损耗;用 WeakMap 存储 targetMap  reactiveMap,利用 WeakMap 自动垃圾回收的特性,避免内存泄漏;Dep 采用 Set 类型,避免重复收集依赖,减少无效更新。3.  兼容性与健壮性设计:用 Reflect 替代直接操作对象(如 Reflect.get/set 替代 target[key]/target[key] = value),避免触发自身代理陷阱,同时兼容继承关系;通过 hasChanged 函数判断值是否变化,避免无意义的更新触发;区分对象和集合类型,分别使用不同的代理处理器,提升适配性。4.  代码复用与扩展性:抽取 createReactiveObjecttracktrigger 等核心函数,实现逻辑复用;通过 isReadonly 参数区分可变/只读响应式对象,扩展响应式能力;Effect 类统一管理副作用,替代 Vue 2  WatcherComputed 等分散的实现,提升扩展性。5.  核心差异总结:Vue 2 基于 Object.definePropertyVue 3 基于 ProxyVue 2 无法劫持新增/删除属性和数组索引,Vue 3 天然支持;Vue 2 嵌套对象一次性递归,Vue 3 惰性代理;Vue 2 依赖 Dep + WatcherVue 3 依赖 targetMap + Effectdiff 算法部分一、流程图
flowchart TD
    A[组件更新/初始化] --> B[生成新 VNode ]
    B --> C[获取新旧 VNode,执行 patch 函数]
    C --> D{是否有 patchFlagVue 3 新增)}
    D -->|| E[ patchFlag 精准对比(仅更新动态内容)]
    D -->|| F[全量对比(同 Vue 2 逻辑)]
    E --> G{通过 shapeFlag 判断节点类型}
    F --> G
    G -->|元素节点(ELEMENT| H[对比标签名,一致则继续对比属性/子节点]
    G -->|组件节点(COMPONENT| I[对比组件 props、插槽,更新组件内部状态]
    G -->|文本节点(TEXT| J[直接对比文本内容,不同则更新 DOM]
    G -->|片段节点(FRAGMENT| K[直接对比子节点,不处理自身 DOM]
    H --> L[对比属性:优先更新 patchFlag 标记的动态属性]
    L --> M[对比子节点:采用首尾指针对比 + 最长递增子序列优化]
    I --> M
    K --> M
    M --> N[移动/新增/删除 DOM 节点,复用已有节点]
    J --> N
    N --> O[diff 流程结束,视图更新完成]
    二、结合源码对流程图的讲解diff 算法是 Vue 虚拟 DOMVNode)的核心,用于对比新旧 VNode 树的差异,高效更新真实 DOM,避免全量重渲染,提升性能。Vue 2  Vue 3  diff 核心思路一致(基于虚拟 DOM 对比),但 Vue 3 新增 patchFlag  shapeFlag 优化,大幅提升对比效率,以下结合双方带详细注释的核心源码,逐环节拆解流程。(一)Vue 2 diff 核心实现(对比参考,带详细注释)Vue 2 diff 核心逻辑集中在 src/core/vdom/patch.js,采用「双端对比 + 全量遍历」的思路,核心函数为 patch  patchChildren,以下是简化后的核心源码(保留关键逻辑,添加详细注释):// Vue 2 核心:patch 函数(对比新旧 VNode,更新真实 DOM)
function patch(oldVnode, vnode) {
  // 1. 新旧 VNode 相同(引用一致),直接返回,无需对比
  if (oldVnode === vnode) {
    return;
  }

  // 2. 文本节点判断:若旧 VNode 是文本节点,直接替换文本并返回
  if (oldVnode.text !== undefined) {
    // 真实 DOM 元素(oldVnode.elm 是旧 VNode 对应的真实 DOM)
    const elm = oldVnode.elm;
    // 替换文本内容
    elm.textContent = vnode.text;
    return;
  }

  // 3. 元素节点判断:对比标签名,标签名不同则直接替换整个节点
  const oldElm = oldVnode.elm;
  const oldTag = oldVnode.tag;
  const newTag = vnode.tag;
  if (oldTag !== newTag) {
    // 标签名不同,直接替换旧 DOM 为新 VNode 对应的 DOM
    replaceVnode(oldVnode, vnode);
    return;
  }

  // 4. 标签名相同,对比元素属性(全量对比,无精准过滤)
  const oldData = oldVnode.data || {};
  const newData = vnode.data || {};
  // 对比并更新属性(class、style、事件、普通属性等)
  patchData(oldElm, oldData, newData);

  // 5. 对比子节点(核心 diff 逻辑)
  const oldCh = oldVnode.children || [];
  const newCh = vnode.children || [];
  patchChildren(oldElm, oldCh, newCh);
}

// 核心:子节点对比(双端对比 + 全量遍历,Vue 2 diff 核心)
function patchChildren(parentElm, oldCh, newCh) {
  // 旧子节点长度、新子节点长度
  const oldLen = oldCh.length;
  const newLen = newCh.length;

  // 双端指针初始化:oldStartIdx(旧子节点起始索引)、oldEndIdx(旧子节点结束索引)
  // newStartIdx(新子节点起始索引)、newEndIdx(新子节点结束索引)
  let oldStartIdx = 0;
  let oldEndIdx = oldLen - 1;
  let newStartIdx = 0;
  let newEndIdx = newLen - 1;

  // 双端指针对应的 VNode
  let oldStartVnode = oldCh[oldStartIdx];
  let oldEndVnode = oldCh[oldEndIdx];
  let newStartVnode = newCh[newStartIdx];
  let newEndVnode = newCh[newEndIdx];

  // 循环对比,直到某一端指针越界
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 1. 跳过已销毁的 VNode(如 v-if 销毁的节点)
    if (oldStartVnode == null) {
      oldStartVnode = oldCh[++oldStartIdx];
    } else if (oldEndVnode == null) {
      oldEndVnode = oldCh[--oldEndIdx];
    }

    // 2. 双端对比:旧起始 vs 新起始,相同则直接复用
    else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 递归对比子节点的子节点
      patch(oldStartVnode, newStartVnode);
      // 指针后移
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
    }

    // 3. 双端对比:旧结束 vs 新结束,相同则直接复用
    else if (sameVnode(oldEndVnode, newEndVnode)) {
      patch(oldEndVnode, newEndVnode);
      // 指针前移
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
    }

    // 4. 双端对比:旧起始 vs 新结束,相同则复用并移动 DOM
    else if (sameVnode(oldStartVnode, newEndVnode)) {
      patch(oldStartVnode, newEndVnode);
      // 将旧起始节点对应的 DOM 移动到旧结束节点 DOM 之后
      parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
      // 指针移动
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
    }

    // 5. 双端对比:旧结束 vs 新起始,相同则复用并移动 DOM
    else if (sameVnode(oldEndVnode, newStartVnode)) {
      patch(oldEndVnode, newStartVnode);
      // 将旧结束节点对应的 DOM 移动到旧起始节点 DOM 之前
      parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
      // 指针移动
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
    }

    // 6. 以上情况均不匹配,采用全量遍历查找可复用节点
    else {
      // 创建旧子节点 key 映射表,用于快速查找
      const keyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
      // 查找新起始节点在旧子节点中的索引
      const idxInOld = keyToIdx[newStartVnode.key];
      if (idxInOld == null) {
        // 无匹配节点,创建新 DOM 并插入
        createElm(newStartVnode, parentElm, oldStartVnode.elm);
      } else {
        // 有匹配节点,复用并移动 DOM
        const vnodeToMove = oldCh[idxInOld];
        patch(vnodeToMove, newStartVnode);
        oldCh[idxInOld] = null; // 标记为已复用,避免重复对比
        parentElm.insertBefore(vnodeToMove.elm, oldStartVnode.elm);
      }
      // 新起始指针后移
      newStartVnode = newCh[++newStartIdx];
    }
  }

  // 7. 处理剩余节点:新子节点有剩余,创建并插入
  while (newStartIdx <= newEndIdx) {
    createElm(newCh[newStartIdx], parentElm, oldCh[oldStartIdx]?.elm);
    newStartIdx++;
  }

  // 8. 处理剩余节点:旧子节点有剩余,删除对应的 DOM
  while (oldStartIdx <= oldEndIdx) {
    if (oldCh[oldStartIdx] != null) {
      removeVnode(oldCh[oldStartIdx]);
    }
    oldStartIdx++;
  }
}

// 辅助函数:判断两个 VNode 是否可复用(核心:key 相同 + 标签名相同)
function sameVnode(a, b) {
  // key 相同、标签名相同、是否为注释节点相同、是否为异步组件相同
  return (
    a.key === b.key &&
    a.tag === b.tag &&
    a.isComment === b.isComment &&
    isDef(a.data) === isDef(b.data) &&
    sameInputType(a, b)
  );
}

// 辅助函数:创建旧子节点 key 映射表,用于快速查找可复用节点
function createKeyToOldIdx(children, beginIdx, endIdx) {
  const map = {};
  for (let i = beginIdx; i <= endIdx; i++) {
    const key = children[i]?.key;
    if (isDef(key)) {
      map[key] = i;
    }
  }
  return map;
}

Vue 2 diff 流程对应流程图拆解:

  1. 组件更新时,生成新 VNode 树,调用 patch 函数对比新旧 VNode;

  2. 先判断新旧 VNode 是否相同、是否为文本节点、标签名是否一致,标签名不同则直接替换整个 DOM 节点;

  3. 标签名一致时,全量对比元素属性(无精准过滤,即使只有一个属性变化,也会对比所有属性);

  4. 调用 patchChildren 对比子节点,采用双端指针法:初始化四个指针(新旧子节点的起始和结束),循环对比四种匹配情况(旧起始-新起始、旧结束-新结束、旧起始-新结束、旧结束-新起始),匹配则复用节点并递归对比子节点;

  5. 四种情况均不匹配时,创建旧子节点 key 映射表,全量遍历查找可复用节点,无匹配则创建新 DOM,有匹配则复用并移动 DOM;

  6. 循环结束后,处理剩余节点:新子节点有剩余则创建插入,旧子节点有剩余则删除对应的 DOM;

  7. 核心局限:无精准对比标记,即使节点只有部分动态内容变化,也会全量对比属性和子节点,存在无效对比,性能损耗较大;无法区分静态节点和动态节点,静态节点也会参与对比。

(二)Vue 3 diff 核心实现(vuejs/core 仓库,带详细注释)

Vue 3 diff 核心逻辑集中在 packages/runtime-core/src/renderer.ts,保留 Vue 2 双端对比的核心思路,新增 patchFlag 和 shapeFlag 优化,核心函数为 patch 和 patchChildren,以下是简化后的核心源码(保留关键逻辑,添加详细注释):

// Vue 3 核心:patch 函数(对比新旧 VNode,更新真实 DOM,新增 patchFlag/shapeFlag 优化)
function patch(
  n1: VNode | null, // 旧 VNode(null 表示初始化)
  n2: VNode, // 新 VNode
  container: RendererElement, // 容器 DOM
  anchor: RendererElement | null = null, // 插入锚点
  parentComponent: ComponentInternalInstance | null = null, // 父组件实例
  parentSuspense: SuspenseBoundary | null = null, // 父 suspense 组件
  isSVG: boolean = false, // 是否为 SVG 元素
  slotScopeIds: string[] | null = null, // 插槽作用域 ID
  optimized: boolean = __DEV__ ? false : true // 是否开启优化(生产环境默认开启)
) {
  // 1. 新旧 VNode 相同(引用一致),直接返回
  if (n1 === n2) {
    return;
  }

  // 2. 旧 VNode 存在但与新 VNode 类型不同,销毁旧 VNode
  if (n1 && !isSameVNodeType(n1, n2)) {
    unmount(n1, parentComponent, parentSuspense, true);
    n1 = null;
  }

  // 3. 新 VNode 是文本节点,处理文本更新
  if (n2.shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 初始化:创建文本 DOM 并插入
    if (n1 == null) {
      hostInsert((n2.el = hostCreateText(n2.children as string)), container, anchor);
    } else {
      // 更新:仅当文本内容不同时,才更新文本(优化点)
      const el = (n2.el = n1.el!);
      if (n2.children !== n1.children) {
        hostSetText(el, n2.children as string);
      }
    }
  } else {
    // 4. 新 VNode 是元素/组件/片段节点,处理元素和子节点
    if (n1 == null) {
      // 初始化:创建元素 DOM 并插入
      mountElement(
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      );
    } else {
      // 更新:对比新旧 VNode,更新属性和子节点(核心 diff 逻辑)
      patchElement(
        n1,
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      );
    }
  }
}

// 核心:元素节点更新(对比属性和子节点,新增 patchFlag 优化)
function patchElement(
  n1: VNode,
  n2: VNode,
  container: RendererElement,
  anchor: RendererElement | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) {
  const el = (n2.el = n1.el!); // 复用旧 VNode 的 DOM 元素(核心复用逻辑)
  const oldProps = n1.props || {};
  const newProps = n2.props || {};

  // 1. 精准对比属性:基于 patchFlag 过滤,仅更新动态属性(Vue 3 核心优化)
  if (n2.patchFlag > 0) {
    // 按 patchFlag 标记的类型,精准更新属性(避免全量对比)
    if (n2.patchFlag & PatchFlags.CLASS) {
      // 仅更新 class 属性
      patchClass(el, oldProps.class, newProps.class, isSVG);
    }
    if (n2.patchFlag & PatchFlags.STYLE) {
      // 仅更新 style 属性
      patchStyle(el, oldProps.style, newProps.style);
    }
    if (n2.patchFlag & PatchFlags.PROPS) {
      // 仅更新指定的动态 props(n2.dynamicProps 存储动态属性名)
      patchProps(
        el,
        n2,
        oldProps,
        newProps,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds
      );
    }
    if (n2.patchFlag & PatchFlags.FORWARD_REF) {
      // 处理 forwardRef 相关逻辑
      patchRef(n1, n2, parentComponent);
    }
  } else if (!optimized) {
    // 未开启优化(开发环境),全量对比属性(同 Vue 2 逻辑)
    patchProps(
      el,
      n2,
      oldProps,
      newProps,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds
    );
  }

  // 2. 对比子节点(保留双端对比,新增优化)
  patchChildren(
    n1,
    n2,
    el,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    optimized
  );
}

// 核心:子节点对比(基于 Vue 2 双端对比,新增 patchFlag/shapeFlag 优化)
function patchChildren(
  n1: VNode,
  n2: VNode,
  container: RendererElement,
  anchor: RendererElement | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) {
  // 旧子节点、新子节点
  const c1 = n1 && n1.children;
  const c2 = n2.children;

  // 新 VNode 的 shapeFlag,用于判断子节点类型(文本/数组)
  const prevShapeFlag = n1 ? n1.shapeFlag : 0;
  const shapeFlag = n2.shapeFlag;

  // 1. 新子节点是文本节点(shapeFlag 包含 TEXT_CHILDREN)
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 旧子节点是数组节点,销毁所有旧子节点
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true);
    }
    // 文本内容不同,更新文本
    if (c2 !== c1) {
      hostSetText(container, c2 as string);
    }
  } else {
    // 2. 新子节点是数组节点(shapeFlag 包含 ARRAY_CHILDREN)
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 新旧子节点都是数组,执行核心 diff 逻辑(双端对比 + 优化)
      if (optimized) {
        // 开启优化:基于 patchFlag 过滤静态节点,仅对比动态子节点
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNode[],
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds
        );
      } else {
        // 未开启优化:全量对比(同 Vue 2 逻辑)
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true);
        mountChildren(
          c2 as VNode[],
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        );
      }
    } else {
      // 3. 旧子节点是文本节点,销毁文本节点,挂载新数组子节点
      if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        hostSetText(container, '');
      }
      // 挂载新子节点
      mountChildren(
        c2 as VNode[],
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized
      );
    }
  }
}

// 核心:开启优化后的子节点 diff(双端对比 + 最长递增子序列优化)
function patchKeyedChildren(
  c1: VNode[], // 旧子节点数组
  c2: VNode[], // 新子节点数组
  container: RendererElement,
  anchor: RendererElement | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null
) {
  let i = 0;
  const l2 = c2.length;
  let e1 = c1.length - 1; // 旧子节点结束索引
  let e2 = l2 - 1; // 新子节点结束索引

  // 1. 前置对比:从起始位置开始,匹配可复用节点
  while (i <= e1 && i <= e2) {
    const n1 = c1[i];
    const n2 = c2[i];
    // 节点可复用,递归对比
    if (isSameVNodeType(n1, n2)) {
      patch(
        n1,
        n2,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        true
      );
    } else {
      // 不匹配,终止前置对比
      break;
    }
    i++;
  }

  // 2. 后置对比:从结束位置开始,匹配可复用节点
  while (i <= e1 && i <= e2) {
    const n1 = c1[e1];
    const n2 = c2[e2];
    if (isSameVNodeType(n1, n2)) {
      patch(
        n1,
        n2,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        true
      );
    } else {
      break;
    }
    e1--;
    e2--;
  }

  // 3. 新子节点有剩余,创建并插入
  if (i > e1) {
    if (i <= e2) {
      const nextAnchor = e2 + 1 < l2 ? c2[e2 + 1].el : anchor;
      while (i <= e2) {
        mountElement(
          c2[i],
          container,
          nextAnchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          true
        );
        i++;
      }
    }
  }

  // 4. 旧子节点有剩余,删除对应的 DOM
  else if (i > e2) {
    while (i <= e1) {
      unmount(c1[i], parentComponent, parentSuspense, true);
      i++;
    }
  }

  // 5. 中间部分:采用最长递增子序列优化,减少 DOM 移动
  else {
    // 创建旧子节点 key 映射表,用于快速查找
    const keyToIdx = createKeyToOldIdx(c1, i, e1);
    let j = i;
    // 存储新子节点在旧子节点中的索引
    const toBePatched = e2 - i + 1;
    const newIndexToOldIndexMap = new Array(toBePatched).fill(-1);

    // 遍历新子节点中间部分,查找可复用节点
    while (j <= e2) {
      const n2 = c2[j];
      let idxInOld = keyToIdx.get(n2.key as string);
      if (idxInOld != null) {
        // 找到可复用节点,递归对比
        const n1 = c1[idxInOld];
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          true
        );
        newIndexToOldIndexMap[j - i] = idxInOld;
        // 标记为已复用,避免重复对比
        keyToIdx.delete(n2.key as string);
      } else {
        // 无匹配节点,创建新 DOM 并插入
        mountElement(
          n2,
          container,
          c2[j + 1]?.el || anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          true
        );
      }
      j++;
    }

    // 处理旧子节点中未被复用的节点,删除对应的 DOM
    keyToIdx.forEach((idx) => {
      unmount(c1[idx], parentComponent, parentSuspense, true);
    });

    // 最长递增子序列优化:计算新子节点索引对应的旧子节点索引的最长递增序列
    // 目的:找到无需移动的节点,减少 DOM 移动次数
    const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap);
    let k = increasingNewIndexSequence.length - 1;
    // 从后往前遍历,移动需要移动的 DOM 节点
    for (j = e2; j >= i; j--) {
      if (newIndexToOldIndexMap[j - i] === -1) {
          // 新节点,插入到对应位置
          mountElement(
            c2[j],
            container,
            c2[j + 1]?.el || anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            true
          );
        } else {
          // 可复用节点,移动到对应位置
          const cur = c2[j];
          const to = increasingNewIndexSequence[k];
          if (j === to) {
            // 无需移动,当前节点在最长递增子序列中
            k--;
          } else {
            // 移动 DOM 节点,减少移动次数
            hostInsert(
              cur.el!,
              container,
              c2[j + 1]?.el || anchor
            );
          }
        }
      }
    }
  }
}

// 辅助函数:判断两个 VNode 是否可复用(Vue 3 优化版,兼容更多场景)
function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  // 核心判断:key 相同 + 标签名相同 + 节点类型相同(元素/组件/文本等)
  return n1.type === n2.type && n1.key === n2.key;
}

// 辅助函数:创建旧子节点 key 映射表(Vue 3 用 Map 替代对象,性能更优)
function createKeyToOldIdx(
  children: VNode[],
  beginIdx: number,
  endIdx: number
): Map<string | number, number> {
  const map = new Map<string | number, number>();
  for (let i = beginIdx; i <= endIdx; i++) {
    const key = children[i].key;
    if (key !== undefined) {
      map.set(key, i);
    }
  }
  return map;
}

// 核心优化:最长递增子序列算法(用于减少 DOM 移动次数)
function getSequence(arr: number[]): number[] {
  const len = arr.length;
  const result = [0]; // 存储最长递增子序列的索引
  const p = new Array(len).fill(0); // 存储每个元素在最长序列中的前驱索引

  for (let i = 1; i < len; i++) {
    const arrI = arr[i];
    const last = result[result.length - 1];

    // 当前元素大于序列最后一个元素,直接加入序列
    if (arrI > arr[last]) {
      p[i] = last;
      result.push(i);
    } else {
      // 二分查找,找到序列中第一个大于当前元素的位置,替换为当前元素
      let left = 0;
      let right = result.length - 1;
      while (left < right) {
        const mid = (left + right) >> 1;
        if (arr[result[mid]] > arrI) {
          right = mid;
        } else {
          left = mid + 1;
        }
      }
      if (arrI < arr[result[left]]) {
        p[i] = result[left - 1] || 0;
        result[left] = i;
      }
    }
  }

  // 回溯,还原最长递增子序列的完整索引
  let i = result.length - 1;
  let last = result[i];
  while (i > 0) {
    last = p[last];
    result[i] = last;
    i--;
  }
  return result;
}

// 补充:patchFlag 和 shapeFlag 相关枚举定义(Vue 3 新增核心优化)
export const enum PatchFlags {
  // 仅更新文本内容
  TEXT = 1,
  // 仅更新 class 属性
  CLASS = 1 << 1,
  // 仅更新 style 属性
  STYLE = 1 << 2,
  // 仅更新指定的 props
  PROPS = 1 << 3,
  // 仅更新 props 中的引用类型(如对象、数组)
  FULL_PROPS = 1 << 4,
  // 有自定义事件监听的更新
  HYDRATE_EVENTS = 1 << 5,
  // 仅更新插槽(不含子节点)
  SLOTS = 1 << 6,
  // 组件自身更新(无 children 变化)
  COMPONENT = 1 << 7,
  // 组件自身更新 + 子节点更新
  COMPONENT_SHOULD_UPDATE = 1 << 8,
  // 组件需要强制更新
  COMPONENT_FORCE_UPDATE = 1 << 9,
  // 动态指令参数更新
  DYNAMIC_SLOTS = 1 << 10,
  // 仅更新 forwardRef
  FORWARD_REF = 1 << 11,
  // 不进行任何更新(静态节点)
  NOOP = 1 << 12,
  // 所有动态内容更新(全量更新)
  ALL = ~0
}

export const enum ShapeFlags {
  // 元素节点
  ELEMENT = 1,
  // 组件节点
  COMPONENT = 1 << 1,
  // 文本节点
  TEXT_CHILDREN = 1 << 2,
  // 子节点为数组
  ARRAY_CHILDREN = 1 << 3,
  // 插槽节点
  SLOTS_CHILDREN = 1 << 4,
  //  teleport 节点
  TELEPORT = 1 << 5,
  // suspense 节点
  SUSPENSE = 1 << 6,
  // 静态节点
  STATIC = 1 << 7
}

Vue 3 diff 流程对应流程图拆解(续):

  1. 组件更新时,生成新 VNode 树,调用 patch 函数对比新旧 VNode,优先判断节点类型是否一致,不一致则直接销毁旧节点并创建新节点;

  2. 通过 shapeFlag 判断节点类型(文本/元素/组件/片段),文本节点直接对比内容,元素/组件节点则进入 patchElement 逻辑;

  3. patchElement 中,基于 patchFlag 进行精准属性对比:仅更新 patchFlag 标记的动态内容(如 class、style、指定 props),静态属性不参与对比,避免无效操作(核心优化点);

  4. 调用 patchChildren 对比子节点,根据 shapeFlag 判断子节点类型(文本/数组),数组子节点在开启优化后,执行 patchKeyedChildren 逻辑;

  5. patchKeyedChildren 保留 Vue 2 双端对比(前置/后置对比),新增最长递增子序列优化:通过计算新子节点索引对应的旧子节点索引的最长递增序列,找到无需移动的节点,减少 DOM 移动次数,提升性能;

  6. 处理剩余节点:新子节点有剩余则创建插入,旧子节点有剩余则删除,同时清理未被复用的节点;

  7. 核心优化:通过 patchFlag 过滤静态节点和无需更新的属性,避免全量对比;通过 shapeFlag 快速判断节点类型,简化对比逻辑;最长递增子序列优化减少 DOM 操作,提升更新性能。

三、patchFlag 和 shapeFlag

patchFlag 和 shapeFlag 是 Vue 3 为优化 diff 算法新增的核心标记,二者协同工作,大幅提升虚拟 DOM 对比效率,解决 Vue 2 全量对比的性能损耗问题,以下结合源码和实际场景详细讲解。

(一)patchFlag:精准标记动态内容,避免无效对比

patchFlag 是 VNode 的一个属性,用于标记当前节点的「动态更新范围」,告诉 diff 算法「哪些内容可能变化,哪些是静态的」,从而实现精准对比,跳过静态内容。

  1. 核心作用:过滤静态节点和无需更新的动态内容,让 diff 算法仅关注可能变化的部分,减少无效对比操作,提升更新性能;

  2. 枚举定义:如上述源码中 PatchFlags 枚举所示,每个枚举值对应一种动态内容类型,可通过按位与(&)判断节点需要更新的内容;

  3. 生成时机:Vue 3 编译阶段(template → render 函数)会自动为 VNode 添加 patchFlag,例如:

  • 静态节点(无任何动态绑定):patchFlag = PatchFlags.NOOP(无需任何更新);

  • 仅文本绑定动态内容(如 {{ msg }}):patchFlag = PatchFlags.TEXT(仅更新文本);

  • 绑定动态 class(如 :class=“cls”):patchFlag = PatchFlags.CLASS(仅更新 class);

  • 绑定多个动态内容(如 :class=“cls” :style=“sty”):patchFlag = PatchFlags.CLASS | PatchFlags.STYLE(按位或组合标记);

  1. 源码应用:在 patchElement 函数中,通过判断 n2.patchFlag 的值,仅执行对应类型的更新操作(如 patchClass、patchStyle),无需全量对比所有属性,大幅提升效率。

(二)shapeFlag:标记节点类型,简化对比逻辑

shapeFlag 也是 VNode 的一个属性,用于标记当前节点的「类型」和「子节点类型」,让 diff 算法快速判断节点的结构,避免不必要的类型判断,简化对比逻辑。

  1. 核心作用:快速区分节点类型(元素/组件/文本/片段等)和子节点类型(文本/数组),让 diff 算法直接进入对应的处理逻辑,减少分支判断;

  2. 枚举定义:如上述源码中 ShapeFlags 枚举所示,每个枚举值对应一种节点类型,同样通过按位与(&)判断节点类型;

  3. 源码应用:在 patch 和 patchChildren 函数中,通过 shapeFlag 快速判断:

  • 若 n2.shapeFlag & ShapeFlags.TEXT_CHILDREN:子节点是文本,直接对比文本内容;

  • 若 n2.shapeFlag & ShapeFlags.ARRAY_CHILDREN:子节点是数组,进入数组子节点对比逻辑;

  • 若 n2.shapeFlag & ShapeFlags.COMPONENT:节点是组件,进入组件更新逻辑;

  1. 与 Vue 2 对比:Vue 2 需通过判断 VNode 的 tag、text 等属性区分节点类型,逻辑繁琐且冗余;Vue 3 通过 shapeFlag 一次性标记节点类型,简化了 diff 算法的分支判断,提升了执行效率。

(三)二者协同工作流程

  1. 编译阶段:Vue 3 编译器为每个 VNode 生成对应的 patchFlag 和 shapeFlag,标记动态内容范围和节点类型;

  2. diff 阶段:patch 函数先通过 shapeFlag 判断节点类型,进入对应处理逻辑;再通过 patchFlag 判断动态内容范围,执行精准对比和更新;

  3. 优势总结:二者协同,让 Vue 3 diff 算法从「全量对比」升级为「精准对比」,既减少了无效对比操作,又简化了对比逻辑,是 Vue 3 性能提升的核心原因之一。

项目工程化体系讲解

Vue 3 相较于 Vue 2,在项目工程化体系上进行了彻底重构,基于 Monorepo 架构(单一代码仓库管理多个包),实现了模块解耦、按需打包、跨平台适配等功能,核心代码集中在 vuejs/core 仓库,同时拆分出多个独立包,形成完整的工程化生态。以下结合 Vue 2 与 Vue 3 工程化体系对比,详细讲解 Vue 3 工程化的核心设计和实现。

一、Vue 2 与 Vue 3 工程化体系对比

对比维度Vue 2Vue 3
架构模式单一包架构,所有功能(响应式、编译、运行时)集成在一个 vue 包中,耦合度高Monorepo 架构,拆分多个独立包(reactivity、compiler-core、runtime-core 等),耦合度低,可独立复用
构建工具基于 Rollup 构建,配置简单,仅支持浏览器端构建基于 Rollup + TypeScript 构建,配置灵活,支持多平台(浏览器、Node.js、小程序)构建
类型支持原生不支持 TypeScript,需通过第三方声明文件(@types/vue)支持,类型覆盖不完整全程用 TypeScript 开发,类型定义完整,原生支持 TypeScript,开发体验更优
模块解耦响应式、编译、运行时等模块深度耦合,无法单独抽取复用模块彻底解耦,如 reactivity 包可独立用于非 Vue 项目,compiler 包可单独用于模板编译
按需打包支持有限,需手动配置 Tree-Shaking,部分功能无法按需引入原生支持 Tree-Shaking,基于 ES 模块,可按需引入所需功能(如仅引入 reactive、ref),减小打包体积
跨平台适配主要面向浏览器端,跨平台适配需依赖第三方框架(如 Weex),原生支持差设计之初考虑跨平台,runtime-core 抽象出平台无关逻辑,可通过适配层支持浏览器、Node.js、小程序等多平台

二、Vue 3 工程化核心包解析(基于 vuejs/core 仓库)

Vue 3 采用 Monorepo 架构,通过 pnpm workspace 管理多个包,核心包集中在 packages 目录下,每个包负责一个独立功能,以下是核心包的详细解析(结合源码结构和功能):

(一)核心基础包

  1. @vue/shared:共享工具函数包
  • 功能:提供 Vue 3 全仓库通用的工具函数,如类型判断(isObject、isArray)、字符串处理、DOM 工具等,是所有其他包的基础依赖;

  • 源码位置:packages/shared;

  • 关键源码(简化版,带注释):

// packages/shared/src/index.ts
// 类型判断工具函数
export const isObject = (val: unknown): val is object =>
  val !== null && typeof val === 'object';

export const isArray = Array.isArray;

export const isFunction = (val: unknown): val is Function =>
  typeof val === 'function';

// 字符串处理工具函数
export const camelize = (str: string): string => {
  return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''));
};

// 空函数(用于默认回调、占位等)
export const NOOP = () => {};

// 空对象(用于默认配置、避免创建新对象)
export const EMPTY_OBJ = Object.freeze({});

// 判断值是否发生变化(响应式、diff 算法均会用到)
export const hasChanged = (value: unknown, oldValue: unknown): boolean => {
  if (value === oldValue) {
    return false;
  } else if (value == null || oldValue == null) {
    return true;
  } else if (typeof value !== 'object' || typeof oldValue !== 'object') {
    return value !== oldValue;
  } else {
    // 复杂对象对比(简化版)
    return isArray(value) ? isArrayEqual(value, oldValue as any) : isObjectEqual(value, oldValue);
  }
};
  1. @vue/reactivity:响应式核心包
  • 功能:实现 Vue 3 响应式系统的核心逻辑,基于 Proxy + Reflect,提供 reactive、ref、computed、watch 等 API,可独立复用(无需依赖 Vue 其他包);

  • 源码位置:packages/reactivity;

  • 核心模块:

  • reactive.ts:提供 reactive、shallowReactive 等函数,实现对象响应式;

  • ref.ts:提供 ref、shallowRef 等函数,实现基本类型响应式;

  • computed.ts:提供 computed 函数,实现计算属性;

  • effect.ts:实现 Effect 类,管理副作用(依赖收集、触发更新);

  • 特点:模块解耦,可单独引入到非 Vue 项目中,如 React 项目中使用 reactive 实现响应式。

(二)编译相关包

  1. @vue/compiler-core:编译核心包
  • 功能:提供模板编译的核心逻辑,与平台无关,负责将模板解析为 AST(抽象语法树),并生成渲染函数(render 函数);

  • 源码位置:packages/compiler-core;

  • 核心流程:模板 → 词法分析(扫描模板字符串)→ 语法分析(生成 AST)→ 转换(优化 AST)→ 生成(生成 render 函数);

  • 关键源码(简化版,模板解析核心逻辑):

// packages/compiler-core/src/parse.ts
// 模板解析入口函数,将模板字符串转为 AST
export function parse(
  template: string,
  options: ParserOptions = {}
): RootNode {
  const context = createParserContext(template, options);
  const start = getCursor(context);
  // 解析模板,生成 AST 根节点
  const root = parseChildren(context, undefined, false);
  // 标记 AST 根节点的位置信息
  root.loc = getSelection(context, start);
  return root;
}

// 解析子节点(元素、文本、注释等)
function parseChildren(
  context: ParserContext,
  parent: ElementNode | undefined,
  isInVPre: boolean
): TemplateChildNode[] {
  const nodes: TemplateChildNode[] = [];
  while (!isEnd(context, parent)) {
    const { source } = context;
    if (startsWith(source, '<!--')) {
      // 解析注释节点
      parseComment(context, nodes);
    } else if (startsWith(source, '{{')) {
      // 解析文本插值({{ msg }})
      parseInterpolation(context, nodes);
    } else if (source[0] === '<') {
      if (source[1] === '/') {
        // 解析闭合标签
        parseClosingTag(context, parent);
        break;
      } else if (source[1] === '!') {
        // 解析 DOCTYPE 或注释(特殊情况)
        parseDocType(context, nodes);
      } else {
        // 解析元素节点
        parseElement(context, nodes, parent, isInVPre);
      }
    } else {
      // 解析纯文本节点
      parseText(context, nodes);
    }
  }
  return nodes;
}
  1. @vue/compiler-dom:浏览器端编译包
  • 功能:基于 @vue/compiler-core,添加浏览器端特有的编译逻辑,如处理 HTML 标签、属性、事件等,生成适配浏览器的渲染函数;

  • 源码位置:packages/compiler-dom;

  • 特点:与 @vue/compiler-core 解耦,可单独用于浏览器端模板编译。

  1. @vue/compiler-sfc:单文件组件(SFC)编译包
  • 功能:负责解析 .vue 单文件组件,将 template、script、style 三部分分别解析、处理,最终生成可执行的 JavaScript 代码;

  • 源码位置:packages/compiler-sfc;

  • 核心功能:

  • 解析 template 部分:调用 @vue/compiler-dom 生成渲染函数;

  • 解析 script 部分:处理 ES 模块、TypeScript 转换、组件导出等;

  • 解析 style 部分:处理 scoped 样式、CSS 预处理器(less/sass)等;

  • 应用场景:Vue 3 项目中解析 .vue 文件的核心依赖,是 vue-loader、vite-plugin-vue 等工具的底层支撑。

(三)运行时相关包

  1. @vue/runtime-core:运行时核心包
  • 功能:提供 Vue 3 运行时的核心逻辑,与平台无关,负责组件实例化、虚拟 DOM 渲染、diff 算法、生命周期管理等;

  • 源码位置:packages/runtime-core;

  • 核心模块:

  • renderer.ts:提供 createRenderer 函数,创建渲染器,实现虚拟 DOM 到真实 DOM 的转换;

  • component.ts:定义 ComponentInternalInstance 接口,管理组件实例的生命周期、props、slots 等;

  • vnode.ts:定义 VNode 接口,实现虚拟 DOM 节点的创建和管理;

  • 特点:抽象出平台无关逻辑,可通过适配层支持多平台(如浏览器、Node.js)。

  1. @vue/runtime-dom:浏览器端运行时包
  • 功能:基于 @vue/runtime-core,添加浏览器端特有的运行时逻辑,如 DOM 操作(创建、插入、删除 DOM)、事件绑定、样式操作等;

  • 源码位置:packages/runtime-dom;

  • 关键源码(简化版,DOM 操作工具):

// packages/runtime-dom/src/index.ts
// 创建渲染器(适配浏览器端)
export const createApp = ((...args) => {
  const app = createAppAPI(createRenderer(rendererOptions));
  return app(...args);
}) as CreateAppFunction<Element>;

// 浏览器端渲染器配置(DOM 操作相关)
const rendererOptions: RendererOptions<Element> = {
  // 创建元素
  createElement: (tag, isSVG, isCustomElement) => {
    const el = isSVG
      ? document.createElementNS('http://www.w3.org/2000/svg', tag)
      : document.createElement(tag, isCustomElement ? { is: tag } : undefined);
    return el;
  },
  // 插入元素
  insert: (el, parent, anchor) => {
    parent.insertBefore(el, anchor || null);
  },
  // 删除元素
  remove: (el) => {
    const parent = el.parentNode;
    if (parent) {
      parent.removeChild(el);
    }
  },
  // 设置文本内容
  setText: (el, text) => {
    el.textContent = text;
  },
  // 设置元素属性
  setElementProp: (el, key, value) => {
    el.setAttribute(key, value);
  },
  // 绑定事件
  addEventListener: (el, event, handler) => {
    el.addEventListener(event, handler);
  }
};

(四)入口包

@vue/vue:Vue 3 入口包,整合所有核心包,对外暴露统一的 API(如 createApp、reactive、ref 等),供开发者直接使用;

  • 源码位置:packages/vue;

  • 核心逻辑(简化版):

// packages/vue/src/index.ts
// 从各核心包引入 API,统一对外暴露
export { createApp } from '@vue/runtime-dom';
export {
  reactive,
  ref,
  computed,
  watch,
  watchEffect
} from '@vue/reactivity';
export { h, Fragment, Teleport, Suspense } from '@vue/runtime-core';
export { compile } from '@vue/compiler-dom';
// 共享工具函数
export * from '@vue/shared';

三、Vue 3 工程化构建流程

Vue 3 采用 Rollup 作为构建工具,结合 TypeScript 进行类型检查和转换,构建流程分为开发环境构建和生产环境构建,核心配置在 rollup.config.js 中,以下是简化的构建流程解析:

  1. 构建入口:每个包的 package.json 中指定入口文件(如 @vue/reactivity 的入口是 src/index.ts);

  2. 类型转换:通过 rollup-plugin-typescript2 将 TypeScript 代码转换为 JavaScript 代码,同时生成类型声明文件(.d.ts);

  3. 代码优化:通过 rollup-plugin-terser 压缩代码(生产环境),通过 rollup-plugin-node-resolve 解析第三方依赖,通过 rollup-plugin-commonjs 转换 CommonJS 模块为 ES 模块;

  4. 多格式输出:每个包构建后输出多种格式的文件,适配不同使用场景:

  • esm:ES 模块格式,用于现代浏览器和打包工具(如 Webpack、Vite);

  • cjs:CommonJS 模块格式,用于 Node.js 环境;

  • umd:UMD 格式,用于浏览器直接引入(通过 script 标签);

  1. 多平台构建:通过不同的渲染器配置,构建出适配浏览器、Node.js 等多平台的版本。

四、Vue 3 工程化体系的核心优势

  1. 模块解耦,可扩展性强:各核心包独立拆分,可单独复用、升级,降低了代码耦合度,便于维护和扩展;例如,reactivity 包可独立用于非 Vue 项目,compiler 包可单独用于模板编译;

  2. 原生支持 TypeScript,开发体验优:全程用 TypeScript 开发,类型定义完整,减少开发中的类型错误,同时提升代码可读性和可维护性;

  3. 按需打包,减小体积:基于 ES 模块,原生支持 Tree-Shaking,开发者可按需引入所需功能,避免打包冗余代码,减小项目体积;

  4. 跨平台适配能力强:runtime-core 抽象出平台无关逻辑,通过适配层可支持浏览器、Node.js、小程序等多平台,拓展了 Vue 的应用场景;

  5. 构建流程灵活,可定制化:基于 Rollup 构建,配置灵活,可根据需求定制构建流程,支持多格式、多平台输出;

  6. 生态完善,工具链成熟:配套的构建工具(Vite)、开发工具(Vue DevTools)、编译工具(vue-loader)等均已适配 Vue 3,形成完整的工程化生态。

五、学习建议

学习 Vue 3 工程化体系,建议从「核心包 → 构建流程 → 实际应用」逐步深入:

  1. 先熟悉各核心包的功能和源码结构,重点掌握 reactivity、runtime-core、compiler-core 三个核心包的实现逻辑;

  2. 了解 Monorepo 架构的设计思想,学习 pnpm workspace 的使用方法,理解多包管理的优势;

  3. 查看 rollup.config.js 配置,理解 Vue 3 的构建流程,掌握 TypeScript 与 Rollup 的结合使用;

  4. 结合实际项目,使用 Vite + Vue 3 开发,感受工程化体系的优势,加深对各包协同工作的理解。

0%