Vue 3 的响应式系统被誉为前端框架的"艺术品"。它如何在数据变化时精准触发视图更新?如何避免不必要的重渲染?
今天,我们不讲表面用法,直接从 V8 引擎的内存布局出发,深度剖析 Vue 响应式系统的底层实现机制。
1. 响应式系统的核心目标
响应式系统的本质是建立一个依赖追踪图(Dependency Graph) :
数据变化 → 触发 Getter → 收集依赖 → 执行 Setter → 通知更新 → 视图刷新
难点在于:
- 精准收集:只收集真正用到该数据的组件
- 高效通知:避免无关组件的重复渲染
- 嵌套支持:深层对象、数组的响应式处理
2. Vue 2 方案:Object.defineProperty 的局限
2.1 核心实现
javascript
function defineReactive(obj, key, val) {
const dep = new Dep(); // 依赖收集器
Object.defineProperty(obj, key, {
get() {
if (Dep.target) {
dep.depend(); // 收集当前 Watcher
}
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
dep.notify(); // 通知所有依赖更新
}
});
}
2.2 致命缺陷
| 问题 | 原因 | 影响 |
|---|---|---|
| 无法检测属性新增/删除 | Object.defineProperty 只能劫持已存在的属性 |
需要用 Vue.set |
| 数组变异方法失效 | 数组索引赋值不会触发 Setter | 需要重写 7 个数组方法 |
| 递归遍历性能差 | 初始化时需要深度遍历整个对象树 | 大型对象卡顿 |
3. Vue 3 方案:Proxy 的降维打击
3.1 核心实现
scss
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key); // 收集依赖
return isObject(res) ? reactive(res) : res;
},
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;
}
});
}
3.2 Proxy 的优势
| 特性 | Object.defineProperty | Proxy |
|---|---|---|
| 拦截范围 | 单个属性 | 整个对象 |
| 新增/删除属性 | 不支持 | ✅ 原生支持 |
| 数组索引操作 | ❌ 需重写方法 | ✅ 原生支持 |
| 性能 | 递归遍历 O(n) | 惰性代理 O(1) |
4. 依赖收集机制:Dep 与 Watcher 的协作
4.1 Dep(依赖收集器)
javascript
class Dep {
constructor() {
this.subscribers = new Set(); // 使用 Set 去重
}
depend() {
if (Dep.target) {
this.subscribers.add(Dep.target);
}
}
notify() {
this.subscribers.forEach(watcher => {
watcher.update();
});
}
}
4.2 WeakMap 存储映射
Vue 3 使用 WeakMap 建立数据到依赖的映射:
ini
const targetMap = new WeakMap();
function track(target, key) {
if (!Dep.target) return;
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let dep = depsMap.get(key);
if (!dep) {
dep = new Set();
depsMap.set(key, dep);
}
dep.add(Dep.target);
}
数据结构:
scss
targetMap (WeakMap)
└─ target (对象)
└─ depsMap (Map)
└─ key (属性名)
└─ dep (Set)
└─ effect (副作用函数)
5. 调度系统:异步更新队列
Vue 不会在数据变化时立即更新视图,而是使用异步批处理:
ini
const queue = [];
let pending = false;
function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job);
}
if (!pending) {
pending = true;
nextTick(flushJobs);
}
}
function flushJobs() {
queue.sort((a, b) => a.id - b.id); // 按优先级排序
for (const job of queue) {
job();
}
queue.length = 0;
pending = false;
}
优势:
- 多次数据变化只触发一次渲染
- 避免中间状态导致的闪烁
- 按优先级排序,确保父子组件更新顺序
6. 计算属性与侦听器:衍生状态处理
6.1 Computed(惰性求值)
ini
function computed(getter) {
let value;
let dirty = true;
const runner = effect(getter, {
scheduler: () => {
dirty = true; // 标记为脏数据
}
});
return {
get value() {
if (dirty) {
value = runner();
dirty = false;
}
return value;
}
};
}
核心机制:
- 只有在访问时才计算(惰性)
- 依赖变化时标记
dirty,下次访问重新计算 - 避免不必要的重复计算
6.2 Watch(主动侦听)
scss
function watch(source, callback) {
const getter = () => traverse(source);
effect(getter, {
scheduler: () => {
callback(getter());
}
});
}
7. 工业界实战:性能优化技巧
7.1 markRaw(跳过响应式)
ini
const rawObj = markRaw({ /* 大型数据 */ });
不需要响应式的对象(如图表实例、第三方库对象),用 markRaw 标记,避免 Proxy 开销。
7.2 shallowReactive(浅层响应式)
php
const state = shallowReactive({
nested: { deep: { value: 1 } }
});
只代理第一层,嵌套对象保持原始引用,减少内存占用。
7.3 冻结对象优化
ini
const constant = Object.freeze({ /* 常量配置 */ });
Vue 会自动跳过已冻结的对象,不会进行响应式转换。
8. 面试考点
Q1: Vue 2 为什么无法检测对象属性的新增?
A:
Object.defineProperty只能劫持对象上已存在的属性。新增属性时没有 Getter/Setter,需要在初始化时递归遍历所有属性,动态新增的属性无法被劫持。
Q2: Proxy 为什么比 Object.defineProperty 性能好?
A: Proxy 是惰性代理,只有在访问属性时才递归代理子对象。而
Object.defineProperty在初始化时需要完整遍历整个对象树,时间复杂度 O(n)。
Q3: Vue 3 的依赖收集用了什么数据结构?
A: 使用
WeakMap → Map → Set三层映射。targetMap(WeakMap)存储目标对象,depsMap(Map)存储属性名,dep(Set)存储副作用函数。使用 Set 自动去重。
9. 总结
Vue 响应式系统的核心设计:
- 数据劫持:Proxy 拦截属性访问
- 依赖收集:WeakMap 建立映射关系
- 副作用调度:异步队列批量更新
- 惰性求值:Computed 避免重复计算
这套系统不仅是 Vue 的核心,更是响应式编程范式的经典实现。理解它,你就掌握了现代前端框架的精髓。
💡 提示: 完整源码解析(含 Dep/Watcher 实现)已开源到 GitHub。
如果你觉得这篇关于"Vue 底层原理"的文章对你有帮助,欢迎点赞收藏!