要彻底理解 Vue 3 的实现原理,我们需要先抛开框架的光环,回到最基础的原生 JavaScript 层面。
使用原生 JavaScript 操作 DOM 时,我们通常面临两个核心痛点:
-
状态同步麻烦:数据发生变化时,需要手动获取 DOM 元素并更新对应的内容(例如
document.getElementById('msg').innerText = newMsg)。当应用变得庞大,这种“命令式”的代码会导致逻辑极度混乱。 -
性能开销大:频繁、无序地直接操作真实的 DOM 元素是非常消耗性能的。
Vue 3 的核心本质,就是用一套精妙的 JavaScript 算法,帮开发者自动、高效地完成“数据到 DOM 的映射”。
这套机制可以拆解为三大核心模块:响应式系统(Reactivity)、虚拟 DOM 与渲染器(Renderer)、编译器(Compiler)。
一、 响应式系统(Reactivity):让数据变化能被“感知”
Vue 3 响应式系统的核心是拦截(或者说代理)对象的操作。当你修改一个变量时,Vue 需要知道这个变量被修改了,从而去触发页面更新。
在 Vue 3 中,这是通过 ES6 的 Proxy 对象实现的。Proxy 可以理解为在目标数据之前架设了一层“拦截网”。
1. 核心思路:依赖收集(Track)与触发更新(Trigger)
假设我们有一个渲染页面的函数,它依赖了某个数据。我们希望:在渲染页面时,记录下用到了哪些数据(依赖收集);在这些数据改变时,重新执行渲染函数(触发更新)。
下面是用纯原生 JavaScript 模拟 Vue 3 响应式的核心代码:
JavaScript
// 用于存储副作用函数(例如渲染界面的函数)的全局变量
let activeEffect = null;
// effect 函数:接收一个函数,并在其内部所依赖的数据发生变化时重新执行
function effect(fn) {
activeEffect = fn; // 将当前函数设为激活状态
fn(); // 执行一次函数,这会触发内部数据的“读取”操作
activeEffect = null; // 执行完毕后清除
}
// 依赖存储结构:WeakMap -> Map -> Set
// 结构为:target (原始对象) -> key (属性名) -> Set (依赖该属性的 effect 函数集合)
const targetMap = new WeakMap();
// 依赖收集函数
function track(target, key) {
if (activeEffect) {
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); // 将当前的 effect 函数收集起来
}
}
// 触发更新函数
function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (dep) {
dep.forEach(effectFn => effectFn()); // 遍历执行所有收集到的副作用函数
}
}
// 响应式数据工厂函数 (类似 Vue 3 的 reactive)
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key); // 核心:读取属性时,进行依赖收集
return result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 核心:修改属性时,触发对应的依赖函数
}
return result;
}
});
}
2. 运作流程展示
通过这段逻辑,Vue 就实现了“数据驱动”。我们只需要修改被 Proxy 包裹的数据,之前绑定过该数据的函数就会自动重新执行。
二、 虚拟 DOM 与渲染器(Renderer):高效地绘制页面
当响应式数据改变,导致渲染函数重新执行时,如果我们直接用原生 JS 去重绘整个 DOM(如 innerHTML = ...),性能损耗极大。
为了解决这个问题,Vue 引入了虚拟 DOM(Virtual DOM)。虚拟 DOM 本质上就是一个普通的 JavaScript 对象,它用来描述真实的 DOM 结构。
1. 什么是虚拟 DOM (VNode)
一段真实的 HTML 代码:
HTML
<div class="box" id="app">Hello Vue</div>
对应的虚拟 DOM 对象大概长这样:
JavaScript
const vnode = {
tag: 'div',
props: {
class: 'box',
id: 'app'
},
children: 'Hello Vue'
}
2. 渲染器的工作:挂载(Mount)与打补丁(Patch)
渲染器(Renderer)包含两个主要职责:
-
挂载 (Mount):把第一次生成的 VNode 转化为真实的 DOM 并插入页面。
-
打补丁 (Patch/Diff):当数据变化产生新的 VNode 时,将新旧两个 VNode 进行比对(Diff 算法),只找出需要修改的地方,然后精准地去操作真实 DOM。
原生 JS 模拟渲染器的核心逻辑如下:
JavaScript
// 创建真实 DOM 并挂载
function mount(vnode, container) {
// 1. 根据 tag 创建真实 DOM 节点,并将真实 DOM 存储在 vnode 上以便后续使用
const el = (vnode.el = document.createElement(vnode.tag));
// 2. 处理属性 props
if (vnode.props) {
for (const key in vnode.props) {
el.setAttribute(key, vnode.props[key]);
}
}
// 3. 处理子节点 children
if (typeof vnode.children === 'string') {
el.textContent = vnode.children;
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => mount(child, el)); // 递归挂载
}
// 4. 插入容器
container.appendChild(el);
}
// 核心的 Diff 算法 (精简版思路)
function patch(n1, n2) {
// 假设两者 tag 相同,复用旧的真实 DOM 节点
const el = (n2.el = n1.el);
// 1. 比对 Props (属性)
const oldProps = n1.props || {};
const newProps = n2.props || {};
for (const key in newProps) { // 添加或更新新属性
if (newProps[key] !== oldProps[key]) {
el.setAttribute(key, newProps[key]);
}
}
for (const key in oldProps) { // 删除旧属性
if (!(key in newProps)) {
el.removeAttribute(key);
}
}
// 2. 比对 Children (子节点) - 这里是 Diff 算法最复杂的地方
const oldChildren = n1.children;
const newChildren = n2.children;
if (typeof newChildren === 'string') {
// 新的是文本
if (oldChildren !== newChildren) {
el.textContent = newChildren;
}
} else if (Array.isArray(newChildren)) {
// 新的是数组
if (typeof oldChildren === 'string') {
// 旧的是文本,清空文本并挂载新数组
el.innerHTML = '';
newChildren.forEach(child => mount(child, el));
} else {
// 核心:新旧都是数组,需要进行复杂的 Diff 比对
// Vue 3 这里采用的是基于 "最长递增子序列" 的快速 Diff 算法
// 目的是用最少的移动操作,把旧 DOM 序列变成新 DOM 序列
const commonLength = Math.min(oldChildren.length, newChildren.length);
// 先 patch 公共长度的部分
for (let i = 0; i < commonLength; i++) {
patch(oldChildren[i], newChildren[i]);
}
// 如果新节点更多,挂载新增的
if (newChildren.length > oldChildren.length) {
newChildren.slice(oldChildren.length).forEach(child => mount(child, el));
}
// 如果旧节点更多,卸载多余的
else if (oldChildren.length > newChildren.length) {
for (let i = newChildren.length; i < oldChildren.length; i++) {
el.removeChild(oldChildren[i].el);
}
}
}
}
}
Vue 3 的 Diff 算法相比 Vue 2 做了大量优化,引入了静态提升、补丁标记(PatchFlag)和最长递增子序列算法。它在编译阶段就标记好了哪些节点是动态的(会发生变化),在比对时直接跳过静态节点,极大地提升了比对速度。
三、 编译器(Compiler):将模板转化为代码
在开发时,我们写的是 Vue 特有的模板语法:
HTML
<template>
<div id="app">
<p>{{ msg }}</p>
</div>
</template>
但由于虚拟 DOM 是一个 JavaScript 对象,浏览器和 JavaScript 引擎并不认识上面这种包含了 {{ }} 的 HTML 字符串。
编译器的任务就是:把上面这段类似 HTML 的字符串,转化为一个能返回虚拟 DOM 对象(VNode)的 JavaScript 函数(渲染函数 Render Function)。
编译的三个步骤
代码段
-
Parse (解析):利用正则表达式等词法分析手段,把模板字符串切割,并解析成一个描述 HTML 结构的对象树,即 AST (抽象语法树)。
-
Transform (转换):遍历这棵 AST,将里面的 Vue 指令(如
v-if,v-for)转换为对应的逻辑代码标记。同时在这个阶段,Vue 会进行静态分析,给动态节点打上PatchFlag(补丁标记),告诉渲染器将来只用更新这些动态部位。 -
Generate (生成):将处理好的 AST 转换为一段 JavaScript 原生字符串代码,并构造出一个函数。
经过编译器处理,最终在内存中生成的渲染函数(简化版)如下:
JavaScript
// 编译后生成的 JS 渲染函数
function render() {
return {
tag: 'div',
props: { id: 'app' },
children: [
{
tag: 'p',
// 这里的 this.msg 会触发响应式系统(Proxy)的 get 拦截
children: this.msg
}
]
}
}
四、 融会贯通:Vue 3 的完整运行链路
现在,我们将响应式系统、编译器、渲染器拼接在一起,这也是 Vue 3 组件从初始化到页面更新的完整工作流。
代码段
完整执行逻辑如下:
-
初始化阶段:
-
Vue 拿到你的组件对象。
-
编译器将
<template>中的内容编译为render函数。 -
响应式模块把
data或者setup中返回的数据通过Proxy转化为响应式对象。
-
-
首次挂载阶段 (Mount):
-
Vue 内部调用
effect(component.render)。 -
由于是在
effect中执行,render函数运行的过程中,会读取响应式数据的属性,触发Proxy的get。 -
响应式系统记录下:这个数据的依赖是当前的
render函数。 -
render函数执行完毕,返回一棵完整的 VNode 树。 -
渲染器的
mount函数将这棵 VNode 树转换为真实的 DOM 节点,插入页面。
-
-
更新阶段 (Update):
-
当用户操作(如点击按钮)修改了某个响应式数据。
-
触发
Proxy的set,响应式系统查找到依赖这个数据的render函数,并触发其重新执行。 -
render函数再次运行,基于新的数据,生成一棵新的 VNode 树。 -
渲染器的
patch函数拿着旧的 VNode 和新的 VNode 进行 Diff 比对。 -
比对出两棵树的差异(例如某个文本节点从 'A' 变成了 'B'),最后渲染器指挥原生 JS 去修改对应的真实 DOM 节点。
-
以上就是从原生 JavaScript 出发,推导出 Vue 3 底层运作全貌的实现原理。一切看似魔法的自动更新,其基石建立在数据劫持(Proxy)、虚拟结构对比(VDOM Diff)以及编译期优化这三大支柱之上。