前言

Angular 在 v18.1 推出了 Template 新语法 @let

这个 @let 和上一篇教的 Control Flow @if, @for, @swtich, @defer 语法上类似,但是用途却差很多。

如果要给它们分类的话,@if @for @switch 可以算一类,@defer 独立一类,@let 又是独立一类,总之大家不要混淆就是了。

没有 @let 的日子

我们先来看看没有 @let 的日子里,我们都遇到了哪些不方便。

Long path access

App 组件

export class AppComponent {
person = signal({
name: 'Derrick',
address: {
country: 'Malaysia',
state: 'Johor',
postalCode: '81300'
}
})
}

有一个 person 对象,它里面又有一个 address 对象。

App Template

<p>{{ person().name }}</p>
<p>{{ person().address.country }}</p>
<p>{{ person().address.state }}</p>
<p>{{ person().address.postalCode }}</p>

上述代码最大的问题就是 person().address 一直重复,代码很长,很丑。

倘若在 JS 里的话,我们一定会 declare 一个 variable 把 address 对象装起来,像这样访问

const address = person.address;
console.log(address.country);
console.log(address.state);
console.log(address.postalCode);

没有一直重复 person.address 干净多了。

但是在 Template 里,我们无法 declare variable,能 declare variable 的只有组件

export class AppComponent {
person = signal({
name: 'Derrick',
address: {
country: 'Malaysia',
state: 'Johor',
postalCode: '81300'
}
});
address = computed(() => this.person().address);
}

App Template

<p>{{ person().name }}</p>
<p>{{ address().country }}</p>
<p>{{ address().state }}</p>
<p>{{ address().postalCode }}</p>

虽然 path 是短了,但是组件却为了 View 而多了一个 property,这样的职责分配合理吗?

再说,如果是在 @for 里面

@for (person of people(); track person.name) {
<p>{{ person.name }}</p>
<p>{{ person.address.country }}</p>
<p>{{ person.address.state }}</p>
<p>{{ person.address.postalCode }}</p>
}

组件 property 也无法解决这个问题丫。

the hacking way

有些人会用 hacking way 来做到这一点,像这样

@for (person of people(); track person.name) {
<p>{{ person.name }}</p> @if (person.address; as address) {
<p>{{ address.country }}</p>
<p>{{ address.state }}</p>
<p>{{ address.postalCode }}</p>
}
}

虽然 path 是短了,但是 @if 是这样用的吗?这种不顺风水的用法,随时会掉坑里的,忌讳啊。

Async pipe

App 组件

export class AppComponent {
value$ = of('hello world');
}

有一个 value stream。

App Template

<header>
<p>{{ value$ | async }}</p>
</header>
<main>
<p>body</p>
</main>
<footer>
<p>{{ value$ | async }}</p>
</footer>

我要在 header 和 footer 显示这个 value。

上述代码有两个问题:

  1. 两次 pipe async 会导致 value stream 被 subscribe 两次。

    如果这个 value 需要发 ajax,那它就会发 2 次 ajax。

  2. 不管是使用什么 pipe 都好,每一次使用 value 都要重复同样的 pipe,这就不合理。

    而,倘若我们把职责交给组件,那组件就需要使用 pipe,这样也不合理。

    总之,怎样都不合理,这就是一个设计失误。

总结

Angular 对 Template 限制太多了,以至于许多简单的 View 逻辑无法在 Template 里完成,必须转交给不合适的组件,或者大费周章的指令去完成。

这也是为什么这个 issue 能常年霸榜

幸好,在经历了 2657 个日夜,Angular 团队终于解决了上述这些问题。这就是本篇的主角 -- @let。

@let 的使用方式

@let 允许我们在 Template 里 declare variables,就这么一个简简单单的功能。

@let value = 'hello world';
<h1>{{ value }}</h1>

效果

@let 的语法

@let 开头

跟着一个 variable name

然后等于 =

然后一个 Angular Template 支持的 expression。

最后 ends with ; 分号

@let 解决 long path access 问题

@for (person of people(); track person.name) {
<p>{{ person.name }}</p> @let address = person.address; <p>{{ address.country }}</p>
<p>{{ address.state }}</p>
<p>{{ address.postalCode }}</p>
}

瞬间干净了

@let 解决 pipe async 问题

@let value = value$ | async;

<header>
<p>{{ value }}</p>
</header>
<main>
<p>body</p>
</main>
<footer>
<p>{{ value }}</p>
</footer>

只有一次 pipe async,所以只会 subscribe 一次,完美

@let 的特性

@let 的语法和意图都很简单,所以使用上是没有什么问题的,只是有一些特性大家还是需要知道,避免掉坑。

Immutable

虽然它叫 @let,但是它是不可以被赋值的。

下面这样直接报错

<h1>Hello World</h1>

@let value = 'Hello World';
<p>{{ value }}</p> <button (click)="value = 'new value'">update value</button>

不要问我为什么 svelte 叫 @const ‍♂️

Angular 团队给出的解释

const 代表这个值永远不会变,但是如果我们这样 declare

@let fullName = firstName() + ' ' + lastName();

每一次 refreshView 后,fullName 的 value 都有可能不一样,所以他们认为叫 @let 比较合理。

关键不是我们能不能改变这个 variable,而是这个 variable 的值会不会被改变,大家看的角度不同。

@let = signal 可以赋值?

如果 @let 的值是 signal,我们当然可以调用 WritableSignal.set 方法给 signal 赋值。

因为这不是赋值给 @let 而是赋值给 signal。

App 组件

export class AppComponent {
$value = signal('default value');
}

App Template

@let value = $value;
<p>{{ value() }}</p>
<button (click)="value.set('new value')">update value</button>

效果

但是!请不要妄想利用这个特性让 @let 实现 mutable。

App 组件

export class AppComponent {
signal = signal;
}

把 signal 函数传给 Template

App Template

@let value = signal('default value');
<p>{{ value() }}</p>
<button (click)="value.set('new value')">update value</button>

表明上看,只要把 signal 函数传给 Template,在 Template @let 时使用 signal,这样 @let 就好像等同于 mutable 了。

效果

虽然成功显示了 default value,但是点击 button 并不会修改成 new value。why?

原理我们留到下面逛源码时解答。

Duplicated declare

variable name 不可以重复 declare,下面这样直接报错。

@let value = 'value';
@let value = 'new value';

也不可以撞 Template Variables

@let value = 'hello world';
<input #value>
{{ value }}

也是直接报错。

也不应该撞组件属性

export class AppComponent {
value = 'component value';
}
@let value = 'hello world';

{{ value }}

虽然它不会报错,优先显示的是 @let 的值,但是撞名字始终不是好现象,能免则免吧。

Template as scope

@let 是有 scope 概念的,每一个 Template 都可以 declare 属于自己 scope 的 @let

@let value1 = 'one';

<ng-template #template>
@let value2 = 'two'; <p>{{ value1 }}</p>
<p>{{ value2 }}</p>
</ng-template> <ng-container [ngTemplateOutlet]="template" />

效果

里面有几个知识点:

  1. 子层可以访问父层的 @let

    比如,在 ng-template 里可以使用 value1。

    当然,反过来就不行,ng-temaplte 外不可以使用 value2。

  2. 父层和子层可以 declare 相同的 variable name,子层会胜出。

    @let value1 = 'one';
    <ng-template #template>
    @let value1 = 'two';
    <p>{{ value1 }}</p> <!-- will be two -->
    </ng-template>

element 不是 scope

只有 Template 才是 scope,element 不是

<div class="container">
@let value = 'Hello World';
<p>{{ value }}</p>
</div> <div class="container">
@let value = 'Hello World';
<p>{{ value }}</p>
</div>

上面这样 @let value 就 duplicated 了,因为 div.container 不是 scope。

When will @let be updated?

@let 属于 LView,当 LView refresh 时,它就会被刷新。下一 part 逛源码时会看到这一点。

逛一逛 @let 源码

App Template

@let value = 'Hello World'
<p>{{ value }}</p>

compile

yarn run ngc -p tsconfig.json

app.component.js

在 renderView 阶段,有一个 ɵɵdeclareLet。

在 refreshView 阶段 declare variable with 'Hello World',然后把 variable 传给 ɵɵtextInterpolate。

所有工作 compiler 做完了。runtime 执行 refreshView 就可以了。

我们这个例子太过简单,ɵɵdeclareLet 其实可有可无,可以看到 refreshView 里的代码已经足够渲染出正确的画面了。

我们来一个比较复杂的例子

@let name = person().name;
<p>{{ name }}</p> <ng-template>
<p>{{ name }}</p>
</ng-template>

主要是多了一个 ng-template,在 ng-template 内使用到了父层的 @let name。

app.component.js

首先是 renderView 阶段 ɵɵdeclareLet 函数的源码在 let_declaration.ts

store 函数的源码 storage.ts

renderView 阶段主要做了两件事,

第一件是创建 TNode 插入到 App TView.data 里。

第二件是插入一个初始值到 App LView 里。

接着到 refreshView 阶段

ɵɵstoreLet 函数的源码在 let_declaration.ts

这时会把 App 组件实例的值写入 LView @let 的位置。

我们看回 App template 方法 refreshView 的部分

name_r2 的 value 就是 'Derrick',然后它被传给了 ɵɵtextInterpolate。

也就是说,到目前位置,上面记入到 TView LView 里的资料都没有人在用。

正真会从 LView 里取出 @let value 来使用的人是子层 ng-template 里的 {{ name }}

我们看看它 compile 后的 template 方法。

renderView 阶段和 @let 毫无关系。

refreshView 阶段,ɵɵnextContext 函数我们以前讲解过。

它主要是设置了当前 LView (ng-template LView) 的 contextLView

这个 contextLView 就是 ng-template LView 的 declaration view 也就是 App LView。

接着是 ɵɵreadContextLet 函数,它的源码在 let_declaration.ts

简单说就是去父层 LView 拿出 @let value。

总结

  1. 在 App Template 里 declare @let。

  2. renderView 阶段

    它会创建 @let TNode,存放在 App TView.data 里。

    它会创建 @let initial value,存放到 App LView 里。

  3. refreshView 阶段

    它会从 App 实例获取到 @let value,然后 update App LView 里的 @let value。(注:从这里也可以看出,假如 @let = complex formula 是会影响性能的,因为每一次 refreshView 它都会重跑 formula)

  4. LView 里的 @let value 并不是给当前 LView binding 使用的,当前 binding 会直接用 const variable。

  5. LView 里的 @let 是给子层 LView binding 使用的。

    子层 LView 在 refreshView 时会透过 contextLView 来到父层 LView,然后取出 @let value。

解答 @let = signal 可以赋值?

上一 part 我们留了一道问题。

App 组件

export class AppComponent {
signal = signal;
}

把 signal 函数传给 Template

App Template

@let value = signal('default value');
<p>{{ value() }}</p>
<button (click)="value.set('new value')">update value</button>

为什么点击 button 后没有变成 'new value'?

其实很好理解:

  1. 当 refreshView 的时候 @let value 会获得一个新的 signal,value 是 'default value'。

  2. {{ value() }} 会渲染出 'default value'。

  3. 点击以后 value signal 会被赋值 'new value'。

  4. 同时会触发新一轮的 refreshView。

  5. 结果新一轮的 refreshView 又重新给 @let value 一个新的 signal,value 是 'default value'。

  6. 这样就轮回了,所以 {{ value() }} 永远不可能渲染出 'new value'。

总结

本篇简单的介绍了 Angular v18.1 推出的 @let 新模板语法。它主要的功能是让我们能在 Template declare variables,这能让代码变得更干净,职责管理更分明。

它主要是靠 compiler 实现的,runtime 中,它只是在 LView 做一些记入,然后使用它,这样而已。

目录

上一篇 Angular 18+ 高级教程 – Component 组件 の Control Flow

下一篇 Angular 18+ 高级教程 – NgModule

想查看目录,请移步 Angular 18+ 高级教程 – 目录

喜欢请点推荐,若发现教程内容以新版脱节请评论通知我。happy coding

Angular 18+ 高级教程 – Component 组件 の @let Template Local Variables的更多相关文章

  1. 小程序-组件component和模版template的选择和使用

    小程序提供了组件component和模版template那什么时候 选择哪一个使用呢?我总结了一下 template主要是模版,对于重复的展示型模块进行展示,其中调用的方法或者数据data都是需要引用 ...

  2. 微信小程序template模板与component组件的区别和使用

    前言: 除了component,微信小程序中还有另一种组件化你的方式template模板,这两者之间的区别是,template主要是展示,方法则需要在调用的页面中定义.而component组件则有自己 ...

  3. angular2 学习笔记 ( Component 组件)

    refer : https://angular.cn/docs/ts/latest/guide/template-syntax.html https://angular.cn/docs/ts/late ...

  4. vue 基础-->进阶 教程(3):组件嵌套、组件之间的通信、路由机制

    前面的nodejs教程并没有停止更新,因为node项目需要用vue来实现界面部分,所以先插入一个vue教程,以免不会的同学不能很好的完成项目. 本教程,将从零开始,教给大家vue的基础.高级操作.组件 ...

  5. 一篇文章看懂angularjs component组件

     壹 ❀ 引 我在 angularjs 一篇文章看懂自定义指令directive 一文中详细介绍了directive基本用法与完整属性介绍.directive是个很神奇的存在,你可以不设置templa ...

  6. NgRx/Store 4 + Angular 5使用教程

    这篇文章将会示范如何使用NgRx/Store 4和Angular5.@ngrx/store是基于RxJS的状态管理库,其灵感来源于Redux.在NgRx中,状态是由一个包含action和reducer ...

  7. Angular CLI 使用教程指南参考

    Angular CLI 使用教程指南参考 Angular CLI 现在虽然可以正常使用但仍然处于测试阶段. Angular CLI 依赖 Node 4 和 NPM 3 或更高版本. 安装 要安装Ang ...

  8. angular里使用vue/vue组件怎么在angular里用

    欢迎加入前端交流群交流知识&&获取视频资料:749539640 如何在angularjs(1)中使用vue参考: https://medium.com/@graphicbeacon/h ...

  9. angularjs中directive指令与component组件有什么区别?

     壹 ❀ 引 我在前面花了两篇博客分别系统化介绍了angularjs中的directive指令与component组件,当然directive也能实现组件这点毋庸置疑.在了解完两者后,即便我们知道co ...

  10. vue高级进阶( 三 ) 组件高级用法及最佳实践

      vue高级进阶( 三 ) 组件高级用法及最佳实践 世界上有太多孤独的人害怕先踏出第一步. ---绿皮书 书接上回,上篇介绍了vue组件通信比较有代表性的几种方法,本篇主要讲述一下组件的高级用法和最 ...

随机推荐

  1. 实用!一键生成数据库文档的神器,支持MySQL/SqlServer/Oracle多种数据库

    Screw(螺丝钉)是一款简洁好用的数据库表结构文档生成工具,它的特点是:简洁.轻量.设计良好.多数据库支持.多种格式文档.灵活扩展以及支持自定义模板,对于有经常要进行数据库设计.评审.文档整理等需求 ...

  2. Microsoft宣布将在开发人员会议上专注于.NET Aspire

    2024年7月15日微软宣布,其开发执行团队将在下个月的开发者大会上聚焦于使用 .NET Aspire 的云原生开发,以及结合人工智能的"现代 SQL"在 Microsoft Fa ...

  3. Github关于PAT(Personal Access Token)

    Github关于PAT(Personal Access Token) 创建个人访问令牌 您应该通过命令行或 API 创建个人访问令牌来代替密码. 注意: 如果您在命令行上使用 GitHub CLI 向 ...

  4. redis数据结构:RedisObject,SkipList,SortedSet

    1.RedisObject对象 redis中任何KV都会被封装为RedisObject对象,也叫做Redis对象 2.SkipList 跳表 元素按照升序排列存储,是有序的双向链表 节点可以有多个指针 ...

  5. 微服务:openFeign

    openFeign是一个声明式http客户端.作用:基于springMVC常见注解,帮我们更优雅的实现http请求 引入依赖 <!--openFeign--> <dependency ...

  6. linux的一些常用端口

    hdfs:9870 yarn:8088 sparkMaster的端口是:8080 worker的端口是:8081 历史服务器的默认端口是: 18080

  7. M1安装Anaconda遇到的问题

    1. 安装时报错:"Anaconda3 is already installed in /opt/anaconda3. Use 'conda update anaconda3' to upd ...

  8. 对精确率(P)、召回率(R)、F1值的理解以及对应的实现

    对精确率.召回率.F1值的理解 算法理解 在机器学习中,P.R和F1值在各种评测中很常见,那么到底什么是P.R.F1值呢,怎么理解呢,困扰了很多人,下面给我对P.R.F1值的理解, 首先,我们先看一个 ...

  9. 视频分享---------《无人机背后的PID控制》

    在B站上看到有讲无人机自动控制方面的视频,感觉不错,分享下: https://www.bilibili.com/video/BV1aW411E7Qq/?spm_id_from=333.788.vide ...

  10. 东北某海滨城市的某高校的某分校区的校园网登录程序,(python3, 模拟浏览器的登入方式)

    前些年写过这个登录程序,过了几年系统有所升级,于是做了一定的修改. 新版本的校园网登录程序依然是模拟浏览器去登录校园网. Python3.7编写. #encoding:UTF-8 from urlli ...