This article has participated in the activity of “New person creation Ceremony”, and started the road of digging gold creation together.
As a front-end framework designed for “big front-end projects,” Angular has a lot of design to learn from, and this series focuses on how those designs and features work. This article focuses on metadata, which is ubiquitous in Angular.
Decorators are a core concept when developing with Angular. In Angular, decorators are used to attach metadata to classes or properties to let you know what those classes or properties mean and what to do with them.
Decorators and metadata
Neither decorators nor metadata are concepts introduced by Angular. So let’s take a quick look.
Metadata
In a general sense, metadata is data that describes user data. It summarizes the basic information about the data and makes it easier to find and use specific data instances. For example, author, create date, modify date, and file size are examples of very basic document metadata.
In the case of classes, metadata is used to decorate the class to describe its definition and behavior so that the expected behavior of the class can be configured.
Decorator
Decorators are a language feature of JavaScript and are an experimental feature in Stage 2.
Decorators are functions that are called during the definition of a class, class element, or other JavaScript syntax.
Decorative appliances have three main functions:
- You can replace the value being decorated with a matching value with the same semantics. (For example, decorators can replace methods with another method, one field with another, one class with another, and so on).
- Metadata can be associated with the value being decorated; This metadata can be read externally and used for metaprogramming and self-checking.
- Metadata provides access to the value being decorated. For common values, they can be implemented by value names; For private values, they receive accessor functions and can then choose to share them.
In essence, decorators can be used to metaprogram and add functionality to values without fundamentally changing their external behavior.
For more information, see the TC39 / proposal-Decorators proposal.
Decorators and metadata in Angular
When developing Angular applications, components, directives, services, modules, etc., are defined and developed through decorators. A decorator appears immediately before the class definition, declaring that the class has the specified type and providing metadata appropriate for that type.
For example, we can declare Angular classes with the following decorators: @Component(), @Directive(), @pipe (), @Injectable(), and @ngModule ().
Use decorators and metadata to change the behavior of classes
In the case of @Component(), this decorator does some things:
- Mark the class as an Angular component.
- Provides configurable metadata to determine how the component should be processed, instantiated, and used at run time.
See how @Component() can be used, not covered here. Let’s look at the definition of this decorator:
// Provide configuration metadata interface definitions for Angular components
// In Angular, components are subsets of directives that are always associated with templates
export interface Component extends Directive {
// changeDetection the changeDetection policy for this component
// When a component is instantiated, Angular creates a change detector that propagates the component's binding.changeDetection? : ChangeDetectionStrategy;// Defines a collection of injectable objects visible to the DOM child objects of its viewviewProviders? : Provider[];// The module ID of the module that contains the component, which must be able to resolve relative urls for templates and stylesmoduleId? :string; .// Encapsulating strategies for templates and CSS stylesencapsulation? : ViewEncapsulation;// Override the default interpolation start and end delimiters (' {{' and '}} ')interpolation? : [string.string];
}
// Component decorators and metadata
export const Component: ComponentDecorator = makeDecorator(
'Component'.// Use the default CheckAlways policy, in which change detection is automatic until explicitly disabled.
(c: Component = {}) = > ({changeDetection: ChangeDetectionStrategy.Default, ... c}), Directive,undefined.(type: Type<any>, meta: Component) = > SWITCH_COMPILE_COMPONENT(type, meta));
Copy the code
With this definition of component decorator and component metadata, let’s take a look at the decorator creation process.
Decorator creation process
As we can see from the source code, component and directive decorators are generated by makeDecorator() :
export function makeDecorator<T> (
name: string, props? : (... args:any[]) = >any, parentClass? :any.// Decorator name and propertiesadditionalProcessing? : (type: Type<T>) => void, typeFn? : (type: Type<T>, ... args:any[]) = >void) :{new(... args:any[]) :any; (... args:any[]) :any; (... args:any[]) :(cls: any) = > any; } {// noSideEffects is used to verify that functions wrapped by the closure compiler have noSideEffects
return noSideEffects(() = > {
const metaCtor = makeMetadataCtor(props);
// Decorator factory
function DecoratorFactory(
this: unknown|typeofDecoratorFactory, ... args:any[]) : (cls: Type<T>) = >any {
if (this instanceof DecoratorFactory) {
// Assign metadata
metaCtor.call(this. args);return this as typeof DecoratorFactory;
}
// Create a decorator factory
const annotationInstance = new (DecoratorFactory as any) (... args);return function TypeDecorator(cls: Type<T>) {
/ / compile
if(typeFn) typeFn(cls, ... args);// Using Object.defineProperty is important because it creates an attribute that is not enumerable, preventing it from being copied during subclassing.
const annotations = cls.hasOwnProperty(ANNOTATIONS) ?
(cls as any)[ANNOTATIONS] :
Object.defineProperty(cls, ANNOTATIONS, {value: []})[ANNOTATIONS];
annotations.push(annotationInstance);
// Execution of specific logic
if (additionalProcessing) additionalProcessing(cls);
return cls;
};
}
if (parentClass) {
// Inherits the parent class
DecoratorFactory.prototype = Object.create(parentClass.prototype);
}
DecoratorFactory.prototype.ngMetadataName = name;
(DecoratorFactory as any).annotationCls = DecoratorFactory;
return DecoratorFactory as any;
});
}
Copy the code
In the example above, we use makeDecorator() to generate a Component decorator factory that defines the Component. When a Component is created using @Component(), Angular compiles the Component based on metadata.
Compile components from decorator metadata
ɵ CMP Angular compiles the Angular component from the decorator metadata and then patches the generated component definition (ɵ CMP) to the component type:
export function compileComponent(type: Type<any>, metadata: Component) :void {
// Initialize ngDevMode
(typeof ngDevMode === 'undefined' || ngDevMode) && initNgDevMode();
let ngComponentDef: any = null;
// Metadata may have resources that need to be parsed
maybeQueueResolutionOfComponentResources(type, metadata);
// The same functionality is used here as the directive, because this is only a subset of the metadata required to create ngFactoryDef
addDirectiveFactoryDef(type, metadata);
Object.defineProperty(type, NG_COMP_DEF, {
get: () = > {
if (ngComponentDef === null) {
const compiler = getCompilerFacade();
// Parse components based on metadata
if (componentNeedsResolution(metadata)) {
...
// Exception handling}...// Create the complete metadata needed to compile the component
const templateUrl = metadata.templateUrl || `ng:///The ${type.name}/template.html`;
constmeta: R3ComponentMetadataFacade = { ... directiveMetadata(type, metadata),
typeSourceSpan: compiler.createParseSourceSpan('Component'.type.name, templateUrl),
template: metadata.template || ' ',
preserveWhitespaces,
styles: metadata.styles || EMPTY_ARRAY,
animations: metadata.animations,
directives: [].changeDetection: metadata.changeDetection,
pipes: new Map(),
encapsulation,
interpolation: metadata.interpolation,
viewProviders: metadata.viewProviders || null};// The compilation process needs to compute depth in order to verify that the compilation is finally complete
compilationDepth++;
try {
if (meta.usesInheritance) {
addDirectiveDefToUndecoratedParents(type);
}
// Compile components based on the metadata required by the template, environment, and component
ngComponentDef = compiler.compileComponent(angularCoreEnv, templateUrl, meta);
} finally {
// Even if the compile fails, make sure to reduce the compile depth
compilationDepth--;
}
if (compilationDepth === 0) {
// When executing the NgModule decorator, we queue the module definition so that the queue is only de-queued if all declarations have been resolved, and add itself to all declarations as a module scope
// This call runs a check to see if any modules in the queue can be queued and adds ranges to their declarations
flushModuleScopingQueueAsMuchAsPossible();
}
// If component compilation is asynchronous, the @NgModule annotation declaring the component can be executed and set the ngSelectorScope property on the component type
// This allows a component to patch itself after compiling using directiveDefs in the module
if (hasSelectorScope(type)) {
const scopes = transitiveScopesFor(type.ngSelectorScope); patchComponentDefWithScope(ngComponentDef, scopes); }}returnngComponentDef; },... }); }Copy the code
The process of compiling a component can be asynchronous (such as the need to resolve the URL of a component template or other resource). If the compiler is not immediate, compileComponent will parse resources to join the global queue, and will not be able to return to ɵ CMP, until it is solved by invoking resolveComponentResources global queue.
Metadata during compilation
Metadata is information about a class, but it is not an attribute of the class. Therefore, the data used to configure the definition and behavior of the class should not be stored in an instance of the class; we need to store this data elsewhere.
Angular generates metadata that is managed and maintained using CompileMetadataResolver. Here we’ll look at logic related to directives (components) :
export class CompileMetadataResolver {
private _nonNormalizedDirectiveCache =
new Map<Type, {annotation: Directive, metadata: cpl.CompileDirectiveMetadata}>();
// Use Map to save
private _directiveCache = new Map<Type, cpl.CompileDirectiveMetadata>();
private _summaryCache = new Map<Type, cpl.CompileTypeSummary|null> ();private _pipeCache = new Map<Type, cpl.CompilePipeMetadata>();
private _ngModuleCache = new Map<Type, cpl.CompileNgModuleMetadata>();
private _ngModuleOfTypes = new Map<Type, Type>();
private _shallowModuleCache = new Map<Type, cpl.CompileShallowModuleMetadata>();
constructor(
private _config: CompilerConfig, private _htmlParser: HtmlParser,
private _ngModuleResolver: NgModuleResolver, private _directiveResolver: DirectiveResolver,
private _pipeResolver: PipeResolver, private _summaryResolver: SummaryResolver<any>,
private _schemaRegistry: ElementSchemaRegistry,
private _directiveNormalizer: DirectiveNormalizer, private _console: Console,
private _staticSymbolCache: StaticSymbolCache, private _reflector: CompileReflector,
private_errorCollector? : ErrorCollector) {}
// Clear metadata for a specific instruction
clearCacheFor(type: Type) {
const dirMeta = this._directiveCache.get(type);
this._directiveCache.delete(type); . }// Clear all metadata
clearCache(): void {
this._directiveCache.clear(); . }/** * Loads declared directives and pipes in NgModule */
loadNgModuleDirectiveAndPipeMetadata(moduleType: any.isSync: boolean, throwIfNotFound = true) :Promise<any> {
const ngModule = this.getNgModuleMetadata(moduleType, throwIfNotFound);
const loading: Promise<any= > [] [];if (ngModule) {
ngModule.declaredDirectives.forEach((id) = > {
const promise = this.loadDirectiveMetadata(moduleType, id.reference, isSync);
if(promise) { loading.push(promise); }}); ngModule.declaredPipes.forEach((id) = > this._loadPipeMetadata(id.reference));
}
return Promise.all(loading);
}
// Load the instruction (component) metadata
loadDirectiveMetadata(ngModuleType: any.directiveType: any.isSync: boolean): SyncAsync<null> {
// If already loaded, return directly
if (this._directiveCache.has(directiveType)) {
return null;
}
directiveType = resolveForwardRef(directiveType);
const {annotation, metadata} = this.getNonNormalizedDirectiveMetadata(directiveType)! ;// Create instruction (component) metadata
const createDirectiveMetadata = (templateMetadata: cpl.CompileTemplateMetadata|null) = > {
const normalizedDirMeta = new cpl.CompileDirectiveMetadata({
isHost: false.type: metadata.type,
isComponent: metadata.isComponent,
selector: metadata.selector,
exportAs: metadata.exportAs,
changeDetection: metadata.changeDetection,
inputs: metadata.inputs,
outputs: metadata.outputs,
hostListeners: metadata.hostListeners,
hostProperties: metadata.hostProperties,
hostAttributes: metadata.hostAttributes,
providers: metadata.providers,
viewProviders: metadata.viewProviders,
queries: metadata.queries,
guards: metadata.guards,
viewQueries: metadata.viewQueries,
entryComponents: metadata.entryComponents,
componentViewType: metadata.componentViewType,
rendererType: metadata.rendererType,
componentFactory: metadata.componentFactory,
template: templateMetadata
});
if (templateMetadata) {
this.initComponentFactory(metadata.componentFactory! , templateMetadata.ngContentSelectors); }// Store complete metadata information, as well as metadata summary information
this._directiveCache.set(directiveType, normalizedDirMeta);
this._summaryCache.set(directiveType, normalizedDirMeta.toSummary());
return null;
};
if (metadata.isComponent) {
// If it is a component, the process may be asynchronous and you need to wait for the template to return after the asynchronous process ends
consttemplate = metadata.template ! ;const templateMeta = this._directiveNormalizer.normalizeTemplate({
ngModuleType,
componentType: directiveType,
moduleUrl: this._reflector.componentModuleUrl(directiveType, annotation),
encapsulation: template.encapsulation,
template: template.template,
templateUrl: template.templateUrl,
styles: template.styles,
styleUrls: template.styleUrls,
animations: template.animations,
interpolation: template.interpolation,
preserveWhitespaces: template.preserveWhitespaces
});
if (isPromise(templateMeta) && isSync) {
this._reportError(componentStillLoadingError(directiveType), directiveType);
return null;
}
// Store the metadata
return SyncAsync.then(templateMeta, createDirectiveMetadata);
} else {
// store metadata directly
createDirectiveMetadata(null);
return null; }}// Get metadata information for a given instruction (component)
getDirectiveMetadata(directiveType: any): cpl.CompileDirectiveMetadata {
const dirMeta = this._directiveCache.get(directiveType)! ; .return dirMeta;
}
// Get the metadata summary information for the given instruction (component)
getDirectiveSummary(dirType: any): cpl.CompileDirectiveSummary {
const dirSummary =
<cpl.CompileDirectiveSummary>this._loadSummary(dirType, cpl.CompileSummaryKind.Directive); .returndirSummary; }}Copy the code
As you can see, Map is used to store metadata during compilation of classes, whether component, instruction, pipe, or module.
conclusion
This section introduces decorators and metadata in Angular, which describes the definition and behavior of a class.
The Map data structure is used to maintain and store decorator metadata during Angular compilation, and components, directives, pipes, modules, and so on are compiled from this metadata information.
reference
- 11. Metadata, Metamodelling, and Metaprogramming
- How does the TypeScript Angular DI magic work?