基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

Vue 3.0 的响应式原理

知识点图片

Vue 3.0 的响应式系统相较于 Vue 2.x 发生了根本性的变化。核心在于它抛弃了 Vue 2 中的 Object.defineProperty,转而使用 ES6 的 ProxyReflect API。

以下是 Vue 3.0 响应式原理的详细解析,分为核心机制、工作流程、与 Vue 2 的对比以及代码实现模型四个部分。


1. 核心机制:Proxy 与 Reflect

Vue 3 使用 Proxy 对象创建一个代理,拦截对原始对象的所有操作(读、写、删除等)。

  • Proxy (代理): 就像在目标对象外面包裹了一层壳。当你访问或修改数据时,必须先经过这层壳。这使得 Vue 能够捕获到任何对数据的变动。
  • Reflect (反射): 配合 Proxy 使用。在 Proxy 的拦截器(Handler)中,使用 Reflect 来执行原本要在对象上进行的操作(如 Reflect.get()Reflect.set())。它的主要作用是确保 this 指向正确,并返回操作结果。

2. 响应式工作流程

Vue 3 的响应式系统主要由三个动作组成:拦截 (Intercept)依赖收集 (Track)派发更新 (Trigger)

A. 拦截 (Intercept)

当你把一个普通对象传给 reactive 函数时,Vue 会返回这个对象的 Proxy 实例。

  • Get (读取): 当读取属性时,触发 get 拦截器。
  • Set (写入): 当修改属性时,触发 set 拦截器。
  • Delete (删除): 当删除属性时,触发 deleteProperty 拦截器。

B. 依赖收集 (Track) - 在 get 中进行

当某个副作用函数(Effect,例如组件的渲染函数、computed、watch)读取响应式数据时:

  1. 触发 Proxy 的 get 拦截器。
  2. 调用 track 函数。
  3. Vue 会将当前正在运行的副作用函数(activeEffect)记录下来,存入一个全局的 WeakMap 数据结构中。

存储结构:
targetMap (WeakMap) -> key: target对象, value: depsMap (Map)
depsMap (Map) -> key: 属性名, value: dep (Set)
dep (Set) -> value: effect函数

简单理解: Vue 记了个小本本:哪个对象的哪个属性,被哪个函数用到了。

C. 派发更新 (Trigger) - 在 set 中进行

当响应式数据被修改时:

  1. 触发 Proxy 的 set 拦截器。
  2. 调用 trigger 函数。
  3. Vue 根据目标对象和属性名,去 targetMap 中找到对应的 dep 集合。
  4. 遍历集合,执行所有相关的副作用函数(比如重新渲染组件)。

3. 为什么比 Vue 2 好? (Vue 3 vs Vue 2)

Vue 2 使用 Object.defineProperty 将属性转化为 getter/setter。Vue 3 使用 Proxy 有以下显著优势:

特性 Vue 2 (Object.defineProperty) Vue 3 (Proxy)
新增/删除属性 无法监听 (需要 Vue.set / Vue.delete) 原生支持 (直接 obj.newProp = x 即可)
数组监听 受限 (不能监听索引和 length 变化,重写了数组方法) 原生支持 (直接修改索引或 length 均可)
嵌套对象性能 递归遍历 (初始化时一次性递归所有层级,大对象性能差) 懒代理 (Lazy) (只有访问到嵌套属性时才通过 Proxy 代理下一层)
数据结构支持 仅支持 Object 支持 Map, Set, WeakMap, WeakSet

4. 简易代码实现模型

为了帮助理解,这里写一个极简版的 Vue 3 reactive 实现:

javascript
// 1. 存储副作用的容器
let activeEffect = null;

// 2. 依赖收集器 (数据结构: WeakMap -> Map -> Set)
const targetMap = new WeakMap();

// 3. track: 收集依赖
function track(target, key) {
    if (!activeEffect) 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(activeEffect); // 将当前副作用函数加入依赖集合
}

// 4. trigger: 触发更新
function trigger(target, key) {
    const depsMap = targetMap.get(target);
    if (!depsMap) return;

    const dep = depsMap.get(key);
    if (dep) {
        dep.forEach(effect => effect()); // 执行所有依赖该属性的函数
    }
}

// 5. reactive: 创建响应式对象
function reactive(target) {
    // 如果不是对象,直接返回
    if (typeof target !== 'object' || target === null) {
        return target;
    }

    const handler = {
        get(target, key, receiver) {
            // 收集依赖
            track(target, key);
            
            const result = Reflect.get(target, key, receiver);
            
            // 递归代理(懒代理):只有当访问到深层属性时,才对该属性进行响应式处理
            if (typeof result === 'object' && result !== null) {
                return reactive(result);
            }
            
            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;
        },
        deleteProperty(target, key) {
            const result = Reflect.deleteProperty(target, key);
            trigger(target, key); // 删除属性也要触发更新
            return result;
        }
    };

    return new Proxy(target, handler);
}

// --- 测试 ---

// 模拟一个副作用函数(比如组件渲染)
function effect(fn) {
    activeEffect = fn;
    fn(); // 立即执行一次,触发 get 从而收集依赖
    activeEffect = null;
}

const state = reactive({ count: 0, user: { name: 'Vue' } });

effect(() => {
    console.log('UI 更新了,count:', state.count);
});

state.count++; // 控制台输出: UI 更新了,count: 1

5. 关于 ref 的原理

你可能会问,Proxy 只能代理对象,那基本数据类型(如 string, number)怎么办?

这就是 ref 的作用。

  • ref 内部并没有使用 Proxy 来代理基本类型(因为 Proxy 无法代理基本类型)。
  • ref 创建了一个对象(RefImpl 类的实例),该对象有一个 .value 属性。
  • 它利用了 gettersetter (类似于 Vue 2 的方式) 来拦截对 .value 的访问和修改,从而实现依赖收集和更新触发。
  • 如果 ref 接收的是一个对象,它内部会自动调用 reactive 将其转化为 Proxy。

总结

Vue 3 响应式原理的核心是 Proxy

  1. Proxy 拦截对象操作。
  2. Getter 中进行 Track (依赖收集),建立数据与副作用函数的映射关系。
  3. Setter 中进行 Trigger (派发更新),执行副作用函数。
  4. 相比 Vue 2,它更强大(支持数组、新增属性)、更高效(懒代理)。
00:00
00:00