Vue 响应式系统源码级剖析:从 Object.defineProperty 到 Proxy

Vue 3 的响应式系统被誉为前端框架的"艺术品"。它如何在数据变化时精准触发视图更新?如何避免不必要的重渲染?

今天,我们不讲表面用法,直接从 V8 引擎的内存布局出发,深度剖析 Vue 响应式系统的底层实现机制。

1. 响应式系统的核心目标

响应式系统的本质是建立一个依赖追踪图(Dependency Graph)

复制代码
数据变化 → 触发 Getter → 收集依赖 → 执行 Setter → 通知更新 → 视图刷新

难点在于:

  1. 精准收集:只收集真正用到该数据的组件
  2. 高效通知:避免无关组件的重复渲染
  3. 嵌套支持:深层对象、数组的响应式处理

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 响应式系统的核心设计:

  1. 数据劫持:Proxy 拦截属性访问
  2. 依赖收集:WeakMap 建立映射关系
  3. 副作用调度:异步队列批量更新
  4. 惰性求值:Computed 避免重复计算

这套系统不仅是 Vue 的核心,更是响应式编程范式的经典实现。理解它,你就掌握了现代前端框架的精髓。


💡 提示: 完整源码解析(含 Dep/Watcher 实现)已开源到 GitHub。

如果你觉得这篇关于"Vue 底层原理"的文章对你有帮助,欢迎点赞收藏!

相关推荐
前端那点事2 小时前
Vue十万条数据渲染无卡顿!3种工业级方案(附可复制代码+避坑指南)
前端·vue.js
神奇小汤圆3 小时前
快手一面:为什么要求用Static来修饰ThreadLocal变量?
javascript
用户6688599847663 小时前
第一个Vue3.0程序
vue.js
行业研究员3 小时前
HTML头部元信息避坑指南大纲
javascript
Beginner x_u3 小时前
前端八股整理总索引|JS/TS、HTML/CSS、Vue、浏览器、工程化与手写题
前端·javascript·html
Cobyte3 小时前
10.响应式系统演进:通过位运算优化动态依赖收集(Vue3.2)
前端·javascript·vue.js
yqcoder4 小时前
JavaScript 事件流:从“捕获”到“冒泡”的完整旅程
服务器·前端·javascript
普修罗双战士4 小时前
项目设计-文章系统发布文章完整前后端设计
java·数据库·vue.js·spring boot·git·intellij-idea
Csvn4 小时前
Vue 3 Composition API 深度解析
前端·vue.js