面试题答案
一键面试性能问题原因分析
- 额外函数调用开销:每次使用装饰器,都会创建新的函数包裹原函数,增加了函数调用栈的深度和额外的函数调用开销。在大规模应用中,频繁调用这些被装饰函数会导致性能下降。
- 元数据处理开销:装饰器常与元数据操作相关,如使用
Reflect
API 读取和设置元数据。频繁的元数据操作,尤其是在循环或高频调用场景下,会带来额外的性能开销。 - 代码体积增大:大量装饰器的使用会使代码体积膨胀,增加了打包和加载时间,特别是在初始加载阶段,对应用的启动性能产生影响。
设计和实现层面优化
- 减少不必要的装饰器使用:仔细评估业务场景,仅在真正需要 AOP 功能的地方使用装饰器,避免过度使用。
- 缓存元数据:对于需要频繁读取的元数据,进行缓存处理。例如,在第一次读取元数据后,将其存储在一个变量中,后续直接使用缓存数据,减少
Reflect
API 的调用次数。 - 合并装饰器:如果多个装饰器执行类似功能,考虑将它们合并为一个装饰器,减少函数嵌套层数和调用开销。
- 懒加载装饰器:对于一些不急需执行的装饰器逻辑,采用懒加载方式,即在真正需要时才执行装饰器逻辑,而不是在模块加载时就执行。
优化后的 AOP 装饰器示例(日志记录场景)
假设我们有一个服务类,其中的方法需要记录调用日志。
// 缓存元数据存储
const methodCallLogMetadataKey = Symbol('methodCallLog');
const methodCallLogCache: Map<Function, string[]> = new Map();
function logMethodCall(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
// 从缓存中获取日志记录
let logs = methodCallLogCache.get(originalMethod);
if (!logs) {
logs = [];
methodCallLogCache.set(originalMethod, logs);
}
descriptor.value = function (...args: any[]) {
const startTime = new Date();
console.log(`Calling method ${propertyKey} at ${startTime}`);
const result = originalMethod.apply(this, args);
const endTime = new Date();
console.log(`Method ${propertyKey} finished at ${endTime}, execution time: ${endTime - startTime}ms`);
logs.push(`Call at ${startTime}, execution time: ${endTime - startTime}ms`);
return result;
};
// 提供获取日志记录的方法
target.getMethodCallLogs = function () {
return logs;
};
return descriptor;
}
class UserService {
@logMethodCall
getUserById(id: number) {
// 模拟业务逻辑
return { id, name: 'User' + id };
}
}
const userService = new UserService();
const user = userService.getUserById(1);
console.log(user);
const logs = userService.getMethodCallLogs();
console.log(logs);
在上述示例中,通过缓存日志记录,减少了每次调用时不必要的重复操作,同时展示了如何在具体业务场景(日志记录)中优化 AOP 装饰器的实现。