阐述 JavaScript Proxy 对象在实现数据响应式 (如 Vue 3) 或模拟对象行为 (如 Mocking) 中的高级应用。

大家好,我是老码,今天咱们聊聊 JavaScript Proxy,这玩意儿可不只是个摆设,玩好了能让你的代码飞起来。咱们主要讲讲它在数据响应式和模拟对象行为这两大领域的骚操作,尤其是看看 Vue 3 怎么用它实现响应式,以及怎么用它做 Mocking。

开场白:Proxy 是个啥玩意儿?

简单来说,Proxy 就是一个“代理人”,它可以拦截并控制对另一个对象的各种操作。你可以把它想象成一个门卫,你想进屋(访问对象),得先过他这关。他可以让你进,也可以不让你进,甚至可以篡改你进屋后看到的东西。

第一部分:Proxy 的基本用法

先来点基础知识热热身,别一下子就晕了。

// 目标对象
const target = {
  name: '老码',
  age: 30
};

// 处理函数 (handler)
const handler = {
  get: function(target, property, receiver) {
    console.log(`有人想访问 ${property} 属性`);
    return Reflect.get(target, property, receiver); // 必须返回,否则访问不到
  },
  set: function(target, property, value, receiver) {
    console.log(`有人想修改 ${property} 属性为 ${value}`);
    target[property] = value;
    return true; // set 必须返回 true,表示设置成功
  }
};

// 创建 Proxy 实例
const proxy = new Proxy(target, handler);

// 访问属性
console.log(proxy.name); // 输出: 有人想访问 name 属性  老码

// 修改属性
proxy.age = 35; // 输出: 有人想修改 age 属性为 35
console.log(target.age); // 输出: 35

这段代码创建了一个 Proxy,拦截了对 target 对象的 getset 操作。每次访问或修改属性,都会先执行 handler 里的对应函数。

Handler 函数都有哪些?

Proxy 的 handler 可以拦截很多操作,常用的包括:

  • get(target, property, receiver): 拦截读取属性的操作。
  • set(target, property, value, receiver): 拦截设置属性的操作。
  • has(target, property): 拦截 in 操作符。 例如:'name' in proxy
  • deleteProperty(target, property): 拦截 delete 操作。 例如:delete proxy.age
  • ownKeys(target): 拦截 Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)
  • getOwnPropertyDescriptor(target, property): 拦截 Object.getOwnPropertyDescriptor(proxy, property)
  • defineProperty(target, property, descriptor): 拦截 Object.defineProperty(proxy, property, descriptor)
  • preventExtensions(target): 拦截 Object.preventExtensions(proxy)
  • getPrototypeOf(target): 拦截 Object.getPrototypeOf(proxy)
  • setPrototypeOf(target, prototype): 拦截 Object.setPrototypeOf(proxy, prototype)
  • apply(target, thisArg, argumentsList): 拦截函数调用。只有当目标对象是函数时才有效。
  • construct(target, argumentsList, newTarget): 拦截 new 操作符。只有当目标对象是构造函数时才有效。

每个 handler 函数都有自己的参数,但第一个参数永远是 target,表示目标对象。receiver 通常是 Proxy 实例本身,用于处理继承的情况。

Reflect 是个啥?

在 handler 函数里,我们经常看到 Reflect.get(target, property, receiver) 这样的代码。 Reflect 是一个内置对象,提供了一组与对象操作相关的静态方法,这些方法与 Proxy handler 的方法一一对应。

使用 Reflect 的好处是:

  • 默认行为: Reflect 提供了默认的对象操作行为,比如 Reflect.get 就是获取属性的默认行为。
  • 错误处理: 如果对象操作失败,Reflect 会抛出错误,而不是默默地失败。
  • this 指向: Reflect 会正确处理 this 指向,避免一些意想不到的问题。

第二部分:Proxy 在数据响应式中的应用 (Vue 3 为例)

Vue 3 抛弃了 Vue 2 的 Object.defineProperty,拥抱了 Proxy。这是为什么呢?

特性 Object.defineProperty Proxy
监听对象 只能监听对象的属性 可以直接监听整个对象
监听数组 需要特殊处理 可以直接监听数组
监听新增/删除属性 无法直接监听 可以直接监听
性能 性能较差 性能更好,尤其是在大型对象上

简单来说,Proxy 完胜。

Vue 3 的响应式原理

Vue 3 的响应式系统主要依赖两个函数: reactive()ref()reactive() 用于创建响应式对象, ref() 用于创建响应式基本类型数据。 它们都使用了 Proxy。

我们来模拟一下 reactive() 的实现:

function reactive(target) {
  if (typeof target !== 'object' || target === null) {
    return target; // 不是对象,直接返回
  }

  const handler = {
    get(target, key, receiver) {
      track(target, key); // 收集依赖
      return Reflect.get(target, key, receiver);
    },
    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;
    }
  };

  return new Proxy(target, handler);
}

// 简化的依赖收集和触发更新函数 (实际 Vue 3 中更复杂)
const targetMap = new WeakMap();
let activeEffect = null;

function effect(fn) {
  activeEffect = fn;
  fn(); // 立即执行一次,收集依赖
  activeEffect = null;
}

function track(target, key) {
  if (activeEffect) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      depsMap = new Map();
      targetMap.set(target, depsMap);
    }

    let deps = depsMap.get(key);
    if (!deps) {
      deps = new Set();
      depsMap.set(key, deps);
    }

    deps.add(activeEffect);
  }
}

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

  const deps = depsMap.get(key);
  if (deps) {
    deps.forEach(effect => {
      effect();
    });
  }
}

// 示例
const data = reactive({ count: 0 });

effect(() => {
  console.log('count is:', data.count);
});

data.count++; // 输出: count is: 1
data.count = 10; // 输出: count is: 10

这段代码模拟了 Vue 3 的响应式核心。 reactive() 函数将普通对象转换为响应式对象,每次访问属性时, track() 函数会收集依赖;每次修改属性时, trigger() 函数会触发更新。 effect() 函数用于注册副作用函数,当依赖的数据发生变化时,副作用函数会自动执行。

响应式数组

Proxy 也能轻松处理数组的响应式。Vue 3 对数组的一些特殊方法(如 push, pop, shift, unshift, splice, sort, reverse)进行了特殊处理,以便在修改数组时触发更新。

const originalArrayProto = Array.prototype;
const arrayMethods = Object.create(originalArrayProto);

['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
  arrayMethods[method] = function(...args) {
    const result = originalArrayProto[method].apply(this, args);
    // 触发更新
    trigger(this, 'length'); // 数组长度可能发生变化
    return result;
  };
});

function reactiveArray(arr) {
    Object.setPrototypeOf(arr, arrayMethods); //修改数组的原型链
    return arr
}

const myArray = reactiveArray([1, 2, 3]);

effect(() => {
  console.log('array is:', myArray.join(','));
});

myArray.push(4); // 输出: array is: 1,2,3,4
myArray.splice(1, 1); // 输出: array is: 1,3,4

这段代码通过修改数组的原型链,拦截了数组的修改方法,并在修改后触发更新。

第三部分:Proxy 在 Mocking 中的应用

在单元测试中,我们经常需要 Mock 外部依赖,比如 API 请求、数据库操作等。Proxy 可以很方便地创建 Mock 对象,模拟这些外部依赖的行为。

Mocking API 请求

假设我们有一个函数,用于获取用户数据:

async function getUserData(userId) {
  const response = await fetch(`/api/users/${userId}`);
  const data = await response.json();
  return data;
}

在单元测试中,我们不想真正发送 API 请求,而是希望 Mock fetch 函数,返回预定义的数据。

// Mock fetch 函数
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ id: 1, name: 'Mock User' })
  })
);

// 测试 getUserData 函数
test('getUserData should return mock user data', async () => {
  const userData = await getUserData(1);
  expect(userData).toEqual({ id: 1, name: 'Mock User' });
  expect(fetch).toHaveBeenCalledWith('/api/users/1');
});

这段代码使用 jest.fn() 创建了一个 Mock fetch 函数,并用 toHaveBeenCalledWith() 验证了 fetch 函数是否被正确调用。

使用 Proxy 创建更灵活的 Mock 对象

有时候,我们需要根据不同的参数,返回不同的 Mock 数据。Proxy 可以更灵活地实现这一点。

const mockData = {
  1: { id: 1, name: 'User 1' },
  2: { id: 2, name: 'User 2' }
};

const mockFetch = new Proxy({}, {
  get(target, property) {
    return () => Promise.resolve({
      json: () => Promise.resolve(mockData[property])
    });
  }
});

global.fetch = mockFetch;

test('getUserData should return different data for different user IDs', async () => {
  const userData1 = await getUserData(1);
  const userData2 = await getUserData(2);

  expect(userData1).toEqual({ id: 1, name: 'User 1' });
  expect(userData2).toEqual({ id: 2, name: 'User 2' });
});

这段代码创建了一个 Proxy,拦截了对 fetch 对象的属性访问。当访问 fetch[userId] 时,Proxy 会返回一个函数,该函数返回对应 userId 的 Mock 数据。

Mocking 复杂的对象

有时候,我们需要 Mock 的对象非常复杂,包含很多属性和方法。手动创建 Mock 对象会非常繁琐。Proxy 可以帮助我们动态地创建 Mock 对象,只 Mock 我们需要的部分。

const realObject = {
  method1: () => 'real method1',
  method2: () => 'real method2',
  method3: () => 'real method3',
  property1: 'real property1',
  property2: 'real property2'
};

const mockedMethods = ['method1', 'method2'];

const mockObject = new Proxy(realObject, {
  get(target, property) {
    if (mockedMethods.includes(property)) {
      return jest.fn(() => `mocked ${property}`);
    }
    return Reflect.get(target, property);
  }
});

test('mockObject should mock specified methods', () => {
  expect(mockObject.method1()).toBe('mocked method1');
  expect(mockObject.method2()).toBe('mocked method2');
  expect(mockObject.method3()).toBe('real method3'); // 未被 mock,返回原始值
  expect(mockObject.property1).toBe('real property1'); // 未被 mock,返回原始值
});

这段代码创建了一个 Proxy,只 Mock 了 method1method2 方法,其他属性和方法都保持不变。

第四部分:Proxy 的高级应用

Proxy 还有一些更高级的应用,比如:

  • 数据校验: 在 set handler 中对数据进行校验,防止非法数据写入。
  • 权限控制: 根据用户权限,控制对对象的访问。
  • 日志记录: 在 getset handler 中记录日志,方便调试。
  • 性能优化: 在 get handler 中进行缓存,避免重复计算。

数据校验示例

const validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('Age is not an integer');
      }
      if (value < 0 || value > 150) {
        throw new RangeError('Age is invalid');
      }
    }

    // The default behavior to store the value
    obj[prop] = value;

    // Indicate success
    return true;
  }
};

const person = new Proxy({}, validator);

person.age = 100;
console.log(person.age); // 100

person.age = 'young'; // 抛出 TypeError: Age is not an integer
person.age = 200; // 抛出 RangeError: Age is invalid

权限控制示例 (简化版)

const hasPermission = (user, action, target) => {
    // 假设这里根据用户角色和权限列表进行判断
    if (user === 'admin' && action === 'delete') {
        return true;
    }
    return false;
}

const protectedData = {
    sensitiveInfo: 'Top Secret',
    publicInfo: 'Open to all'
}

const user = 'guest';

const protectedProxy = new Proxy(protectedData, {
    get(target, property) {
        return target[property];
    },
    set(target, property, value) {
        throw new Error('No permission to modify');
    },
    deleteProperty(target, property) {
        if (hasPermission(user, 'delete', target)) {
            delete target[property];
            return true;
        } else {
            throw new Error('No permission to delete');
        }
    }
});

console.log(protectedProxy.publicInfo); // Open to all
// protectedProxy.sensitiveInfo = 'New Value'; // 抛出 Error: No permission to modify
// delete protectedProxy.sensitiveInfo; // 抛出 Error: No permission to delete

const admin = 'admin';
user = admin; //切换到管理员身份
delete protectedProxy.sensitiveInfo; // 成功删除

总结

Proxy 是一个强大的工具,可以拦截和控制对对象的各种操作。它在数据响应式、Mocking 等领域都有广泛的应用。掌握 Proxy 的用法,可以让你写出更灵活、更可维护的代码。希望今天的讲座能帮助你更好地理解和应用 Proxy。

课后作业

  1. 实现一个 deepReactive() 函数,可以递归地将对象及其嵌套对象转换为响应式对象。
  2. 使用 Proxy 实现一个简单的缓存系统,当访问对象的属性时,如果属性已经存在于缓存中,则直接返回缓存中的值,否则计算属性的值并将其缓存起来。

好了,今天的课就到这里。下课!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注