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 响应式流程对应流程图拆解:
调用 observe 函数传入目标数据,判断数据类型,仅对象/数组会进入劫持流程,创建 Observer 实例;
若为对象,通过 walk 方法遍历所有属性,调用 defineReactive 为每个属性添加 get/set 钩子;若为数组,重写 7 个可改变数组的原型方法,同时通过 observeArray 递归劫持数组中的嵌套对象;
当属性被访问(触发 get 钩子)时,执行依赖收集:Dep 类收集当前活跃的 Watcher,将其加入依赖列表,同时递归收集嵌套对象的依赖;
当属性被修改(触发 set 钩子)时,执行更新触发:Dep 类调用 notify 方法,遍历所有依赖的 Watcher,执行 update 方法,最终触发视图更新或回调函数执行;
核心局限:无法劫持对象新增/删除的属性(需借助 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 处理对象的 get、set、deleteProperty 等操作,替代 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 响应式与核心代码耦合度高,复用性差。通过枚举(ReactiveFlags、TrackOpTypes 等)规范操作类型,提升代码可维护性和可读性。2. 性能优化思路:Vue 3 采用惰性代理替代 Vue 2 的一次性递归劫持,减少初始化时的性能损耗;用 WeakMap 存储 targetMap 和 reactiveMap,利用 WeakMap 自动垃圾回收的特性,避免内存泄漏;Dep 采用 Set 类型,避免重复收集依赖,减少无效更新。3. 兼容性与健壮性设计:用 Reflect 替代直接操作对象(如 Reflect.get/set 替代 target[key]/target[key] = value),避免触发自身代理陷阱,同时兼容继承关系;通过 hasChanged 函数判断值是否变化,避免无意义的更新触发;区分对象和集合类型,分别使用不同的代理处理器,提升适配性。4. 代码复用与扩展性:抽取 createReactiveObject、track、trigger 等核心函数,实现逻辑复用;通过 isReadonly 参数区分可变/只读响应式对象,扩展响应式能力;Effect 类统一管理副作用,替代 Vue 2 中 Watcher、Computed 等分散的实现,提升扩展性。5. 核心差异总结:Vue 2 基于 Object.defineProperty,Vue 3 基于 Proxy;Vue 2 无法劫持新增/删除属性和数组索引,Vue 3 天然支持;Vue 2 嵌套对象一次性递归,Vue 3 惰性代理;Vue 2 依赖 Dep + Watcher,Vue 3 依赖 targetMap + Effect。diff 算法部分一、流程图
flowchart TD
A[组件更新/初始化] --> B[生成新 VNode 树]
B --> C[获取新旧 VNode,执行 patch 函数]
C --> D{是否有 patchFlag(Vue 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 虚拟 DOM(VNode)的核心,用于对比新旧 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 流程对应流程图拆解:
组件更新时,生成新 VNode 树,调用 patch 函数对比新旧 VNode;
先判断新旧 VNode 是否相同、是否为文本节点、标签名是否一致,标签名不同则直接替换整个 DOM 节点;
标签名一致时,全量对比元素属性(无精准过滤,即使只有一个属性变化,也会对比所有属性);
调用 patchChildren 对比子节点,采用双端指针法:初始化四个指针(新旧子节点的起始和结束),循环对比四种匹配情况(旧起始-新起始、旧结束-新结束、旧起始-新结束、旧结束-新起始),匹配则复用节点并递归对比子节点;
四种情况均不匹配时,创建旧子节点 key 映射表,全量遍历查找可复用节点,无匹配则创建新 DOM,有匹配则复用并移动 DOM;
循环结束后,处理剩余节点:新子节点有剩余则创建插入,旧子节点有剩余则删除对应的 DOM;
核心局限:无精准对比标记,即使节点只有部分动态内容变化,也会全量对比属性和子节点,存在无效对比,性能损耗较大;无法区分静态节点和动态节点,静态节点也会参与对比。
(二)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 流程对应流程图拆解(续):
组件更新时,生成新 VNode 树,调用 patch 函数对比新旧 VNode,优先判断节点类型是否一致,不一致则直接销毁旧节点并创建新节点;
通过 shapeFlag 判断节点类型(文本/元素/组件/片段),文本节点直接对比内容,元素/组件节点则进入 patchElement 逻辑;
patchElement 中,基于 patchFlag 进行精准属性对比:仅更新 patchFlag 标记的动态内容(如 class、style、指定 props),静态属性不参与对比,避免无效操作(核心优化点);
调用 patchChildren 对比子节点,根据 shapeFlag 判断子节点类型(文本/数组),数组子节点在开启优化后,执行 patchKeyedChildren 逻辑;
patchKeyedChildren 保留 Vue 2 双端对比(前置/后置对比),新增最长递增子序列优化:通过计算新子节点索引对应的旧子节点索引的最长递增序列,找到无需移动的节点,减少 DOM 移动次数,提升性能;
处理剩余节点:新子节点有剩余则创建插入,旧子节点有剩余则删除,同时清理未被复用的节点;
核心优化:通过 patchFlag 过滤静态节点和无需更新的属性,避免全量对比;通过 shapeFlag 快速判断节点类型,简化对比逻辑;最长递增子序列优化减少 DOM 操作,提升更新性能。
三、patchFlag 和 shapeFlag
patchFlag 和 shapeFlag 是 Vue 3 为优化 diff 算法新增的核心标记,二者协同工作,大幅提升虚拟 DOM 对比效率,解决 Vue 2 全量对比的性能损耗问题,以下结合源码和实际场景详细讲解。
(一)patchFlag:精准标记动态内容,避免无效对比
patchFlag 是 VNode 的一个属性,用于标记当前节点的「动态更新范围」,告诉 diff 算法「哪些内容可能变化,哪些是静态的」,从而实现精准对比,跳过静态内容。
核心作用:过滤静态节点和无需更新的动态内容,让 diff 算法仅关注可能变化的部分,减少无效对比操作,提升更新性能;
枚举定义:如上述源码中 PatchFlags 枚举所示,每个枚举值对应一种动态内容类型,可通过按位与(&)判断节点需要更新的内容;
生成时机: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(按位或组合标记);
- 源码应用:在 patchElement 函数中,通过判断 n2.patchFlag 的值,仅执行对应类型的更新操作(如 patchClass、patchStyle),无需全量对比所有属性,大幅提升效率。
(二)shapeFlag:标记节点类型,简化对比逻辑
shapeFlag 也是 VNode 的一个属性,用于标记当前节点的「类型」和「子节点类型」,让 diff 算法快速判断节点的结构,避免不必要的类型判断,简化对比逻辑。
核心作用:快速区分节点类型(元素/组件/文本/片段等)和子节点类型(文本/数组),让 diff 算法直接进入对应的处理逻辑,减少分支判断;
枚举定义:如上述源码中 ShapeFlags 枚举所示,每个枚举值对应一种节点类型,同样通过按位与(&)判断节点类型;
源码应用:在 patch 和 patchChildren 函数中,通过 shapeFlag 快速判断:
若 n2.shapeFlag & ShapeFlags.TEXT_CHILDREN:子节点是文本,直接对比文本内容;
若 n2.shapeFlag & ShapeFlags.ARRAY_CHILDREN:子节点是数组,进入数组子节点对比逻辑;
若 n2.shapeFlag & ShapeFlags.COMPONENT:节点是组件,进入组件更新逻辑;
- 与 Vue 2 对比:Vue 2 需通过判断 VNode 的 tag、text 等属性区分节点类型,逻辑繁琐且冗余;Vue 3 通过 shapeFlag 一次性标记节点类型,简化了 diff 算法的分支判断,提升了执行效率。
(三)二者协同工作流程
编译阶段:Vue 3 编译器为每个 VNode 生成对应的 patchFlag 和 shapeFlag,标记动态内容范围和节点类型;
diff 阶段:patch 函数先通过 shapeFlag 判断节点类型,进入对应处理逻辑;再通过 patchFlag 判断动态内容范围,执行精准对比和更新;
优势总结:二者协同,让 Vue 3 diff 算法从「全量对比」升级为「精准对比」,既减少了无效对比操作,又简化了对比逻辑,是 Vue 3 性能提升的核心原因之一。
项目工程化体系讲解
Vue 3 相较于 Vue 2,在项目工程化体系上进行了彻底重构,基于 Monorepo 架构(单一代码仓库管理多个包),实现了模块解耦、按需打包、跨平台适配等功能,核心代码集中在 vuejs/core 仓库,同时拆分出多个独立包,形成完整的工程化生态。以下结合 Vue 2 与 Vue 3 工程化体系对比,详细讲解 Vue 3 工程化的核心设计和实现。
一、Vue 2 与 Vue 3 工程化体系对比
| 对比维度 | Vue 2 | Vue 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 目录下,每个包负责一个独立功能,以下是核心包的详细解析(结合源码结构和功能):
(一)核心基础包
- @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);
}
};- @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 实现响应式。
(二)编译相关包
- @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;
}- @vue/compiler-dom:浏览器端编译包
功能:基于 @vue/compiler-core,添加浏览器端特有的编译逻辑,如处理 HTML 标签、属性、事件等,生成适配浏览器的渲染函数;
源码位置:packages/compiler-dom;
特点:与 @vue/compiler-core 解耦,可单独用于浏览器端模板编译。
- @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 等工具的底层支撑。
(三)运行时相关包
- @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)。
- @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 中,以下是简化的构建流程解析:
构建入口:每个包的 package.json 中指定入口文件(如 @vue/reactivity 的入口是 src/index.ts);
类型转换:通过 rollup-plugin-typescript2 将 TypeScript 代码转换为 JavaScript 代码,同时生成类型声明文件(.d.ts);
代码优化:通过 rollup-plugin-terser 压缩代码(生产环境),通过 rollup-plugin-node-resolve 解析第三方依赖,通过 rollup-plugin-commonjs 转换 CommonJS 模块为 ES 模块;
多格式输出:每个包构建后输出多种格式的文件,适配不同使用场景:
esm:ES 模块格式,用于现代浏览器和打包工具(如 Webpack、Vite);
cjs:CommonJS 模块格式,用于 Node.js 环境;
umd:UMD 格式,用于浏览器直接引入(通过 script 标签);
- 多平台构建:通过不同的渲染器配置,构建出适配浏览器、Node.js 等多平台的版本。
四、Vue 3 工程化体系的核心优势
模块解耦,可扩展性强:各核心包独立拆分,可单独复用、升级,降低了代码耦合度,便于维护和扩展;例如,reactivity 包可独立用于非 Vue 项目,compiler 包可单独用于模板编译;
原生支持 TypeScript,开发体验优:全程用 TypeScript 开发,类型定义完整,减少开发中的类型错误,同时提升代码可读性和可维护性;
按需打包,减小体积:基于 ES 模块,原生支持 Tree-Shaking,开发者可按需引入所需功能,避免打包冗余代码,减小项目体积;
跨平台适配能力强:runtime-core 抽象出平台无关逻辑,通过适配层可支持浏览器、Node.js、小程序等多平台,拓展了 Vue 的应用场景;
构建流程灵活,可定制化:基于 Rollup 构建,配置灵活,可根据需求定制构建流程,支持多格式、多平台输出;
生态完善,工具链成熟:配套的构建工具(Vite)、开发工具(Vue DevTools)、编译工具(vue-loader)等均已适配 Vue 3,形成完整的工程化生态。
五、学习建议
学习 Vue 3 工程化体系,建议从「核心包 → 构建流程 → 实际应用」逐步深入:
先熟悉各核心包的功能和源码结构,重点掌握 reactivity、runtime-core、compiler-core 三个核心包的实现逻辑;
了解 Monorepo 架构的设计思想,学习 pnpm workspace 的使用方法,理解多包管理的优势;
查看 rollup.config.js 配置,理解 Vue 3 的构建流程,掌握 TypeScript 与 Rollup 的结合使用;
结合实际项目,使用 Vite + Vue 3 开发,感受工程化体系的优势,加深对各包协同工作的理解。
