Search code examples
javascriptangularangular-cli

How using 'as' works under the hood in angular?


<div>
  @if (user.name as userName) {
    {{ userName }}
  }
</div>



<div>
 @if (user.name) {
   {{ user.name }}
 }
</div>

I am wondering what is the difference between this two approaches? just a syntax? or something else? For example when using as does it creates variable? What is happening under the hood?


Solution

  • I don't know how deep you want such an explaination to go.
    if you really want to go down to each line of execution, I'm sorry.. I will not put hours into this question.
    Basically this happens:

    First of all, you have to keep in mind, what you are writing there, is a dialect that does resemble js, but isn't.

    So to not go into too much depth and keep this somewhat reasonable, let's start at the point, where the parser will encounter a @ symbol: https://github.com/angular/angular/blob/c213a4e15a594ff141cf312ad301128e7ed4127c/packages/compiler/src/ml_parser/lexer.ts#L206-L207

    } else if (this._tokenizeBlocks && this._attemptCharCode(chars.$AT)) {
      this._consumeBlockStart(start);
    

    basically what you see there, is that when encountering an @, it will consume the continuing data as block.

    the consumption of that block will parse as follows:
    https://github.com/angular/angular/blob/c213a4e15a594ff141cf312ad301128e7ed4127c/packages/compiler/src/ml_parser/lexer.ts#L245-L277

    private _consumeBlockStart(start: CharacterCursor) {
      this._beginToken(TokenType.BLOCK_OPEN_START, start);
      const startToken = this._endToken([this._getBlockName()]);
    
      if (this._cursor.peek() === chars.$LPAREN) {
        // Advance past the opening paren.
        this._cursor.advance();
        // Capture the parameters.
        this._consumeBlockParameters();
        // Allow spaces before the closing paren.
        this._attemptCharCodeUntilFn(isNotWhitespace);
    
        if (this._attemptCharCode(chars.$RPAREN)) {
          // Allow spaces after the paren.
          this._attemptCharCodeUntilFn(isNotWhitespace);
        } else {
          startToken.type = TokenType.INCOMPLETE_BLOCK_OPEN;
          return;
        }
      }
    
      if (this._attemptCharCode(chars.$LBRACE)) {
        this._beginToken(TokenType.BLOCK_OPEN_END);
        this._endToken([]);
      } else {
        startToken.type = TokenType.INCOMPLETE_BLOCK_OPEN;
      }
    }
    
    private _consumeBlockEnd(start: CharacterCursor) {
      this._beginToken(TokenType.BLOCK_CLOSE, start);
      this._endToken([]);
    }
    

    after all the blocks of your code are parsed, it will be thrown into angulars expression parser. (since @ refers to an expression)

    this will also go through, and parse the as bindings.
    https://github.com/angular/angular/blob/c213a4e15a594ff141cf312ad301128e7ed4127c/packages/compiler/src/expression_parser/parser.ts#L1145-L1180

    /**
     * Parse a directive keyword, followed by a mandatory expression.
     * For example, "of items", "trackBy: func".
     * The bindings are: ngForOf -> items, ngForTrackBy -> func
     * There could be an optional "as" binding that follows the expression.
     * For example,
     * ```
     *   *ngFor="let item of items | slice:0:1 as collection".
     *                    ^^ ^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^
     *               keyword    bound target   optional 'as' binding
     * ```
     *
     * @param key binding key, for example, ngFor, ngIf, ngForOf, along with its
     * absolute span.
     */
    private parseDirectiveKeywordBindings(key: TemplateBindingIdentifier): TemplateBinding[] {
      const bindings: TemplateBinding[] = [];
      this.consumeOptionalCharacter(chars.$COLON);  // trackBy: trackByFunction
      const value = this.getDirectiveBoundTarget();
      let spanEnd = this.currentAbsoluteOffset;
      // The binding could optionally be followed by "as". For example,
      // *ngIf="cond | pipe as x". In this case, the key in the "as" binding
      // is "x" and the value is the template key itself ("ngIf"). Note that the
      // 'key' in the current context now becomes the "value" in the next binding.
      const asBinding = this.parseAsBinding(key);
      if (!asBinding) {
        this.consumeStatementTerminator();
        spanEnd = this.currentAbsoluteOffset;
      }
      const sourceSpan = new AbsoluteSourceSpan(key.span.start, spanEnd);
      bindings.push(new ExpressionBinding(sourceSpan, key, value));
      if (asBinding) {
        bindings.push(asBinding);
      }
      return bindings;
    }
    

    which will then go there:
    https://github.com/angular/angular/blob/c213a4e15a594ff141cf312ad301128e7ed4127c/packages/compiler/src/expression_parser/parser.ts#L1202-L1223

    /**
     * Return the binding for a variable declared using `as`. Note that the order
     * of the key-value pair in this declaration is reversed. For example,
     * ```
     *   *ngFor="let item of items; index as i"
     *                              ^^^^^    ^
     *                              value    key
     * ```
     *
     * @param value name of the value in the declaration, "ngIf" in the example
     * above, along with its absolute span.
     */
    private parseAsBinding(value: TemplateBindingIdentifier): TemplateBinding|null {
      if (!this.peekKeywordAs()) {
        return null;
      }
      this.advance();  // consume the 'as' keyword
      const key = this.expectTemplateBindingKey();
      this.consumeStatementTerminator();
      const sourceSpan = new AbsoluteSourceSpan(value.span.start, this.currentAbsoluteOffset);
      return new VariableBinding(sourceSpan, key, value);
    }
    

    and will then do a variable binding as seen here:
    https://github.com/angular/angular/blob/c213a4e15a594ff141cf312ad301128e7ed4127c/packages/compiler/src/expression_parser/ast.ts#L343-L374

    /**
     * TemplateBinding refers to a particular key-value pair in a microsyntax
     * expression. A few examples are:
     *
     *   |---------------------|--------------|---------|--------------|
     *   |     expression      |     key      |  value  | binding type |
     *   |---------------------|--------------|---------|--------------|
     *   | 1. let item         |    item      |  null   |   variable   |
     *   | 2. of items         |   ngForOf    |  items  |  expression  |
     *   | 3. let x = y        |      x       |    y    |   variable   |
     *   | 4. index as i       |      i       |  index  |   variable   |
     *   | 5. trackBy: func    | ngForTrackBy |   func  |  expression  |
     *   | 6. *ngIf="cond"     |     ngIf     |   cond  |  expression  |
     *   |---------------------|--------------|---------|--------------|
     *
     * (6) is a notable exception because it is a binding from the template key in
     * the LHS of a HTML attribute to the expression in the RHS. All other bindings
     * in the example above are derived solely from the RHS.
     */
    export type TemplateBinding = VariableBinding|ExpressionBinding;
    
    export class VariableBinding {
      /**
       * @param sourceSpan entire span of the binding.
       * @param key name of the LHS along with its span.
       * @param value optional value for the RHS along with its span.
       */
      constructor(
          public readonly sourceSpan: AbsoluteSourceSpan,
          public readonly key: TemplateBindingIdentifier,
          public readonly value: TemplateBindingIdentifier|null) {}
    }
    

    afterwards the bindings and other entities that drop out the AST compiler will be rendered here:
    https://github.com/angular/angular/blob/c213a4e15a594ff141cf312ad301128e7ed4127c/packages/compiler/src/render3/view/template.ts#L2875-L2876

    const {nodes, errors, styleUrls, styles, ngContentSelectors, commentNodes} = htmlAstToRender3Ast(
      rootNodes, bindingParser, {collectCommentNodes: !!options.collectCommentNodes});
    

    if you then go further into that function there, called htmlAstToRender3Ast, you will encounter a call to HtmlAstToIvyAst which then goes further into roughly 600 more lines of code that do yet another AST to AST convertion..

    I could go further ofc, but as I said at the start, going deep will take a long time.

    I hope you get a gist of how this somewhat works now.

    essentially it parses this as expression as a variable expansion, like let [foo, bar] = [1, 5] in vanilla js. except in this case it's more like [1, 5][0] as foo

    also a small reminder: all this code is ran every time you compile a template.