聊聊Angular中的模板输入变量(let-变量)

我这个人,写文章或者说心得,不喜欢直接抄官网上面的东西,实在是没啥意思。我还是喜欢用我的大白话来写文章。今天这个关于模板输入变量的这个我今天啃官网啃了许久,总算是初步的了解了是啥东西。【相关教程推荐:《angular教程》】

那么模板输入变量到底是什么

我想研究这玩意的起因在于之前我使用ng-zorro的时候,用到了它的分页组件Pagination(官网链接)。这其中有一个自定义上一页下一页模板的功能。代码的话如下:

@Component({
  selector: 'nz-demo-pagination-item-render',
  template: `
    <nz-pagination [nzPageIndex]="1" [nzTotal]="500" [nzItemRender]="renderItemTemplate"></nz-pagination>
    <ng-template #renderItemTemplate let-type let-page="page">
      <ng-container [ngSwitch]="type">
        <a *ngSwitchCase="'page'">{{ page }}</a>
        <a *ngSwitchCase="'prev'">Previous</a>
        <a *ngSwitchCase="'next'">Next</a>
        <a *ngSwitchCase="'prev_5'"><<</a>
        <a *ngSwitchCase="'next_5'">>></a>
      </ng-container>
    </ng-template>
  `
})
export class NzDemoPaginationItemRenderComponent {}

看完这个之后我就很是疑惑,这个let是啥,为啥let-跟上一个变量之后就有值了呢?然后我就开始在官网中找这个let是什么东西。最终,我在主要概念-指令-结构性指令微语法一节找到了关于let的说明。官网描述:微语法。

在下面还有一段简短的说明:

模板输入变量(Template input variable)

模板输入变量是这样一种变量,你可以在单个实例的模板中引用它的值。 这个例子中有好几个模板输入变量:heroiodd。 它们都是用 let 作为前导关键字。

……

你使用 let 关键字(如 let hero)在模板中声明一个模板输入变量。 这个变量的范围被限制在所重复模板的单一实例上。 事实上,你可以在其它结构型指令中使用同样的变量名。

……

官网还是一如既往的不说人话,短短几句话就给你介绍完了,也不告诉你这玩意怎么用,也不告诉你let声明的变量的值到底是从哪里来的。我越看越气,得,官网你不说,我自己找。

先说一个粗略的结论:let声明的变量是这个template模板的上下文对象中的变量。不然为啥叫模板输入变量呢。在*ngFor内幕这小节中,我们了解到了其内幕,结构性指令其实就是将宿主元素包裹在一个<ng-template></ng-template>中,然后在这个模板上将*ngFor中的表达式解析成一个个的let模板输入变量和这个指令需要传入的值。由于模板中代码并不会直接被渲染成视图,所以一定需要某种方法来使模板变成视图。我们的结构性指令就是干这个事的,就是将我们的模板变成视图。

*ngFor的官方示例代码如下:

//解析前的模板
<div *ngFor="let hero of heroes; let i=index; let odd=odd; trackBy: trackById" [class.odd]="odd">
  ({{i}}) {{hero.name}}
</div>
//angular解析后的模板
<ng-template ngFor let-hero [ngForOf]="heroes" let-i="index" let-odd="odd" [ngForTrackBy]="trackById">
  <div [class.odd]="odd">({{i}}) {{hero.name}}</div>
</ng-template>
  • 在此提示一下,所谓的宿主元素就是指令所在的那个元素,像上面的例子中,div就是*ngFor指令的宿主元素。
  • trackBy这个类似于vue和react中的key。可以给模板一个标识,使其重新渲染的时候性能开销降低一些。

然后我们再看官网说的:

  • let-ilet-odd 变量是通过 let i=indexlet odd=odd 来定义的。 Angular 把它们设置为上下文对象中的 indexodd 属性的当前值。
  • 这里并没有指定 let-hero 的上下文属性。它的来源是隐式的。 Angular 将 let-hero 设置为此上下文中 $implicit 属性的值, 它是由 NgFor 用当前迭代中的英雄初始化的。

看完这段描述,我们可以得知:angular为这个模板设置了上下文对象。但是我们看不到这个过程,因为这是在ngFor的源码内部实现的。并且这个上下文对象具备indexodd属性,并且包含一个$implicit(implicit:含蓄的;不直接言明的)的属性。那么我们推断这个上下文对象至少有以下几个属性:

{
    $implicit:null,
    index:null,
    odd:null,
}

那么我们声明let变量的本质其实就是声明一个变量获取上下文对象中的同名属性的值let-hero不进行任何赋值的话,hero默认等于$implicit的值。无论是有多少个let-alet-blet-c还是let-me。声明的这个变量的值都等于$implicit的值。而我们进行赋值过的,比如let-i = "index"i的值就等于这个上下文对象中的index属性对应的值。

上下文对象是如何设置的

当我们知道这个上下文对象是什么了,就该想这个上下文对象是怎么设置的了

结构性指令这一节当中,我们跟着官方示例做了一遍这个自定义结构性指令(如果还没有做的话,建议先跟着做一遍)。在UnlessDirective这个指令中,其构造器constructor声明了两个可注入的变量,分别是TemplateRefViewContainerRef。官网给的解释我觉得太过晦涩难懂,我这里给出一下我自己的理解:TemplateRef代表的是宿主元素被包裹之后形成的模板的引用。而ViewContainerRef代表的是一个视图容器的引用。那么问题来了,这个视图容器在哪儿呢?我们在constructor构造器中打印一下ViewContainerRef。打印结果如图:

然后我们点进去这个comment元素。发现其就是一个注释元素。如图所示:

其实我也不是很确定这个视图容器到底是不是这个注释元素。但是毋庸置疑的是,视图容器和宿主元素是兄弟关系,紧挨着宿主元素。我们可以使用ViewContainerRef中的createEmbeddedView() 方法(Embedded:嵌入式,内嵌式),将templateRef模板引用传入进去,创建出来一个真实的视图。由于这个视图是被插入到视图容器ViewContainerRef中了,所以又叫内嵌视图。那么这又和我们的上下文对象有什么关系呢?其实createEmbeddedView这个方法不止一个参数,其第二个参数就是给我们的模板设置上下文对象的。API的详情介绍请看createEmbeddedView这个API的详情。

就这样。我们就可以将上下文对象塞入模板中了,这样的话我们也可以直接使用let声明变量的方法来使用这个上下文对象了。

自定义一个简单的类*ngFor指令——appRepeat

那么我们知道是如何设置的了,那么我们就来验证一下是否是对的。接下来,我们仿照ngfor的功能,自己写一个简单的渲染指令。

首先我们定义一个指令:RepeatDirective。代码如下:

@Directive({
  selector: '[appRepeat]',
})
export class RepeatDirective {
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef,
  ) { }

  @Input() set appRepeatOf(heroesList: string[]) {
    heroesList.forEach((item, index, arr) => {
      this.viewContainer.createEmbeddedView(this.templateRef, {
        //当前条目的默认值
        $implicit: item,
        //可迭代对象的总数
        count: arr.length,
        //当前条目的索引值
        index: index,
        //如果当前条目在可迭代对象中的索引号为偶数则为 true。
        even: index % 2 === 0,
        //如果当前条目在可迭代对象中的索引号为奇数则为 true。
        odd: index % 2 === 1,
      });
    });
  }
}

然后我们将其导入NgModule中,这个过程就省略不写了。然后我们在组件中使用一下这个指令:

@Component({
  selector: 'app-structural-likeNgFor-demo',
  template: `
    <h2>原神1.5版本卡池角色</h2>
    <h4>自定义ngFor(appRepeat)</h4>
    <ul>
      <li *appRepeat="let h of heroesList;let i = index;let even = even">
        索引:{{i}} -- {{h}} -- 索引值是否是偶数:{{even.toString()}}
      </li>
    </ul>
    <h4>真正的ngFor</h4>
    <ul>
      <li *ngFor="let h of heroesList;let i = index;let even = even">
        索引:{{i}} -- {{h}} -- 索引值是否是偶数:{{even.toString()}}
      </li>
    </ul>
  `,
})
export class StructuralLikeNgForDemoComponent {
  public heroesList: string[] = ['钟离', '烟绯', '诺艾尔', '迪奥娜'];
}

在这里需要注意的是指令中的appRepeatOf不是乱写的。在微语法的解析过程中let h of heroesList中的of首先首字母会变成大写的,变成Of。然后在给它加上这个指令的前缀,也就是appRepeat。组合起来就是appRepeatOf了。由它来接收一个可迭代的对象。

最后显示的效果图:

运行结果的话和*ngFor没有区别。但是功能肯定是欠缺的,如果有能力的小伙伴可以去阅读*ngFor的源码:*ngFor的源码。

总结

通过这篇文章,我们知道了let-变量这个模板输入变量是通过模板的上下文对象中定义并获取值的。然后想要设置上下文对象的话需要通过createEmbeddedView方法的第二个参数来设置。

结语

但是我总觉得了解的还不够透彻,我总觉得设置模板的上下文对象可能不只是createEmbeddedView这一种方法,但是我并没有找到其他的方法。如果各位小伙伴有知道其他的方法可以留言告诉我。

参考资料:

这篇文章灵感来自:Angular 实现一个”repeat” 指令

转载地址:https://juejin.cn/post/6956466729891561503

更多编程相关知识,请访问:编程入门!!

以上就是聊聊Angular中的模板输入变量(let-变量)的详细内容,更多请关注其它相关文章!