Vue 核心原理笔记:响应式原理与渲染机制
文章目录
Vue 核心原理笔记:响应式原理与渲染机制
1. Vue 响应式原理
核心目标:实现「数据与视图自动同步」—— 当 Model(数据)变化时,View(视图)自动更新;当用户操作 View 时,Model 同步更新(双向绑定是响应式的典型应用,而非响应式的全部)。
响应式系统的三大核心组成:
-
数据层(Model):应用状态数据及业务逻辑(如组件的
data、ref、reactive数据)。 -
视图层(View):数据的可视化展示(如模板、DOM 元素)。
-
响应式桥接层(ViewModel):框架封装的核心(Vue2 的
Object.defineProperty+Watcher,Vue3 的Proxy+Effect),负责监听数据变化、触发视图更新。

1.1 响应式核心模块
Vue 响应式系统的核心是「数据监听-依赖收集-触发更新」的闭环,核心模块可明确为以下四部分:
1.1.1 监听器(Observer)
作用:将普通数据(对象/数组)转换为响应式数据,监听数据的「读取」和「修改」操作,是响应式的入口。
版本差异:
-
Vue2:通过
Object.defineProperty重写数据的getter/setter,仅能监听对象属性的读写。 -
Vue3:通过
Proxy代理整个对象(天然支持对象/数组),通过getter/setter包裹处理ref基础类型数据。
1.1.2 依赖收集器(Dep)
作用:为每个响应式属性维护一个「订阅者集合」(Set<Effect/Watcher>),记录哪些逻辑依赖该属性,是连接监听器与副作用的桥梁。
核心逻辑:
-
数据被「读取」时,将当前活跃的副作用(
Effect)加入集合。 -
数据被「修改」时,遍历集合中所有副作用并触发重新执行。
1.1.3 副作用(Effect / Watcher)
作用:依赖响应式数据的「执行逻辑」,数据变化时需重新运行,是响应式的「消费端」。
常见类型:
-
渲染副作用:组件渲染函数(数据变 → 重新渲染 DOM)。
-
计算副作用:
computed回调(依赖变 → 重新计算值)。 -
监听副作用:
watch/watchEffect回调(依赖变 → 触发自定义逻辑)。
版本差异:Vue2 中称为 Watcher,Vue3 统一为 Effect(底层实现逻辑一致,命名更贴合「副作用」语义)。
1.1.4 编译器(Compiler)
作用:编译阶段(构建时/运行时)将模板转换为渲染函数,同时标记动态节点(如 {{}}、:bind),为运行时优化提供关键信息(如修补标记)。
运行时配合:渲染函数执行时触发数据「读取」,完成依赖收集;数据变化时,副作用重新执行渲染函数,生成新的虚拟 DOM。
1.2 双向绑定与响应式的关系
-
响应式是「数据 → 视图」的基础能力(Vue 核心的单向数据流依赖此实现)。
-
双向绑定(
v-model)是响应式的「增强应用」:在「数据 → 视图」的基础上,通过监听视图事件(如input、change)反向更新数据,本质是v-bind+v-on的语法糖。 -
注意:Vue 核心是「单向响应式」,双向绑定仅用于表单等特定场景,避免混淆两者概念。
1.3 响应式底层实现差异(Vue2 vs Vue3)
1.3.1 Vue2:Object.defineProperty 实现
核心逻辑:遍历对象属性,重写 getter(收集依赖)和 setter(触发更新)。
// Vue2 响应式核心伪代码
function defineReactive(obj, key, value) {
// 递归处理嵌套对象
if (typeof value === 'object') new Observer(value);
const dep = new Dep(); // 为当前属性创建依赖收集器
Object.defineProperty(obj, key, {
get() {
// 读取时收集依赖(当前活跃副作用加入 Dep)
if (Dep.target) dep.addSub(Dep.target);
return value;
},
set(newVal) {
if (newVal === value) return;
value = newVal;
// 修改时触发更新(通知 Dep 中所有副作用执行)
dep.notify();
}
});
}
// 数组兼容:重写数组原型方法(push、pop 等)
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push', 'pop', 'splice'].forEach(method => {
arrayMethods[method] = function() {
arrayProto[method].apply(this, arguments);
// 触发数组所在对象的更新
dep.notify();
};
});
局限性(核心缺陷):
-
无法监听数组索引变化(如
arr[0] = 1)和数组长度变化(如arr.length = 0),需通过重写数组原型方法实现兼容。 -
无法监听对象新增属性(如
obj.newKey = 1),需通过Vue.set(obj, 'newKey', 1)手动触发响应式。 -
需递归遍历嵌套对象才能实现深响应,初始化性能开销较大。
1.3.2 Vue3:Proxy + Ref 实现
核心逻辑:
-
reactive:通过Proxy代理整个对象,天然支持对象新增属性、数组索引/长度变化,默认深响应。 -
ref:通过getter/setter包裹基础类型(string、number等),解决Proxy无法代理基础类型的问题。
// Vue3 响应式核心伪代码
function reactive(obj) {
// 代理整个对象,支持对象/数组
return new Proxy(obj, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver);
track(target, key); // 收集依赖
// 懒代理:访问嵌套对象时才动态代理
return typeof result === 'object' ? reactive(result) : result;
},
set(target, key, value, receiver) {
const oldVal = Reflect.get(target, key, receiver);
const success = Reflect.set(target, key, value, receiver);
if (success && oldVal !== value) {
trigger(target, key); // 触发更新
}
return success;
}
});
}
function ref(value) {
// 包裹基础类型,通过 .value 访问/修改
const refObject = {
get value() {
track(refObject, 'value'); // 收集依赖
return value;
},
set value(newValue) {
if (newValue === value) return;
value = newValue;
trigger(refObject, 'value'); // 触发更新
}
};
return refObject;
}
优势:
-
无需递归遍历初始对象,访问嵌套属性时才动态代理(懒代理),初始化性能更优。
-
天然支持对象新增属性、数组索引/长度变化,无需手动调用 API。
-
提供
shallowReactive、shallowRef支持浅响应,满足灵活场景需求。 -
通过
toRef/toRefs解决响应式对象解构后断开连接的问题。
1.3.3 实战补充:解构响应式对象的解决方案
响应式对象解构后,普通变量会丢失 getter/setter,导致响应式断开,需用 toRefs 处理:
import { reactive, toRefs } from 'vue'
const user = reactive({ name: '张三', age: 20 })
// 问题:解构后 name 是普通变量,无响应式
const { name } = user
name = '李四' // 不触发更新
// 解决方案:toRefs 转换后,属性仍为响应式
const { name: reactiveName, age } = toRefs(user)
reactiveName.value = '李四' // 触发响应式更新
age.value = 21 // 触发响应式更新
1.4 响应式原理闭环(完整链路)
以 Vue3 为例,响应式从数据定义到视图更新的完整流程:
-
步骤1:创建响应式数据:调用
reactive/ref生成响应式数据(Proxy 代理或 getter/setter 包裹)。 -
步骤2:执行副作用:运行副作用(如渲染函数、
watchEffect),过程中读取响应式数据。 -
步骤3:依赖收集:触发数据的
getter(ref)或Proxy.get(reactive),调用track方法将当前副作用加入对应属性的Dep集合。 -
步骤4:修改数据触发更新:修改响应式数据,触发
setter(ref)或Proxy.set(reactive),调用trigger方法通知Dep中所有副作用重新执行。 -
步骤5:副作用执行更新视图:
渲染副作用:重新执行渲染函数生成新虚拟 DOM,对比旧虚拟 DOM 后更新真实 DOM。 -
计算副作用:重新计算值并同步到计算属性。
-
监听副作用:执行自定义监听回调。
2. Vue 渲染机制
Vue 渲染机制的核心是「将响应式数据转换为真实 DOM」,依赖「虚拟 DOM + 编译优化 + 响应式副作用」实现高效更新,核心流程分为「编译 → 挂载 → 修补」三步。
2.1 虚拟 DOM(Virtual DOM)
虚拟 DOM 是一个与平台无关的 JavaScript 对象,包含创建真实 DOM 所需的所有信息(类型、属性、子节点等),是连接数据与真实 DOM 的中间层。
// 虚拟 DOM 示例(vnode)
const vnode = {
type: 'div', // 节点类型
props: { id: 'app' }, // 节点属性
children: [ // 子节点(可嵌套其他 vnode)
{ type: 'span', children: 'Hello Vue' }
]
}
2.1.1 虚拟 DOM 核心价值
-
跨平台能力:虚拟 DOM 与平台无关,可被渲染为浏览器 DOM、小程序节点、Native 组件(如 Vue Native)等。
-
减少 DOM 操作开销:DOM 操作比 JavaScript 运算慢得多,虚拟 DOM 通过批量对比差异(diff),只更新变化的部分,减少频繁 DOM 操作导致的回流重绘。
-
声明式编程支持:开发者只需描述「目标视图状态」,虚拟 DOM 负责处理「如何从旧状态更新到新状态」,无需手动操作 DOM。
2.2 渲染管线(完整流程)
Vue 渲染分为「编译、挂载、修补」三个阶段,形成完整的渲染管线,其中「挂载、修补」阶段与响应式系统深度联动。
2.2.1 阶段1:编译(Template → 渲染函数)
将模板字符串转换为可执行的渲染函数(返回虚拟 DOM 树),可通过「构建时编译」(借助 vue-loader)或「运行时编译」(Vue 完整版)完成,核心分三步:
-
解析(Parse):将模板字符串解析为抽象语法树(AST),标记出静态节点、动态节点、指令等信息。
-
优化(Optimize):标记静态节点(如纯文本节点)和动态节点的更新类型(如仅 class 变化),生成修补标记(patch flag),为运行时优化提供依据。
-
生成(Generate):将优化后的 AST 转换为渲染函数代码(如
_sfc_render函数)。
示例:模板 <div :class="cls">{{ msg }}</div> 编译后生成的渲染函数:
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", {
class: _ctx.cls
}, _toDisplayString(_ctx.msg), 2 /* CLASS */))
// 最后一个参数 2 是修补标记,标识该节点仅 class 动态变化
}
2.2.2 阶段2:挂载(渲染函数 → 真实 DOM)
将渲染函数生成的虚拟 DOM 树转换为真实 DOM 并插入页面,同时完成依赖收集,核心分两步:
-
生成虚拟 DOM:执行渲染函数,生成初始虚拟 DOM 树。
-
创建真实 DOM:渲染器遍历虚拟 DOM 树,调用原生 DOM API(如
document.createElement)创建真实 DOM 节点,插入到挂载点(如#app)。
与响应式联动:挂载阶段会将渲染函数作为响应式副作用执行,过程中读取的响应式数据会被收集到 Dep 中,建立「数据 → 视图」的依赖关联。
2.2.3 阶段3:修补(数据变化 → 视图更新)
当响应式数据变化时,触发副作用重新执行,生成新虚拟 DOM 树,通过对比新旧虚拟 DOM 树的差异,仅将变化部分应用到真实 DOM,核心分两步:
-
差异对比(diff):渲染器采用「同层对比」策略遍历新旧虚拟 DOM 树,通过节点类型、key、修补标记等信息,快速定位变化的节点。
-
应用更新(patch):对变化的节点执行最小化 DOM 操作(如修改 class、更新文本、新增节点),避免全量重绘。
2.3 带编译时信息的虚拟 DOM(Vue 核心优化)
Vue 同时控制编译器和运行时,编译器可在编译阶段为虚拟 DOM 注入「编译时信息」(如修补标记、静态节点标识),让运行时渲染器跳过无变化的节点,实现「高效 diff」,这是 Vue 虚拟 DOM 区别于 React 等纯运行时虚拟 DOM 的核心优势。
2.3.1 优化1:静态提升(Static Hoisting)
核心逻辑:模板中完全静态的节点(无动态绑定)会被提升到渲染函数外部,每次渲染时直接复用同一个 vnode,避免重复创建和对比。
进阶优化:连续的静态节点会被「预字符串化」为 HTML 字符串,挂载时直接通过 innerHTML 创建 DOM,后续复用通过原生 cloneNode() 克隆,性能更优。
// 编译后:静态节点被提升到渲染函数外部
const _hoisted_1 = _createElementVNode("div", null, "静态文本1")
const _hoisted_2 = _createElementVNode("div", null, "静态文本2")
function _sfc_render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_1, // 直接复用静态 vnode
_hoisted_2, // 直接复用静态 vnode
_createElementVNode("div", null, _ctx.dynamic) // 动态节点
]))
}
2.3.2 优化2:修补标记(Patch Flags)
核心逻辑:编译器为动态节点添加「修补标记」,标识该节点的更新类型(如仅 class 变化、仅文本变化),运行时渲染器通过位运算快速判断更新范围,跳过无变化的属性和子节点。
常用修补标记表:
| 修补标记常量 | 数值 | 含义 | 应用场景 |
|---|---|---|---|
| TEXT | 1 | 仅文本子节点动态 | {{ dynamic }} |
| CLASS | 2 | 仅 class 动态 | :class="cls" |
| STYLE | 4 | 仅 style 动态 | :style="style" |
| PROPS | 8 | 仅 props 动态 | :id="id"、:value="val" |
| STABLE_FRAGMENT | 64 | 片段子节点顺序稳定 | 多根节点模板(无 v-if/v-for) |
| DYNAMIC_FRAGMENT | 128 | 片段子节点顺序动态 | 含 v-for 的多根节点 |
| 运行时判断逻辑:通过位运算快速匹配更新类型,仅执行必要操作: |
// 若节点仅 class 动态,则只更新 class
if (vnode.patchFlag & PatchFlags.CLASS) {
updateElementClass(el, vnode.props.class);
}
2.3.3 优化3:树结构打平(Tree Flattening)
核心概念:区块(Block)—— 由结构性指令(v-if、v-for)分割的、内部结构稳定的虚拟 DOM 片段。
核心逻辑:编译时,每个区块会「打平」其内部的「动态后代节点」为一个数组(静态后代节点无需追踪),重渲染时仅遍历该数组,而非整棵虚拟 DOM 树,大幅减少遍历节点数量。
示例:
<div>
<div>静态文本</div>
<div :id="id">
<span>{{ text }}</span>
</div>
<div v-if="show">
{{ dynamic }}
</div>
</div>
根区块打平后的动态数组:[div:id, 区块v-if],重渲染时仅遍历这两个节点,跳过所有静态节点。
2.3.4 优化4:其他编译优化
-
事件处理函数缓存:模板中
@click="handleClick"会被编译为cacheHandlers: true,避免每次渲染创建新函数,导致子组件不必要的重渲染。 -
动态指令合并:多个动态绑定(如
:id="id" :class="cls")会被合并为一个对象,减少 vnode props 的创建开销。 -
SSR 激活优化:修补标记和树结构打平让 SSR 激活时仅遍历动态节点,实现高效的部分激活。
2.4 虚拟 DOM Diff 算法核心原则
Vue diff 算法基于「同层对比」和「key 匹配」,时间复杂度优化为 O(n),核心原则如下:
-
同层对比:仅对比虚拟 DOM 树的同一层级节点,不跨层级对比(如父节点不与子节点对比),降低算法复杂度。
-
节点类型判断:
节点类型不同(如div→p):直接销毁旧节点,创建新节点,无需深入子节点对比。 -
节点类型相同:对比 props 和子节点,结合修补标记执行最小化更新。
-
列表 Diff 与 key:通过
key建立新旧列表节点的映射关系,最小化移动、新增、删除操作(无 key 时会采用「就地更新」,可能导致节点状态错误)。
3. 核心原理总结
| 模块 | Vue2 核心实现 | Vue3 核心实现 | 核心优势 |
|---|---|---|---|
| 响应式 | Object.defineProperty + Watcher | Proxy + Effect + Ref | 支持数组/新增属性,懒代理,性能更优 |
| 渲染机制 | 纯运行时虚拟 DOM,全量 diff | 编译时优化 + 虚拟 DOM,定向 diff | 静态提升、修补标记等优化,更新效率更高 |
| 核心链路串联:响应式数据变化 → 触发 Effect 副作用 → 重新执行渲染函数生成新虚拟 DOM → 基于编译时信息高效 diff → 最小化更新真实 DOM,最终实现「数据与视图自动同步」。 |
更多推荐



所有评论(0)