Vue3源码剖析
本文对应的Vue3源码版本:3.5.13
工程架构设计
- 【编译处理】将 vue 文件编译为函数或者对象
- compiler-core
- compiler-dom
- compiler-sfc
- compiler-ssr
- 【响应式系统】将状态数据与视图更新链接起来
- reactivity
- 【运行时渲染】数据更新时需要重新渲染到页面中
- runtime-core
- runtime-dom
- runtime-test // 用于测试
- shared 文件夹下的代码是全局共享的,比如一些工具函数、常量等等
- 【入口包】vue 文件夹下的代码是入口文件,提供开发者使用的内容
compiler-core、compiler-dom、compiler-sfc
compiler-core
compiler-core 是 Vue 3 的编译器核心,它负责将 Vue 模板编译为 JavaScript 渲染函数。
它是底层通用模块,不直接处理 .vue 文件,而是为各种 Vue 相关的编译场景提供基础能力(例如:在非 SFC 场景下编译纯模板字符串)。
- parser 解析器(Parser 负责 “读懂” 模板结构)
- 解析器的作用是将 Vue 模板字符串解析为 AST(抽象语法树)
- 解析器的输出结果是一个 AST,AST 中包含了模板的结构信息
- transformer 转换器(Transformer 负责 “理解” 模板语义)
- 转换器的作用是对 AST 进行转换,将 AST 转换为一个更优化的 AST
- 转换器的输出结果是一个优化后的 AST,优化后的 AST 中包含了更多的元数据信息
- codegen 代码生成器(Codegen 负责 “翻译” 为可执行代码render 函数)
- 代码生成器的作用是将 AST 转换为 JavaScript 代码
- 代码生成器的输出结果是一个渲染函数,渲染函数的作用是将数据渲染到 DOM 中
整体流程
- 通过 vite、webpack 启动(打包)vue 项目,vite-plugin-vue(webpack vue-loader)
- 分词器(Tokenizer 负责 “切分” 模板为 tokens)
- 解析器(Parser 将分词 tokens 转换为 ast)
- 转换器(Transformer 负责优化 ast)
- 代码生成器(Codegen 将 ast 转换为 render 函数)
- 渲染函数(Render 负责 “执行” 渲染函数,将数据渲染到 DOM 中)
- 挂载(Mount 负责将渲染函数挂载到 DOM 中)
compiler-dom
compiler-dom 是 Vue 3 的 DOM 编译器,它负责将 Vue 模板编译为 DOM 操作代码。
compiler-dom 是基于 compiler-core 的浏览器环境专用扩展,它在核心框架之上补充了浏览器 DOM 环境特有的编译逻辑,例如:
- 处理 HTML 标签、属性(如 class、style)、自闭合标签等 DOM 特性。
- 支持 v-html、v-text 等操作 DOM 的指令。
- 生成与浏览器 DOM 操作匹配的渲染函数代码(如创建真实 DOM 节点的 API 调用)。
compiler-sfc
compiler-sfc 是 Vue 3 的单文件组件编译器,它负责将 Vue 单文件组件编译为 JavaScript 渲染函数。
是上层专用模块,基于 compiler-core 实现了对 SFC 格式的完整编译流程,是 Vue 单文件组件开发的核心依赖(如 Vite、Vue CLI
等工具都会用到它)。
响应式处理
- dep 依赖
- track
- trigger
- notify
- collection 依赖收集
- MutableReactiveHandler,可读可写状态数据跟踪
- 当数据被访问时,触发 proxy 的 get,track 依赖收集
- 当数据被修改时,触发 proxy 的 set,trigger 依赖触发
- ref、reactive
- watch
- effect 副作用
运行时处理
runtime-core
runtime-core 是 Vue 运行时的核心模块,与具体平台(如浏览器、Node.js、Weex 等)无关,包含了 Vue 最核心的逻辑,是跨平台能力的基础。
主要作用:
- 虚拟 DOM 处理:实现虚拟 DOM 的创建、更新、diff 算法等核心逻辑,是 Vue 高效更新视图的基础。
- 组件系统:定义组件的生命周期、状态管理(如 setup、reactive、ref 等)、事件处理、Props 传递等核心功能。
- 响应式系统集成:与 @vue/reactivity 模块协作,实现数据变化驱动视图更新的响应式机制。
- 渲染调度:负责协调组件渲染的优先级、异步更新队列等,优化渲染性能。
- 平台无关的渲染逻辑:定义了渲染器(Renderer)的抽象接口,不直接操作 DOM,而是通过抽象接口与具体平台交互。
简单说,runtime-core 是 Vue 的 “灵魂”,包含了所有不依赖具体平台的核心逻辑,是实现跨平台的基础。
diff 算法
- 简单 diff 算法
- 双端 diff 算法
- 快速 diff 算法(终极版本)
runtime-dom
runtime-dom 是 针对浏览器平台的运行时扩展,基于 runtime-core 实现,负责将核心逻辑与浏览器的 DOM 环境对接。
主要作用:
- DOM 操作封装:提供浏览器特有的 DOM 操作方法,如创建元素(createElement)、插入元素(insert)、设置属性(setAttribute)、事件绑定(addEventListener)等。
- 处理浏览器特有属性和事件:例如 class、style 等属性的特殊处理,以及浏览器事件的兼容处理(如事件冒泡、默认行为等)。
- 集成浏览器 API:将 runtime-core 的抽象接口映射到实际的浏览器 API,例如将虚拟 DOM 转换为真实 DOM。
- 提供浏览器环境的渲染器:基于 runtime-core 的渲染器接口,实现浏览器环境下的具体渲染逻辑。
简单说,runtime-dom 是 Vue 与浏览器 DOM 交互的 “桥梁”,让 runtime-core 的核心逻辑能在浏览器中生效。
runtime-core 负责组件的状态管理、虚拟 DOM 构建和 diff 计算。
runtime-dom 负责将虚拟 DOM 转换为真实 DOM,并处理浏览器特有的交互(如点击事件、样式更新等)。
手写 reactive
1 |
|
疑问:为什么要用 Reflect 而不是直接用 target[key]?
为了解决 this 指向问题
举个例子:
1 |
|
执行上面的代码,我们会发现 在读取aliasName
属性时,会触发get aliasName() { return this.name + '花鹿' }
,这里面有this.name
,可是我们在读取this.name
的时候,没有触发下面的 proxy 的 get 的打印,这就说明这个 this
指向并不对,他没有被代理到。
我们再看下es6
中Reflect
的get
方法
基本语法:Reflect.get(target, propertyKey[, receiver])
- 参数说明:
- target:要获取属性值的目标对象
- propertyKey:要获取的属性名称(字符串或 Symbol)
- receiver(可选):如果 target 对象中指定了 getter,receiver 会作为 getter 函数的 this 值
- 返回值:返回获取到的属性值
手写 effect 副作用函数
effect
函数是一个核心概念,它负责创建 “副作用”(effect),并建立响应式数据与副作用之间的依赖关系。当响应式数据发生变化时,依赖它的副作用会自动重新执行,这是
Vue 3 响应式系统实现自动更新的基础机制。
基本概念
“副作用” 指的是一段会影响外部环境的代码(例如修改全局变量、更新 DOM 等)。在 Vue 中,组件的渲染函数就是一种典型的副作用(因为它会生成
DOM)。effect 函数的作用就是包裹这段副作用代码,并追踪其中使用的响应式数据,形成 “依赖收集”。
1 |
|
手写计算属性 computed
computed 示例
1 |
|
1 |
|
手写 ref
1 |
|
ref 和 reactive 的区别
ref
- 用于创建基本类型数据的响应式数据,当然也可以包装引用类型(
object
、array
) - 对于基本类型:创建一个
{value:原始值}
的包装对象,通过重写value
的getter
(收集依赖)和setter
(触发更新)实现响应式。 - 对于引用类型:内部会自动调用
reactive
方法,将引用类型转换为响应式对象,因此ref(obj).value
本质是一个reactive
对象。 - 在 js 中必须通过
.value
访问和修改值;在模板中可以直接访问,自动解包,不需要.value
。 - 引用类型的 ref 如果直接替换整个对象,新对象仍会被转为响应式,不丢失响应式
reactive
- 用于创建引用类型数据的响应式代理,无法直接处理基本类型数据(会警告且不生效)。
- 通过
Proxy
代理整个对象,拦截对象的get
和set
操作,实现响应式。如果对象存在嵌套属性,则递归处理。 - 无论在模板还是脚本中,都可以直接访问和修改属性,无需
.value
。 - 不能直接替换整个对象,否则会丢失响应式。
- 解构会丢失响应式,需要通过
toRef
或toRefs
处理,如:const {name,age} = toRefs(user)
。
watch 和 watchEffect 的区别
watch
- 显式指定依赖,需要手动声明要监听的数据源,仅当依赖变化时才会执行回调。
- 默认懒执行,初始化的时候不会自动执行回调,仅当被监听的数据变化时才触发。如果要初始化执行一次,需手动配置
immediate: true
。 - 能获得新旧值,回调函数接收两个参数:newVal(变化后的值)、oldVal(变化前的值),方便对比数据前后变化状态。
- 延迟执行,默认情况下,watch 会在数据变化后立即执行回调。如果要延迟执行,需配置
flush: 'post'
,在 Dom 更新后执行。
watchEffect
- 自动追踪依赖,无需手动声明依赖,自动追踪回调函数内部使用的所有响应式数据,只要函数内部访问的像原始数据发生变化,就会触发回调。
- 默认立即执行,初始化时会立即执行一次回调函数,无需配置
immediate: true
。 - 无参数,无法直接获得响应式数据变化前后的值。
如何将 template 转换为 render 函数
template 是声明式的模板语法,最终会被 vue 编译器编译为 render 函数,用于生成虚拟DOM。
将 template 转换为 render 函数,本质是将模板的标签、属性、指令、插值等语法,转换为通过 h
函数描述的 javascript 逻辑。
h 函数
Vue 提供的创建虚拟节点的函数,语法为:h(tag,props,children)
tag
:节点的标签名或组件,如'div'
、'span'
等。props
:节点的属性/指令/事件,如{id:'app',class:'container'}
等。children
:节点的子节点,如'hello world'
、[h('div','hello world')]
等。
示例
1 |
|
1 |
|
vue 的 diff 算法原理
Vue 的 diff 算法是虚拟 DOM 的核心组成部分,核心目的是通过对比新旧虚拟 DOM 数的差异,计算出最小化的 DOM 操作,从而提高渲染效率。
patch 方法
patch 方法是 Vue 中用于对比新旧虚拟 DOM 数差异并更新真实 DOM 的核心函数。
diff 算法的核心设计原则
同层比较,不跨层级
虚拟 DOM 是树形结构,diff 算法只对比同一层级的节点,不跨层级比较,算法复杂度为 O(n)。
先判断节点是否“可复用”
对比节点时,首先判断两个节点是否为“相同节点”,只有可复用的节点才需要进一步比较细节,否则直接销毁旧节点并创建新节点。
判断是否为“相同节点”的依据:
- 标签名相同
- key 相同
- 是否为注释节点(isComment)
- 对比组件节点,组件类型相同
Vue2的 diff 算法核心
当两个节点被判定为“相同节点”后,会进入详细对比流程,分为属性对比和子节点对比
属性对比
对比新旧节点的属性(attrs、class、style 等),找出差异并更新到真实 DOM:
子节点对比
Vue2采用的双指针法(头尾指针),具体流程如下:
- 初始化指针:
- 旧节点的头指针(oldStartIdx)
- 旧节点的尾指针(oldEndIdx)
- 新节点的头指针(newStartIdx)
- 新节点的尾指针(newEndIdx)
- 四种快速对比场景
- 头头对比
- 尾尾对比
- 旧头新尾对比
- 旧尾新头对比
- key 映射表查找(处理乱序场景)
- 为旧节点列表创建 key → 索引 的映射表(oldKeyToIdx),快速定位新节点的 key 在旧列表中是否存在。
- 若找到对应节点:判断是否为相同节点(避免 key 重复导致误判),若是则复用并移动到对应位置,否则创建新节点。
- 若未找到:直接创建新节点插入。
- 清理剩余节点
当某一侧的指针遍历完成后:- 若旧节点指针未遍历完,将剩余节点全部删除
- 若新节点指针未遍历完,将剩余节点全部创建并插入到对应位置
Vue3对 diff 算法的优化
- 静态标记:编译时对节点进行了标记,标记了该节点哪些是动态的(需要对比)。diff 算法在对比时,会跳过静态节点,只对比动态节点,从而提高对比效率。
- 最长递增子序列:处理列表乱序时,通过最长递增子序列算法算出最少的移动次数,减少 DOM 移动操作。
- 数结构打平:将虚拟 DOM 数的节点按层级打平为数组,通过索引快速访问,减少递归遍历的开销
vue 中 key 的作用
在 Vue 中,key 是一个特殊的属性,主要用于标识虚拟 DOM(VNode)节点的唯一性,其核心作用是帮助 Vue 的 diff
算法更高效、更准确地识别节点的变化,从而优化 DOM 更新性能并避免状态错乱
作为节点的 “唯一标识”,辅助 diff 算法识别可复用节点
Vue 的 diff 算法在对比新旧虚拟 DOM 时,首要任务是判断 “两个节点是否为同一个节点”(即是否可复用)。而 key
是判断的核心依据之一(配合标签名、组件类型等)。
- 当两个节点的 key 相同(且其他条件匹配,如标签名一致)时,Vue 会认为它们是 “同一个节点”,会尝试复用旧节点并仅更新差异(如属性、文本内容)。
- 当 key 不同时,Vue 会判定为 “不同节点”,会直接销毁旧节点并创建新节点,不会尝试复用。
这一机制大幅减少了 diff 算法的复杂度:通过 key 可以快速定位到可复用的节点,避免对 “看似不同但实际可复用” 的节点进行不必要的销毁和重建。
在列表渲染中避免 “节点复用错误”,保证状态正确
在使用 v-for 渲染列表时,key 是必须的(Vue 官方强制推荐),其核心作用是避免因节点复用导致的状态混乱。
错误使用:不使用 key 或使用索引(index)作为 key
优化 DOM 更新性能,减少不必要的操作
当列表发生排序、插入、删除等操作时,key 能帮助 diff 算法快速找到 “移动的节点”,从而仅通过 DOM 移动(而非销毁 +
重建)完成更新,大幅提升性能。
例如,对列表 [A, B, C] 排序为 [C, B, A]:
- 有 key 时,diff 算法能通过 key 识别出节点只是位置变化,直接移动 DOM 元素即可。
- 无 key 时,diff 算法可能无法识别移动,会销毁所有旧节点并重建新节点,性能损耗大。
总结
key 的核心作用是给节点一个 “唯一且稳定的身份标识”,帮助 Vue 的 diff 算法:
- 准确识别可复用的节点,避免状态错乱;
- 高效处理列表的增删改查,减少 DOM 操作,优化性能。
在实际开发中,列表渲染必须使用唯一 ID 作为 key,这是保证列表状态正确和性能优化的关键实践。