|
提到proxy,貌似很多人的印象是这东西是实现vue3的核心,但好像除此以外就没有什么关于proxy实际应用的场景了。今天就重新在了解一下proxy,并在文章最后会给出几个proxy的实际应用案例,扩展下对proxy使用的场景。让大家写出逼格更高,更有深度的代码。首先我们要先复习下proxy的基本知识。Proxy基础proxy是个啥?在JavaScript中,Proxy对象是ES6引入的一种机制,它允许你创建一个代理对象,用于拦截和定义基本操作的自定义行为。Proxy出现的主要原因包括:拦截和修改操作:Proxy允许你拦截并重定义对象上的基本操作,比如读取属性、设置属性、函数调用等。这使得你可以在这些操作发生前后插入自定义逻辑。数据绑定和观察:你可以使用Proxy监听对象属性的变化。当被代理对象的属性发生变化时,可以触发相关操作,这对于实现数据绑定和观察模式非常有用。安全性:Proxy可以用于创建安全的对象,限制对对象的访问和操作。你可以通过拦截器来验证用户的操作,以确保对象的安全性。元编程:Proxy提供了元编程的能力,即在运行时改变语言的行为。通过拦截器,你可以动态地修改对象的行为,这为实现更高级的编程模式提供了可能性。函数式编程:在函数式编程中,Proxy可以用于创建不可变(immutable)的数据结构,确保数据不被修改,从而避免副作用。总的来说,Proxy出现的主要原因是为了提供更灵活、可控制、可定制的对象操作和行为,使得开发者能够更好地掌握和管理代码的执行过程。为啥要有proxy在JavaScript中引入Proxy的历史原因主要是为了提供更灵活和可扩展的对象操作机制。在ES6之前,JavaScript语言中并没有原生的方式来实现对象的拦截和定制操作行为。开发者通常需要依赖对象的getter和setter方法,或者使用一些特定的命名约定来模拟拦截操作,但这些方法都有限制和局限性。随着应用程序变得越来越复杂,需要更多灵活性和可控性来处理对象的操作。因此,在ECMAScript6(ES6)标准中引入了Proxy,以提供一种通用的、标准化的机制,使开发者可以在对象上定义自定义的操作行为。这种机制的引入使得JavaScript的对象系统更加强大和灵活,为开发者提供了更多处理对象的方式,也为实现各种高级编程模式和设计模式提供了基础。因此,Proxy的引入主要是为了满足JavaScript编程语言在处理对象时的需求,提供了一种更现代、更强大的对象操作机制。Proxy的好兄弟ReflectProxy和Reflect是ES6中引入的两个相关的特性。这两者常常一起使用,因为Reflect提供了一套默认行为,这些行为与函数调用对应,与Proxy的handler对象能处理的各种相对应。Proxy和Reflect的交互对称性:ReflectAPI的设计目标之一是与Proxyhandlers的方法保持一致性。例如,Reflect.get(target,property,receiver)与get方法具有相同的参数。这使得我们在编写Proxy时,可以很方便地调用对应的Reflect方法来保留默认行为。默认行为:Proxy的方法可以让我们自定义基本操作,但有时我们想要修改某些行为的同时保留默认行为。这时,我们可以在Proxy内调用对应的Reflect方法。这样做不仅代码更简洁,而且Reflect的方法会处理原型链相关的细节。没有Reflect呢?如果没有Reflect,我们通常需要手动复制原有的行为,这可能导致代码冗长且容易出错。例如,如果你想在get操作前添加日志记录,没有Reflect你可能需要这样做:let proxy = new roxy(target, { get(target, property, receiver) { console.log(`Property ${property} has been read.`); return target[property]; // 如果属性在原型链上,这里就不够用了 }});如果property属性存在于原型链上,这种方法就会失败。而使用Reflect.get(),它会自动处理这些细节:let proxy = new roxy(target, { get(target, property, receiver) { console.log(`Property ${property} has been read.`); return Reflect.get(target, property, receiver); }});使用Reflect还可以确保返回值和异常的正确传递,因为Reflect的方法返回的是操作的状态(成功或者失败),这正好与Proxy的要求相符合。举几个使用案例模拟对象关系数据库// 模拟数据库const database = { users: [ { id: 1, name: 'Alice', age: 25 }, { id: 2, name: 'Bob', age: 30 }, // ...more users ], posts: [ { id: 1, title: 'Post 1', content: 'Content 1', userId: 1 }, { id: 2, title: 'Post 2', content: 'Content 2', userId: 2 }, // ...more posts ],};// 模拟ORM生成器const createORM = (tableName, primaryKey) => { return new roxy(database[tableName], { get(target, property) { if (property === 'findAll') { // 返回所有记录 return () => target; } if (property === 'findById') { // 根据主键查找记录 return (id) => target.find(item => item[primaryKey] === id); } if (property === 'findBy') { // 根据条件查找记录 return (condition) => target.filter(item => { for (const key in condition) { if (item[key] !== condition[key]) { return false; } } return true; }); } // 其他属性返回原始值 return target[property]; } });};// 使用ORM生成器创建User和Post对象const User = createORM('users', 'id');const ost = createORM('posts', 'id');// 使用ORM查询数据console.log(User.findAll()); // 返回所有用户console.log(User.findById(1)); // 返回id为1的用户console.log(User.findBy({ age: 30 })); // 返回年龄为30的用户console.log(Post.findAll()); // 返回所有帖子console.log(Post.findBy({ userId: 1 })); // 返回userId为1的帖子首先,我们有一个名为database的模拟数据库,其中包含两个表:users和posts。每个表都有一些示例记录,包括用户信息和帖子信息。然后,我们有一个createORM函数,它是一个ORM生成器。这个函数接受两个参数:tableName表示表的名称,primaryKey表示主键的名称。它返回了一个代理对象,这个代理对象通过Proxy对象对数据库中的表进行了包装。这个代理对象中的get方法用于捕获对对象属性的访问。在这个方法中,我们检查了被访问的属性是否是findAll、findById或findBy。如果是其中之一,它们分别返回了对应的函数:findAll返回指定表中的所有记录。findById根据指定的主键值返回对应的记录。findBy根据指定的条件返回符合条件的记录。除了以上三个特殊属性外,对于其他属性,代理对象会直接返回数据库中对应表的属性值。接着,我们使用createORM函数创建了User和Post对象,分别对应于users表和posts表。最后,我们使用这些生成的对象执行了一些查询操作。例如,我们调用User.findAll()返回了所有用户的信息,调用Post.findBy({userId:1})返回了所有userId为1的帖子的信息。表单验证器const Validator = (rules) => { return new roxy({}, { set(target, property, value) { const rule = rules[property]; if (rule) { // 验证规则存在 for (const validation of rule) { const { type, message, condition } = validation; // 使用Reflect进行验证 const isValid = Reflect[type](value, condition); if (!isValid) { console.error(`Validation failed for ${property}: ${message}`); return false; } } } // 符合规则,设置值 target[property] = value; return true; } });};// 定义表单验证规则const formRules = { username: [ { type: 'isString', message: 'Username must be a string', condition: {} }, { type: 'isLength', message: 'Username must be between 5 and 15 characters', condition: { min: 5, max: 15 } }, ], password: [ { type: 'isString', message: 'Password must be a string', condition: {} }, { type: 'isLength', message: 'Password must be at least 8 characters', condition: { min: 8 } }, { type: 'matches', message: 'Password must contain at least one uppercase letter', condition: /[A-Z]/ }, ], email: [ { type: 'isString', message: 'Email must be a string', condition: {} }, { type: 'isEmail', message: 'Invalid email format', condition: {} }, ],};// 使用表单验证器const formValidator = Validator(formRules);// 模拟表单数据const formData = { username: 'john_doe', password: 'SecurePass123', email: 'john.doe@example.com',};// 验证表单数据for (const field in formData) { formValidator[field] = formData[field];}// 表单验证结果console.log(formValidator);首先,我们有一个名为Validator的函数,它接受一个规则对象作为参数,并返回了一个代理对象。这个代理对象用于捕获对属性的赋值操作,即在设置属性值时进行验证。在代理对象的set方法中,我们首先检查给定属性的验证规则是否存在。如果存在规则,我们就遍历这些规则,并对属性值进行验证。验证规则包括type(验证函数名称)、message(验证失败时的错误消息)和condition(验证条件)。接着,我们使用Reflect[type]来调用相应的内置验证函数,比如isString、isLength、matches、isEmail等。如果验证失败,我们会输出相应的错误消息,并阻止属性值的设置。如果所有的验证规则都通过了,我们就将属性值设置到目标对象中,并返回true,表示设置成功。接下来,我们定义了一个formRules对象,其中包含了对表单字段的验证规则。每个字段都有一个对应的验证规则数组。然后,我们使用Validator函数并传入formRules来创建了一个表单验证器formValidator。接着,我们定义了一个模拟的表单数据对象formData,其中包含了要验证的字段和对应的值。然后,我们遍历formData中的每个字段,并将其值赋给formValidator对象中的相应属性。这会触发代理对象的set方法进行验证。最后,我们输出了经过验证后的formValidator对象,其中包含了验证通过的表单数据。日志// 创建一个目标对象const targetObject = { value: 42 };// 创建一个日志对象const logger = new Logger();// 创建一个 roxy,用于记录日志const logProxy = new roxy(targetObject, { get(target, prop) { logger.log(`Getting property ${prop}`); return target[prop]; }, set(target, prop, value) { logger.log(`Setting property ${prop} to ${value}`); target[prop] = value; return true; }, deleteProperty(target, prop) { logger.warn(`Deleting property ${prop}`); delete target[prop]; return true; }, apply(target, thisArg, args) { logger.log(`Applying function ${target.name || 'anonymous'}`); return target.apply(thisArg, args); }, construct(target, args) { logger.log(`Constructing object with ${target.name || 'anonymous'} constructor`); return new target(...args); },});// 使用 roxy 访问目标对象logProxy.value; // 获取属性,触发日志logProxy.value = 100; // 设置属性,触发日志delete logProxy.value; // 删除属性,触发日志首先,我们有一个名为targetObject的目标对象,其中包含一个属性value,其初始值为42。然后,我们创建了一个Logger类的实例,用于记录日志。该Logger类在代码中没有完全显示,但我们可以假设它包含了一些日志记录的方法,如log、warn。接着,我们使用Proxy构造函数创建了一个名为logProxy的代理对象。这个代理对象包含了一系列处理器(handler),用于捕获对目标对象的不同操作,比如get、set、deleteProperty、apply、construct。在get处理器中,每当获取目标对象的属性时,会触发日志记录,指示正在获取哪个属性。在set处理器中,每当设置目标对象的属性时,会触发日志记录,指示正在设置哪个属性以及设置的值。在deleteProperty处理器中,每当删除目标对象的属性时,会触发日志记录,指示正在删除哪个属性。在apply处理器中,每当对目标对象进行函数调用时,会触发日志记录,指示正在调用哪个函数。在construct处理器中,每当使用new操作符创建对象时,会触发日志记录,指示正在构造哪个对象。最后,我们使用logProxy对象进行了一系列操作,包括获取属性、设置属性、删除属性。每次操作都触发了相应的日志记录,以便跟踪对象的行为。缓存const { promisify } = require('util');const redis = require('redis');const client = redis.createClient();// 模拟数据库查询async function queryDatabase(query) { // 在实际应用中,这里会是真实的数据库查询操作 console.log(`Executing database query: ${query}`); return `Result for query: ${query}`;}// 使用 roxy 创建一个带缓存的数据库查询代理const cachedDatabaseQuery = new roxy(queryDatabase, { async apply(target, thisArg, argumentsList) { const query = argumentsList[0]; const cacheKey = `cache{query}`; // 尝试从缓存中获取结果 const cachedResult = await promisify(client.get).bind(client)(cacheKey); if (cachedResult) { console.log(`Cache hit! Returning cached result for query: ${query}`); return cachedResult; } // 缓存中没有结果,执行数据库查询 const result = await target(...argumentsList); // 将查询结果存入缓存 await promisify(client.set).bind(client)(cacheKey, result); console.log(`Database query result stored in cache for query: ${query}`); return result; },});// 测试代理async function testProxy() { const result1 = await cachedDatabaseQuery('SELECT * FROM users WHERE id = 1'); const result2 = await cachedDatabaseQuery('SELECT * FROM users WHERE id = 2'); const result3 = await cachedDatabaseQuery('SELECT * FROM users WHERE id = 1'); console.log(result1); console.log(result2); console.log(result3);}testProxy();首先,我们引入了util模块中的promisify函数和redis模块,然后创建了一个Redis客户端client。接着,我们定义了一个模拟数据库查询的异步函数queryDatabase,在实际应用中,这里会是真实的数据库查询操作。这个函数接受一个查询字符串作为参数,打印出执行的查询,并返回一个包含查询结果的字符串。然后,我们使用Proxy创建了一个名为cachedDatabaseQuery的代理对象。这个代理对象用于包装queryDatabase函数,实现了带缓存的数据库查询功能。在代理对象的apply处理器中,我们首先从Redis缓存中尝试获取查询结果。如果缓存中有结果,则直接返回缓存的结果,并打印相应的日志。如果缓存中没有结果,则调用原始的queryDatabase函数执行数据库查询,并将结果存入缓存中,并打印相应的日志。最后,我们定义了一个名为testProxy的异步函数,用于测试代理对象。在这个函数中,我们多次调用cachedDatabaseQuery函数执行数据库查询,并输出查询结果。看看平常可以用的上面的讲的都是比较高大上的东西,其实很多小功能点也可以利用proxy来优化代码。如果你需要一个对照的MapObject,用来映射server传递来的一些特殊值,但server有可能传过来一个null或者空怎么办呢?const MAP = { a: '342412', b: 'qwerasd'}if(!serverKey) { console.log('xxxx')} else { console.log(MAP[serverKey])}可以写个if但是如果判断逻辑哪的代码又多又杂,或者是需求让你写出越来越多的特殊情况,那if就显得不那么优雅了,我们这个时候就可以用proxy来解耦const createObjMapProxy = (obj) => { return new Proxy(obj, { get: (target, propKey: string, receiver) => { const keys = **Object**.keys(target) if (keys.includes(propKey)) { return Reflect.get(target, propKey, receiver) } else { // 将判断逻辑或其他逻辑放到这里 if(xxx) { return x } else { return xx } } } })}const orderTypeProxy = createObjMapProxy({ a: '342412', b: 'qwerasd'})创建一个createObjMapProxy方法传入一个对象,这个方法返回一个Proxy对象,用入参的对象初始化一个proxy对象并监听对象的get行为,并在get行为中构建逻辑。这时我们就将使用的功能和构建逻辑的功能彻底分开了,createObjMapProxy方法随便丢到别的文件中去引用出来,也能有效降低单文件代码量。重复包装Proxyconst targetObject = { value: 42 };// 第一个 Proxyconst firstProxy = new Proxy(targetObject, { get(target, prop) { console.log(`Getting property ${prop} via the first Proxy`); return target[prop]; }, set(target, prop, value) { console.log(`Setting property ${prop} via the first Proxy`); target[prop] = value; return true; },});// 第二个 Proxyconst secondProxy = new Proxy(firstProxy, { get(target, prop) { console.log(`Getting property ${prop} via the second Proxy`); return target[prop]; }, set(target, prop, value) { console.log(`Setting property ${prop} via the second Proxy`); target[prop] = value; return true; },});// 使用第二个 Proxyconsole.log(secondProxy.value); // 通过第二个 Proxy 获取属性secondProxy.value = 100; // 通过第二个 Proxy 设置属性console.log(secondProxy.value); // 通过第二个 Proxy 获取更新后的属性每个Proxy可以只做一个独立的事情然后一层一层的包给属性,进一步解耦代码。总结proxy的优点灵活性和可扩展性:Proxy提供了强大的拦截和定制能力,使开发者能够在对象的访问、修改和删除等操作上进行细粒度的控制,从而实现各种高级功能。代码可读性和维护性:使用Proxy可以将对象的行为和结构分离开,使代码更加清晰、可读,并且更容易维护。通用性:由于Proxy可以拦截多种操作,因此可以创建通用的功能,比如数据绑定、表单验证等。无侵入性:Proxy可以在不修改原始对象的情况下实现功能,使得代码更加模块化和可复用。proxy的缺陷性能:相对于直接访问对象的操作,Proxy的性能会略逊一筹。如果性能是关键问题,而且不需要Proxy提供的特殊功能,可以选择传统的操作方式。兼容性:一些较老版本的浏览器可能不支持Proxy,因此在考虑兼容性时需要注意。学习成本:对于新手来说,掌握Proxy的使用可能需要一些时间,因为它提供了丰富的特性和选项。不可逆性:一旦使用Proxy修改了对象的行为,有可能导致代码的不可逆转。这也意味着在使用Proxy时需要谨慎,确保了解其对对象的影响。const user = { name: 'John', age: 30,};const loggedUser = new Proxy(user, { get(target, key) { console.log(`Getting ${key}`); return target[key]; },});console.log(loggedUser.name); // 输出 "Getting name"console.log(loggedUser.age); // 输出 "Getting age"这种行为是可逆的,我们可以选择不使用这个proxy但如果是这样const securedUser = new Proxy(user, { set(target, key, value) { if (key === 'age' & typeof value !== 'number') { throw new Error('Age must be a number'); } target[key] = value; return true; },});securedUser.age = 'thirty'; // 抛出错误:Agemustbeanumber如果已经设置成了这样我们使用securedUser.age时只要不是number类型他就会一直报错如果是正在写代码没有问题,但如果别人已经写好了现在你要改代码,哪你就得小心了。这样的修改是不可逆的,因为一旦我们使用了这个Proxy,就无法回到没有这个检查的状态。如果后续发现这个检查有问题,需要去掉或者修改,就需要谨慎操作,以免影响到代码的其他部分。这就是"不可逆性"的一个例子。文章到此其实大家也能从上述的例子中发现一些共性,proxy的本质其实就是为对象提供了一层中间层,让我们在操作对象的时候同时触发一些事情,也就是说如果未来我们的场景需要着重对操作对象这件事做很多事情的时候,那么proxy就会成为一个很好的方案。
|
|