•19 min read

TypeScript 5.8+ Stage 3 Decorator: Metaprogramming, DI & Xác thực Runtime trong môi trường sản xuất

TypeScript 5.8+ Stage 3 Decorator: Metaprogramming, DI & Xác thực Runtime trong môi trường sản xuất

TypeScript 5.8+ hỗ trợ nguyên bản Decorator ECMAScript Stage 3, đánh dấu một sự thay đổi then chốt so với cờ experimentalDecorators cũ. Việc triển khai nguyên bản này cung cấp một cơ chế tiêu chuẩn hóa, mạnh mẽ cho metaprogramming, cho phép các mẫu mạnh mẽ như Dependency Injection (DI) và xác thực runtime mà không cần dựa vào các tính năng không chuẩn hoặc polyfill. Hướng dẫn này trình bày chi tiết ứng dụng thực tế của các decorator này, tập trung vào kiến trúc, triển khai và tiện ích sản xuất của chúng.

Audio Briefing
0:00 / 0:00

Tìm hiểu Decorator Stage 3

Decorator Stage 3 hoạt động trên các phần tử lớp trong quá trình định nghĩa, không phải khởi tạo. Chúng là các hàm nhận các ngữ cảnh cụ thể và cho phép sửa đổi hoặc thay thế phần tử được trang trí. Không giống như experimentalDecorators, đặc tả mới cung cấp một API dễ đoán và mạnh mẽ hơn, bao gồm quyền truy cập vào các trường riêng tư và khả năng thêm các trình khởi tạo.

Các loại và chữ ký Decorator

Chữ ký hàm decorator thay đổi tùy theo mục tiêu:

  1. Class Decorators: Áp dụng cho các khai báo lớp.

    type ClassDecorator = <T extends Function>(
      value: T,
      context: ClassDecoratorContext
    ) => T | void;
    

    context cung cấp kind: 'class', name, và addInitializer.

  2. Method Decorators: Áp dụng cho các phương thức trong một lớp.

    type MethodDecorator = <T extends Function>(
      value: T,
      context: ClassMethodDecoratorContext
    ) => T | void;
    

    context cung cấp kind: 'method', name, static, private, access, và addInitializer.

  3. Getter/Setter Decorators: Áp dụng cho các bộ truy cập.

    type AccessorDecorator = <T>(
      value: ClassAccessorDecoratorTarget<T>,
      context: ClassAccessorDecoratorContext<ThisType<T>, T>
    ) => ClassAccessorDecoratorResult<ThisType<T>, T> | void;
    

    context cung cấp kind: 'getter' hoặc 'setter', name, static, private, access, và addInitializer.

  4. Field Decorators: Áp dụng cho các trường lớp.

    type FieldDecorator = <T>(
      value: undefined, // Field decorators receive undefined as value
      context: ClassFieldDecoratorContext<ThisType<T>, T>
    ) => ((initialValue: T) => T) | void;
    

    context cung cấp kind: 'field', name, static, private, access, và addInitializer. Giá trị trả về là một hàm sửa đổi giá trị ban đầu của trường.

  5. Auto-Accessor Decorators: Áp dụng cho các thuộc tính accessor.

    type AutoAccessorDecorator = <T>(
      value: ClassAccessorDecoratorTarget<T>,
      context: ClassAccessorDecoratorContext<ThisType<T>, T>
    ) => ClassAccessorDecoratorResult<ThisType<T>, T> | void;
    

    Tương tự như getter/setter, nhưng dành cho cú pháp accessor kết hợp.

Cấu hình

Đảm bảo tsconfig.json được cấu hình cho các decorator nguyên bản:

{
  "compilerOptions": {
    "target": "ES2022", // Or higher
    "module": "ESNext",
    "lib": ["ES2022", "DOM"],
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "Bundler", // Or Node16/NodeNext
    "experimentalDecorators": false, // Crucial: Disable legacy decorators
    "emitDecoratorMetadata": false, // Crucial: Disable legacy metadata
    "useDefineForClassFields": true // Recommended for class fields
  }
}
Advertisement

Trường hợp sử dụng 1: Lightweight Dependency Injection Container

Một DI container không phụ thuộc nào chứng minh các decorator lớp và trường để quản lý vòng đời dịch vụ và inject các dependency.

// di.ts

// A simple map to store registered services and their instances
const serviceRegistry = new Map<string | symbol, { constructor: new (...args: any[]) => any; instance?: any; singleton: boolean }>();

// Symbol for internal metadata storage on classes
const INJECTABLE_METADATA = Symbol('injectable_metadata');

/**
 * Class decorator to mark a class as injectable and register it with the DI container.
 * @param singleton If true, only one instance of the class will be created and reused.
 */
export function Injectable(singleton: boolean = true) {
  return <T extends new (...args: any[]) => any>(
    target: T,
    context: ClassDecoratorContext<T>
  ) => {
    if (context.kind !== 'class') {
      throw new Error(`@Injectable can only be applied to classes, not ${String(context.kind)}`);
    }

    const serviceName = target.name;
    if (serviceRegistry.has(serviceName)) {
      console.warn(`Service "${serviceName}" already registered. Overwriting.`);
    }

    serviceRegistry.set(serviceName, { constructor: target, singleton });

    // Store metadata about dependencies for this class
    context.addInitializer(function(this: T) {
      // This runs after the class is defined, but before any instances are created.
      // We can use 'this' to refer to the class constructor itself.
      // Any @Inject() decorators would have added metadata to this.
    });

    return target; // Return the original class
  };
}

/**
 * Property decorator to inject a dependency into a class field.
 * The field type annotation is used to determine the dependency to inject.
 */
export function Inject(targetService?: new (...args: any[]) => any) {
  return <T, V>(
    value: undefined, // Field decorators receive undefined as value
    context: ClassFieldDecoratorContext<T, V>
  ) => {
    if (context.kind !== 'field') {
      throw new Error(`@Inject can only be applied to fields, not ${String(context.kind)}`);
    }

    // This initializer runs when an instance of the class is created.
    context.addInitializer(function(this: T) {
      // 'this' refers to the instance of the class being constructed.
      // We need to determine the type of the field to inject.
      // In TypeScript, runtime type information for fields is not directly available
      // without emitDecoratorMetadata (which we're avoiding) or explicit type passing.
      // For simplicity, we'll rely on `targetService` if provided, or assume the field name
      // corresponds to a registered service name. A more robust solution might use a symbol
      // or string literal for the service key.

      const fieldName = String(context.name);
      let serviceKey: string | symbol;

      if (targetService) {
        serviceKey = targetService.name;
      } else {
        // Fallback: assume field name is the service name.
        // This is less robust and prone to naming conflicts.
        // A better approach would be to require @Inject(ServiceClass)
        serviceKey = fieldName.charAt(0).toUpperCase() + fieldName.slice(1); // e.g., 'logger' -> 'Logger'
        console.warn(`@Inject on field '${fieldName}' without explicit service. Assuming service name '${String(serviceKey)}'. Consider @Inject(ServiceClass).`);
      }

      const serviceEntry = serviceRegistry.get(serviceKey);

      if (!serviceEntry) {
        throw new Error(`Service "${String(serviceKey)}" not found in DI container for injection into field "${fieldName}".`);
      }

      if (serviceEntry.singleton && serviceEntry.instance) {
        // Reuse existing singleton instance
        (this as any)[fieldName] = serviceEntry.instance;
      } else {
        // Create new instance, resolving its dependencies recursively
        const instance = resolve(serviceEntry.constructor);
        if (serviceEntry.singleton) {
          serviceEntry.instance = instance; // Store for future reuse
        }
        (this as any)[fieldName] = instance;
      }
    });

    // Field decorators return a function to modify the initial value.
    // Since we're injecting, we don't need an initial value from the decorator.
    return;
  };
}

/**
 * Resolves an instance of a service from the DI container.
 * Handles recursive dependency resolution.
 */
export function resolve<T>(constructor: new (...args: any[]) => T): T {
  const serviceName = constructor.name;
  const serviceEntry = serviceRegistry.get(serviceName);

  if (!serviceEntry) {
    throw new Error(`Service "${serviceName}" not registered with DI container.`);
  }

  if (serviceEntry.singleton && serviceEntry.instance) {
    return serviceEntry.instance as T;
  }

  // Recursively resolve constructor arguments
  // This is a simplified approach. A real DI container would inspect constructor
  // parameters for @Inject annotations or type metadata.
  // For this example, we assume constructor args are also Injectable services
  // or primitive types not managed by DI.
  const dependencies: any[] = []; // In a real system, this would be populated by inspecting constructor params

  const instance = new constructor(...dependencies);

  // The @Inject field decorators will run their initializers *after* the constructor completes
  // and before the instance is fully returned from `new constructor()`.
  // This is handled by the JS engine's class construction process.

  if (serviceEntry.singleton) {
    serviceEntry.instance = instance;
  }

  return instance;
}

// --- Usage Example ---

interface ILogger {
  log(message: string): void;
}

@Injectable(true)
class ConsoleLogger implements ILogger {
  log(message: string): void {
    console.log(`[ConsoleLogger] ${message}`);
  }
}

@Injectable(false) // Not a singleton
class TransientService {
  private id: number;
  constructor() {
    this.id = Math.random();
    console.log(`TransientService instance created: ${this.id}`);
  }

  getId(): number {
    return this.id;
  }
}

@Injectable(true)
class UserService {
  @Inject(ConsoleLogger) // Explicitly inject ConsoleLogger
  private logger!: ILogger; // Use definite assignment assertion

  @Inject() // Implicitly inject TransientService (by field name convention)
  private transientService!: TransientService;

  getUser(id: string): string {
    this.logger.log(`Fetching user ${id}`);
    return `User ${id} (Transient ID: ${this.transientService.getId()})`;
  }
}

// Resolve the root service
const userService = resolve(UserService);
console.log(userService.getUser('123'));

const anotherUserService = resolve(UserService);
console.log(anotherUserService.getUser('456'));

// Verify singleton behavior for UserService and ConsoleLogger
console.log('userService === anotherUserService:', userService === anotherUserService); // Should be true
// Verify transient behavior for TransientService
// The transientService injected into userService and anotherUserService should be different instances
// This is because @Inject() creates a new instance for each injection point if the service is not a singleton.
// However, since UserService itself is a singleton, the same TransientService instance will be injected
// into the *same* UserService instance. To see different TransientService instances, we'd need to
// resolve UserService multiple times if it were *not* a singleton, or inject TransientService into
// different non-singleton services.
// Let's demonstrate by resolving TransientService directly:
const ts1 = resolve(TransientService);
const ts2 = resolve(TransientService);
console.log('ts1 === ts2:', ts1 === ts2); // Should be false, as TransientService is not a singleton

// Expected output:
// TransientService instance created: 0.xxxx
// TransientService instance created: 0.yyyy
// [ConsoleLogger] Fetching user 123
// User 123 (Transient ID: 0.xxxx)
// [ConsoleLogger] Fetching user 456
// User 456 (Transient ID: 0.xxxx)
// userService === anotherUserService: true
// TransientService instance created: 0.zzzz
// TransientService instance created: 0.aaaa
// ts1 === ts2: false

DI container này sử dụng @Injectable để đăng ký các lớp và @Inject để đánh dấu các trường cho dependency injection. Callback addInitializer trong field decorator rất quan trọng: nó chạy sau constructor của lớp nhưng trước khi instance được trả về hoàn toàn, cho phép trường được điền với dependency đã được giải quyết.

Trường hợp sử dụng 2: High-Throughput Runtime Schema Validation

Xác thực runtime rất quan trọng đối với các ranh giới API và tính toàn vẹn dữ liệu. Decorator có thể tự động hóa việc áp dụng và xác thực schema. Chúng ta sẽ sử dụng một thư viện xác thực đơn giản hóa.

// validator.ts

type ValidatorFn = (value: any) => string | null; // Returns error message or null if valid

const VALIDATION_METADATA = Symbol('validation_metadata');

interface FieldValidation {
  propertyName: string | symbol;
  validators: ValidatorFn[];
}

/**
 * Stores validation rules on the class prototype.
 */
function addValidationRule(target: Object, propertyName: string | symbol, validator: ValidatorFn) {
  if (!Reflect.hasOwnMetadata(VALIDATION_METADATA, target)) {
    Reflect.defineMetadata(VALIDATION_METADATA, [], target);
  }
  const validations = Reflect.getOwnMetadata(VALIDATION_METADATA, target) as FieldValidation[];

  let fieldValidation = validations.find(fv => fv.propertyName === propertyName);
  if (!fieldValidation) {
    fieldValidation = { propertyName, validators: [] };
    validations.push(fieldValidation);
  }
  fieldValidation.validators.push(validator);
}

/**
 * Decorator factory for 'required' validation.
 */
export function Required() {
  return <T, V>(
    value: undefined,
    context: ClassFieldDecoratorContext<T, V>
  ) => {
    if (context.kind !== 'field') {
      throw new Error(`@Required can only be applied to fields, not ${String(context.kind)}`);
    }
    context.addInitializer(function(this: T) {
      addValidationRule(Object.getPrototypeOf(this), context.name, (val) =>
        val === null || val === undefined || (typeof val === 'string' && val.trim() === '')
          ? `${String(context.name)} is required.`
          : null
      );
    });
  };
}

/**
 * Decorator factory for 'minLength' validation.
 */
export function MinLength(length: number) {
  return <T, V extends string>(
    value: undefined,
    context: ClassFieldDecoratorContext<T, V>
  ) => {
    if (context.kind !== 'field') {
      throw new Error(`@MinLength can only be applied to fields, not ${String(context.kind)}`);
    }
    context.addInitializer(function(this: T) {
      addValidationRule(Object.getPrototypeOf(this), context.name, (val) =>
        typeof val === 'string' && val.length < length
          ? `${String(context.name)} must be at least ${length} characters long.`
          : null
      );
    });
  };
}

/**
 * Decorator factory for 'max' validation.
 */
export function Max(maxValue: number) {
  return <T, V extends number>(
    value: undefined,
    context: ClassFieldDecoratorContext<T, V>
  ) => {
    if (context.kind !== 'field') {
      throw new Error(`@Max can only be applied to fields, not ${String(context.kind)}`);
    }
    context.addInitializer(function(this: T) {
      addValidationRule(Object.getPrototypeOf(this), context.name, (val) =>
        typeof val === 'number' && val > maxValue
          ? `${String(context.name)} must be at most ${maxValue}.`
          : null
      );
    });
  };
}

/**
 * Class decorator to enable validation for a class.
 * It adds a `validate()` method to the class prototype.
 */
export function Validatable() {
  return <T extends new (...args: any[]) => any>(
    target: T,
    context: ClassDecoratorContext<T>
  ) => {
    if (context.kind !== 'class') {
      throw new Error(`@Validatable can only be applied to classes, not ${String(context.kind)}`);
    }

    // Add a validate method to the class prototype
    context.addInitializer(function(this: T) {
      Object.defineProperty(this.prototype, 'validate', {
        value: function(this: any): string[] {
          const errors: string[] = [];
          const validations = Reflect.getOwnMetadata(VALIDATION_METADATA, Object.getPrototypeOf(this)) as FieldValidation[] || [];

          for (const fieldValidation of validations) {
            const value = this[fieldValidation.propertyName];
            for (const validator of fieldValidation.validators) {
              const error = validator(value);
              if (error) {
                errors.push(error);
              }
            }
          }
          return errors;
        },
        writable: true,
        configurable: true,
      });
    });

    return target;
  };
}

// Polyfill for Reflect.metadata if not available (e.g., in some environments or older TS versions)
// For native decorators, Reflect.metadata is not strictly required by the spec itself,
// but it's a common pattern for storing metadata.
// If you're using a bundler like Webpack/Rollup/ESBuild, ensure 'reflect-metadata' is imported once at the entry point.
// `import 'reflect-metadata';`
// For this example, we'll assume it's available or provide a minimal shim.
if (typeof Reflect === 'undefined' || !Reflect.hasOwnMetadata) {
  console.warn("Reflect.metadata not found. Providing a minimal shim. For production, consider 'reflect-metadata' polyfill.");
  const metadataMap = new WeakMap<object, Map<string | symbol, any>>();

  (Reflect as any).defineMetadata = (key: string | symbol, value: any, target: object, propertyKey?: string | symbol) => {
    let targetMetadata = metadataMap.get(target);
    if (!targetMetadata) {
      targetMetadata = new Map();
      metadataMap.set(target, targetMetadata);
    }
    const metadataKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : String(key);
    targetMetadata.set(metadataKey, value);
  };

  (Reflect as any).getOwnMetadata = (key: string | symbol, target: object, propertyKey?: string | symbol) => {
    const targetMetadata = metadataMap.get(target);
    if (!targetMetadata) return undefined;
    const metadataKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : String(key);
    return targetMetadata.get(metadataKey);
  };

  (Reflect as any).hasOwnMetadata = (key: string | symbol, target: object, propertyKey?: string | symbol) => {
    const targetMetadata = metadataMap.get(target);
    if (!targetMetadata) return false;
    const metadataKey = propertyKey ? `${String(propertyKey)}:${String(key)}` : String(key);
    return targetMetadata.has(metadataKey);
  };
}


// --- Usage Example ---

@Validatable()
class Product {
  @Required()
  @MinLength(3)
  name: string;

  @Required()
  @Max(9999)
  price: number;

  description?: string;

  constructor(name: string, price: number, description?: string) {
    this.name = name;
    this.price = price;
    this.description = description;
  }
}

// Test cases
const product1 = new Product('Laptop', 1200);
const errors1 = (product1 as any).validate();
console.log('Product 1 errors:', errors1); // Expected: []

const product2 = new Product('', 50000);
const errors2 = (product2 as any).validate();
console.log('Product 2 errors:', errors2); // Expected: ["name is required.", "name must be at least 3 characters long.", "price must be at most 9999."]

const product3 = new Product('TV', 10000);
const errors3 = (product3 as any).validate();
console.log('Product 3 errors:', errors3); // Expected: ["price must be at most 9999."]

const product4 = new Product('A', 100);
const errors4 = (product4 as any).validate();
console.log('Product 4 errors:', errors4); // Expected: ["name must be at least 3 characters long."]

// Demonstrate that validate method is added to prototype
console.log('Product.prototype has validate method:', 'validate' in Product.prototype); // true

Hệ thống xác thực này sử dụng các field decorator (@Required, @MinLength, @Max) để gắn các quy tắc xác thực vào các thuộc tính. Class decorator @Validatable sau đó inject một phương thức validate vào prototype của lớp. Phương thức này lặp qua các quy tắc đã thu thập và thực thi chúng đối với các thuộc tính của instance. addInitializer trong các field decorator đảm bảo rằng các quy tắc xác thực được đăng ký trên prototype của lớp trước khi bất kỳ instance nào được tạo, làm cho phương thức validate có sẵn ngay lập tức. Lưu ý việc sử dụng Reflect.defineMetadata và Reflect.getOwnMetadata để lưu trữ các quy tắc xác thực, điều này yêu cầu polyfill reflect-metadata (hoặc một shim như đã cung cấp).

So sánh kiến trúc: Stage 3 Decorator vs. Legacy experimentalDecorators

Tính năngStage 3 Decorator (TS 5.8+)Legacy experimentalDecorators (TS < 5.8)
Trạng thái đặc tảECMAScript Stage 3 (gần hoàn thiện)Không chuẩn, thử nghiệm
tsconfig.jsonexperimentalDecorators: false, emitDecoratorMetadata: falseexperimentalDecorators: true, emitDecoratorMetadata: true (cho DI)
Ngữ cảnh DecoratorClassDecoratorContext phong phú, ClassMethodDecoratorContext, v.v.Không có đối tượng ngữ cảnh rõ ràng; các đối số là target, key, descriptor
addInitializerCó, cho phép chạy code sau định nghĩa lớp hoặc sau khi xây dựng instanceKhông có tương đương trực tiếp; thường yêu cầu thiết lập thuộc tính tĩnh thủ công
Field DecoratorNhận undefined dưới dạng value, trả về hàm khởi tạoNhận target, key; sửa đổi descriptor (nếu là accessor) hoặc target
Trường riêng tưCó thể trang trí và tương tác với các trường riêng tư thông qua accessKhông thể trang trí các trường riêng tư
Reflect.metadataKhông yêu cầu bởi đặc tả; được sử dụng cho siêu dữ liệu tùy chỉnhPhụ thuộc nhiều vào phản ánh kiểu (ví dụ: cho DI)
Hành vi RuntimeDecorator chạy trong quá trình định nghĩa lớpDecorator chạy trong quá trình định nghĩa lớp
Hiệu suấtThường được tối ưu hóa, một phần của runtime công cụ JSCó thể gây ra chi phí do Reflect.metadata và polyfill
Khả năng bảo trìAPI tiêu chuẩn hóa, ổn định lâu dài hơnAPI có thể thay đổi, ít dự đoán hơn
Advertisement

Những vấn đề và cách khắc phục trong sản xuất

  1. experimentalDecorators vẫn được bật:

    • Triệu chứng: Decorator hoạt động không mong muốn, hoặc TypeScript phàn nàn về cú pháp decorator ngay cả với TS 5.8+.
    • Nguyên nhân: Bạn có thể vẫn còn "experimentalDecorators": true trong tsconfig.json của mình.
    • Cách khắc phục: Đặt "experimentalDecorators": false và "emitDecoratorMetadata": false. Đảm bảo target của bạn là ES2022 trở lên và moduleResolution là Bundler hoặc NodeNext.
  2. Thiếu Reflect.metadata:

    • Triệu chứng: Lỗi runtime như Reflect.defineMetadata is not a function khi sử dụng các mẫu metadata (như trong ví dụ xác thực).
    • Nguyên nhân: Polyfill reflect-metadata không được import hoặc không có sẵn trong môi trường runtime của bạn. Các decorator Stage 3 nguyên bản không cung cấp Reflect.metadata.
    • Cách khắc phục: Thêm import 'reflect-metadata'; ở đầu điểm vào ứng dụng của bạn. Đảm bảo gói reflect-metadata đã được cài đặt (npm install reflect-metadata).
  3. Thông tin kiểu tại Runtime:

    • Triệu chứng: Trong DI, bạn muốn inject ServiceA vào class MyClass { @Inject() serviceA: ServiceA; } nhưng decorator không thể xác định kiểu của ServiceA.
    • Nguyên nhân: Các kiểu TypeScript bị xóa trong quá trình biên dịch. emitDecoratorMetadata (cung cấp thông tin kiểu thông qua Reflect.metadata) dành cho các decorator cũ và không tương thích với Stage 3.
    • Cách khắc phục: Truyền rõ ràng kiểu cho decorator, ví dụ: @Inject(ServiceA). Đối với constructor injection, bạn cần phân tích Function.prototype.toString() của constructor hoặc sử dụng bước xây dựng để trích xuất các kiểu.
  4. Thứ tự thực thi của Decorator:

    • Triệu chứng: Hành vi không mong muốn khi nhiều decorator được áp dụng cho cùng một mục tiêu.
    • Nguyên nhân: Decorator được áp dụng theo một thứ tự cụ thể:
      • Field/Method/Accessor: Từ trên xuống dưới, sau đó được đánh giá từ dưới lên trên.
      • Class: Được áp dụng sau khi tất cả các thành viên của nó được trang trí.
      • Các callback addInitializer chạy theo thứ tự chúng được thêm vào.
    • Cách khắc phục: Hiểu thứ tự thực thi. Nếu decorator A phụ thuộc vào đầu ra của decorator B, hãy đảm bảo B chạy trước. Đối với addInitializer, chúng chạy theo thứ tự chúng được đăng ký.
  5. Ngữ cảnh this trong addInitializer:

    • Triệu chứng: this bên trong addInitializer tham chiếu đến đối tượng sai.
    • Nguyên nhân: Đối với ClassDecoratorContext, this tham chiếu đến constructor của lớp. Đối với ClassFieldDecoratorContext, ClassMethodDecoratorContext, v.v., this tham chiếu đến instance đang được xây dựng.
    • Cách khắc phục: Lưu ý ngữ cảnh this. Đối với các hoạt động cấp lớp (như thêm một phương thức prototype), sử dụng Object.getPrototypeOf(this) hoặc this.prototype nếu this là constructor. Đối với các hoạt động cấp instance (như đặt giá trị trường), this trực tiếp tham chiếu đến instance.

Câu hỏi thường gặp

  1. Tôi có thể sử dụng Stage 3 Decorator với React/Angular/Vue không? Có. Các framework hiện đại đang thích nghi. Angular 17+ hỗ trợ đầy đủ các decorator Stage 3. React và Vue không sử dụng decorator cho các thành phần cốt lõi của chúng nhưng có thể hưởng lợi từ chúng trong các lớp tiện ích hoặc dịch vụ. Đảm bảo công cụ xây dựng của bạn (Webpack, Vite, v.v.) được cấu hình để xử lý chúng (ví dụ: Babel với @babel/plugin-proposal-decorators ở chế độ 2023-11).

  2. Stage 3 Decorator có chậm hơn các hàm thông thường không? Decorator thực thi tại thời điểm định nghĩa lớp, không phải tại runtime cho mỗi lần tạo instance (ngoại trừ các callback addInitializer chạy trong quá trình xây dựng instance). Chi phí là tối thiểu và thường không đáng kể đối với hầu hết các ứng dụng. Tác động hiệu suất chính đến từ những gì các decorator làm (ví dụ: phản ánh phức tạp hoặc tính toán nặng).

  3. Làm cách nào để debug decorator? Đặt các câu lệnh debugger; bên trong các hàm decorator của bạn. Vì chúng chạy trong quá trình định nghĩa lớp, trình gỡ lỗi của bạn sẽ tạm dừng khi lớp đang được định nghĩa. Đối với các callback addInitializer, chúng sẽ tạm dừng trong quá trình tạo instance.

  4. Tôi có thể trang trí các tham số constructor không? Đề xuất Stage 3 Decorator hiện tại không bao gồm các decorator tham số. Đây là một tính năng của experimentalDecorators nhưng đã bị loại bỏ do sự phức tạp và lo ngại về phản ánh kiểu runtime. Đối với DI với các tham số constructor, bạn thường cần cung cấp rõ ràng các dependency hoặc sử dụng bước xây dựng để trích xuất thông tin kiểu.

  5. Sự khác biệt giữa value và context trong các đối số decorator là gì? value là thứ đang được trang trí (ví dụ: hàm phương thức, constructor của lớp). Đối với các field decorator, value là undefined vì các trường không có "giá trị" ban đầu theo cách mà các phương thức có. context cung cấp siêu dữ liệu về phần tử được trang trí, chẳng hạn như kind (lớp, phương thức, trường), name, trạng thái static, trạng thái private và hàm addInitializer quan trọng.

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement