详细讲解VUE3中proxy是怎么劫持数据的

119 阅读7分钟

在 Vue 3 中,数据劫持主要通过 Proxy 对象来实现。Proxy 提供了一种更强大和灵活的方式来拦截和操作对象的操作。以下是 Vue 3 中如何使用 Proxy 劫持数据的详细说明:

1. 创建响应式对象

Vue 3 使用 reactive 函数来创建响应式对象。reactive 函数内部会使用 Proxy 来劫持对象的属性。

import { reactive } from 'vue';

const state = reactive({
  count: 0
});

2. 使用 Proxy 劫持对象

reactive 函数内部会创建一个 Proxy 对象,并定义 get 和 set 陷阱来拦截对对象属性的访问和修改。

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      // 收集依赖
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      // 触发更新
      trigger(target, key);
      return result;
    }
  });
}

3. 收集依赖

在 get 陷阱中,Vue 会收集当前正在访问属性的依赖(即哪些响应式函数依赖于该属性)。这通常通过一个全局的依赖收集器来实现。

const targetMap = new WeakMap();

function track(target, key) {
  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);
}

4. 触发更新

在 set 陷阱中,Vue 会触发所有依赖于该属性的响应式函数重新执行。

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effect());
  }
}

5. 响应式函数

响应式函数(如 watchEffect 或 computed)会在执行时收集依赖,并在依赖的属性发生变化时重新执行。

import { watchEffect } from 'vue';

watchEffect(() => {
  console.log(state.count);
});

完整示例

以下是一个完整的示例,展示了 Vue 3 中如何使用 Proxy 劫持数据并实现响应式系统。

const targetMap = new WeakMap();
let activeEffect = null;

function effect(fn) {
  activeEffect = fn;
  fn();
  activeEffect = null;
}

function track(target, key) {
  if (!activeEffect) return;
  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);
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (dep) {
    dep.forEach(effect => effect());
  }
}

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return result;
    }
  });
}

const state = reactive({
  count: 0
});

effect(() => {
  console.log(state.count);
});

state.count++; // 输出: 1
state.count++; // 输出: 2

总结

Vue 3 使用 Proxy 对象来劫持数据,通过 get 和 set 陷阱拦截对对象属性的访问和修改。在 get 陷阱中收集依赖,在 set 陷阱中触发更新。这样可以实现高效的响应式系统,确保数据变化时视图能够自动更新。

WeakMap

WeakMap 是 JavaScript 中的一种内置对象,它允许你存储键值对,其中键必须是对象(或 null),而值可以是任意类型。WeakMap 的键是弱引用,这意味着如果一个对象作为键在 WeakMap 中不再被其他引用持有,那么这个对象可以被垃圾回收器回收。这使得 WeakMap 在处理内存管理时非常有用,因为它不会阻止垃圾回收。

主要特点

  1. 键必须是对象:

    • WeakMap 的键必须是对象或 null。尝试使用非对象作为键会抛出 TypeError
  2. 弱引用:

    • WeakMap 的键是弱引用,这意味着如果一个对象作为键在 WeakMap 中不再被其他引用持有,它可以被垃圾回收器回收。
  3. 不可枚举:

    • WeakMap 的键是不可枚举的,因此不能通过迭代来获取所有的键。
  4. 有限的方法:

    • WeakMap 提供的方法有限,主要包括 setgethas 和 delete

常用方法

  • set(key, value) :

    • 设置 key 对应的值为 value
  • get(key) :

    • 返回 key 对应的值,如果 key 不存在则返回 undefined
  • has(key) :

    • 检查 WeakMap 中是否存在 key,返回布尔值。
  • delete(key) :

    • 删除 key 及其对应的值,返回布尔值表示是否删除成功。

由于 WeakMap 是 JavaScript 引擎的内置对象,其具体源码实现通常位于 JavaScript 引擎的内部。以下是一个简化的概念性实现,帮助理解 WeakMap 的工作原理:

class SimplifiedWeakMap {
  constructor() {
    this._map = new Map();
  }

  set(key, value) {
    if (typeof key !== 'object' && key !== null) {
      throw new TypeError('Invalid value used as weak map key');
    }
    this._map.set(key, value);
  }

  get(key) {
    if (typeof key !== 'object' && key !== null) {
      throw new TypeError('Invalid value used as weak map key');
    }
    return this._map.get(key);
  }

  has(key) {
    if (typeof key !== 'object' && key !== null) {
      throw new TypeError('Invalid value used as weak map key');
    }
    return this._map.has(key);
  }

  delete(key) {
    if (typeof key !== 'object' && key !== null) {
      throw new TypeError('Invalid value used as weak map key');
    }
    return this._map.delete(key);
  }
}

注意事项

  1. 键必须是对象:

    • WeakMap 的键必须是对象或 null。尝试使用非对象作为键会抛出 TypeError
  2. 不可枚举:

    • WeakMap 的键是不可枚举的,因此不能通过迭代来获取所有的键。
  3. 垃圾回收:

    • WeakMap 的键是弱引用,这意味着键对象不会阻止垃圾回收。这有助于保持内存的高效管理。

总结

WeakMap 是 JavaScript 中一种强大的内置对象,用于存储键值对,其中键必须是对象(或 null),而值可以是任意类型。WeakMap 的键是弱引用,这意味着键对象不会阻止垃圾回收。其内部实现依赖于 JavaScript 引擎的垃圾回收机制,确保键对象可以被安全地回收。通过 setgethas 和 delete 方法,WeakMap 提供了高效且安全的键值对存储方式。

Map

Map 的内部实现通常依赖于哈希表(hash table)来高效地存储和检索键值对。以下是一些关键实现细节:

  1. 哈希表:

    • Map 使用哈希表来存储键值对,提供高效的查找、插入和删除操作。
  2. 键的唯一性:

    • 每个键在 Map 中是唯一的。如果使用相同的键设置不同的值,旧值会被覆盖。
  3. 保持插入顺序:

    • Map 保持键值对的插入顺序,这意味着迭代时会按照插入的顺序进行。
  4. 键的类型:

    • Map 的键可以是任何类型,包括对象、原始值等。这与普通对象不同,普通对象的键只能是字符串或 Symbol

简化版 Map 实现

以下是一个简化的概念性实现,帮助理解 Map 的工作原理:

class SimplifiedMap {
  constructor() {
    this._items = [];
  }

  set(key, value) {
    const index = this._items.findIndex(item => item.key === key);
    if (index !== -1) {
      this._items[index].value = value;
    } else {
      this._items.push({ key, value });
    }
    return this;
  }

  get(key) {
    const item = this._items.find(item => item.key === key);
    return item ? item.value : undefined;
  }

  has(key) {
    return this._items.some(item => item.key === key);
  }

  delete(key) {
    const index = this._items.findIndex(item => item.key === key);
    if (index !== -1) {
      this._items.splice(index, 1);
      return true;
    }
    return false;
  }

  clear() {
    this._items = [];
  }

  get size() {
    return this._items.length;
  }

  keys() {
    return this._items.map(item => item.key);
  }

  values() {
    return this._items.map(item => item.value);
  }

  entries() {
    return this._items.map(item => [item.key, item.value]);
  }

  forEach(callbackFn, thisArg) {
    this._items.forEach(item => {
      callbackFn.call(thisArg, item.value, item.key, this);
    });
  }
}

// 示例使用
const map = new SimplifiedMap();
map.set('name', 'Alice');
map.set(1, 'one');
map.set({ id: 1 }, 'objectKey');

console.log(map.get('name')); // 输出: Alice
console.log(map.has(1)); // 输出: true
map.delete('name');
console.log(map.size); // 输出: 2
map.clear();
console.log(map.size); // 输出: 0

注意事项

  1. 键的唯一性:

    • 每个键在 Map 中是唯一的。如果使用相同的键设置不同的值,旧值会被覆盖。
  2. 保持插入顺序:

    • Map 保持键值对的插入顺序,这意味着迭代时会按照插入的顺序进行。
  3. 键的类型:

    • Map 的键可以是任何类型,包括对象、原始值等。
  4. 性能:

    • Map 提供高效的查找、插入和删除操作,适合需要频繁操作键值对的场景。

在 Vue 3 的响应式系统中,track 和 trigger 是两个核心函数,用于收集依赖和触发更新。它们共同实现了数据变化时视图能够自动更新的机制。下面是 track 和 trigger 的原理和作用的详细解释。

1. track 函数

原理
  • 收集依赖:

    • track 函数用于在访问响应式对象的属性时,记录哪些响应式函数(如 watchEffect 或 computed)依赖于该属性。
    • 这通常通过一个全局的依赖收集器来实现,该收集器会记录当前正在执行的响应式函数及其依赖的属性。
作用
  • 依赖收集:

    • 当一个响应式对象的属性被访问时,track 会将当前的响应式函数(activeEffect)与该属性关联起来。
    • 这样,当属性发生变化时,可以知道哪些响应式函数需要重新执行。
实现示例
const targetMap = new WeakMap();
let activeEffect = null;

function track(target, key) {
  if (!activeEffect) return;
  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);
}

2. trigger 函数

原理
  • 触发更新:

    • trigger 函数用于在修改响应式对象的属性时,通知所有依赖于该属性的响应式函数重新执行。
    • 这通常通过从全局依赖收集器中获取所有依赖于该属性的响应式函数,并依次执行它们来实现。
作用
  • 更新视图:

    • 当一个响应式对象的属性被修改时,trigger 会找到所有依赖于该属性的响应式函数,并重新执行这些函数。
    • 这样,视图会根据最新的数据重新渲染。

以下是 Vue 3 中 trigger 函数的简化实现:

const targetMap = new WeakMap();

function trigger(target, key) {
  // 获取与 target 对象关联的依赖映射
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return; // 没有依赖,直接返回
  }

  // 获取与 key 属性关联的依赖集合
  const dep = depsMap.get(key);
  if (!dep) {
    return; // 没有依赖,直接返回
  }

  // 遍历依赖集合,执行所有响应式函数
  dep.forEach(effect => {
    effect();
  });
}