MST
星途 面试题库

面试题:TypeScript属性装饰器的性能优化与应用场景拓展

在大型前端项目中使用TypeScript属性装饰器实现属性监听与验证时,可能会遇到性能问题。请分析可能出现性能问题的点,并阐述如何进行优化。同时,拓展阐述除了属性监听与验证外,属性装饰器在前端开发中的其他潜在应用场景,并举例说明。
48.9万 热度难度
前端开发TypeScript

知识考点

AI 面试

面试题答案

一键面试

性能问题分析

  1. 频繁调用装饰器函数:每次访问或修改被装饰的属性时,装饰器内部逻辑可能都会被执行。例如,在属性监听中,如果监听函数内有复杂计算,频繁触发会消耗大量性能。
  2. 额外的对象包装:TypeScript 的属性装饰器通常通过创建代理对象或修改对象的属性描述符来实现功能,这会带来额外的内存开销。尤其是在大型项目中,如果有大量被装饰的属性,内存占用会显著增加。
  3. 继承与重载问题:在继承体系中,子类可能会继承父类被装饰的属性,若处理不当,装饰器逻辑可能会重复执行,导致性能下降。同时,重载属性时也可能出现类似问题。

优化方法

  1. 防抖与节流:对于监听函数,可以使用防抖(Debounce)或节流(Throttle)技术。例如,在属性值变化监听中,防抖可确保在一定时间内多次触发只执行一次回调,节流则按固定频率执行回调。
function debounce(func: Function, delay: number) {
    let timer: NodeJS.Timeout;
    return function() {
        const context = this;
        const args = arguments;
        clearTimeout(timer);
        timer = setTimeout(() => {
            func.apply(context, args);
        }, delay);
    };
}

class MyClass {
    @debouncePropertyListener(1000)
    myProperty: string;
}

function debouncePropertyListener(delay: number) {
    return function(target: any, propertyKey: string) {
        let value: any;
        const originalGet = Object.getOwnPropertyDescriptor(target, propertyKey)?.get;
        const originalSet = Object.getOwnPropertyDescriptor(target, propertyKey)?.set;

        Object.defineProperty(target, propertyKey, {
            get() {
                return value;
            },
            set(newValue) {
                const debouncedSet = debounce(() => {
                    if (originalSet) {
                        originalSet.call(target, newValue);
                    } else {
                        value = newValue;
                    }
                }, delay);
                debouncedSet();
            }
        });
    };
}
  1. 缓存计算结果:如果装饰器内有计算属性值的逻辑,可缓存结果,避免重复计算。例如,对于一个验证属性值是否符合某种复杂规则的装饰器,若属性值不变,直接返回之前验证结果。
  2. 优化对象包装:尽量减少不必要的代理对象创建,对于简单的属性监听与验证,直接操作属性描述符可能更高效。同时,在项目中合理规划对象生命周期,及时释放不再使用的被装饰对象。
  3. 处理继承与重载:在子类中明确标识是否需要重新应用装饰器逻辑,避免重复执行。例如,可以在父类装饰器逻辑中增加判断,若子类有特殊处理则不再重复执行通用逻辑。

其他潜在应用场景

  1. 日志记录:在属性访问或修改时记录日志,方便调试与监控。
function logProperty(target: any, propertyKey: string) {
    const originalGet = Object.getOwnPropertyDescriptor(target, propertyKey)?.get;
    const originalSet = Object.getOwnPropertyDescriptor(target, propertyKey)?.set;

    Object.defineProperty(target, propertyKey, {
        get() {
            console.log(`Getting property ${propertyKey}`);
            if (originalGet) {
                return originalGet.call(target);
            }
        },
        set(newValue) {
            console.log(`Setting property ${propertyKey} to ${newValue}`);
            if (originalSet) {
                originalSet.call(target, newValue);
            }
        }
    });
}

class LoggerClass {
    @logProperty
    myValue: string;
}
  1. 数据转换:在属性赋值时自动进行数据类型转换。例如,将字符串类型的日期转换为 Date 对象。
function dateConverter(target: any, propertyKey: string) {
    const originalSet = Object.getOwnPropertyDescriptor(target, propertyKey)?.set;

    Object.defineProperty(target, propertyKey, {
        set(newValue) {
            if (typeof newValue ==='string') {
                newValue = new Date(newValue);
            }
            if (originalSet) {
                originalSet.call(target, newValue);
            }
        }
    });
}

class DateClass {
    @dateConverter
    myDate: Date;
}
  1. 权限控制:根据用户权限决定是否允许访问或修改属性。
function accessControl(role: string) {
    return function(target: any, propertyKey: string) {
        const originalGet = Object.getOwnPropertyDescriptor(target, propertyKey)?.get;
        const originalSet = Object.getOwnPropertyDescriptor(target, propertyKey)?.set;

        Object.defineProperty(target, propertyKey, {
            get() {
                if (currentUser.role === role) {
                    if (originalGet) {
                        return originalGet.call(target);
                    }
                } else {
                    throw new Error('Access denied');
                }
            },
            set(newValue) {
                if (currentUser.role === role) {
                    if (originalSet) {
                        originalSet.call(target, newValue);
                    }
                } else {
                    throw new Error('Access denied');
                }
            }
        });
    };
}

class User {
    role: string;
    constructor(role: string) {
        this.role = role;
    }
}

let currentUser = new User('admin');

class SecureClass {
    @accessControl('admin')
    sensitiveData: string;
}