Angular 18+ 高级教程 – Component 组件 の @let Template Local Variables
cnblogs 2024-06-28 08:11:00 阅读 67
前言
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。
上述代码有两个问题:
两次 pipe async 会导致 value stream 被 subscribe 两次。
如果这个 value 需要发 ajax,那它就会发 2 次 ajax。
不管是使用什么 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 的语法和意图都很简单,所以使用上是没有什么问题的,只是有一些特性大家还是需要知道,避免掉坑。
Must delcare in Template top layer
下面这样直接报错
<div class="container">
@let value = 'Hello World';
<p>{{ value }}</p>
</div>
因为 @let 只能 declare 在 Template 的最上层。但不要误会哦,最上层的意思不是说要在第一行,只要不是被 element wrap 起来就可以了。
下面这样是 ok 的
<h1>Hello World</h1>
@let value = 'Hello World';
<p>{{ value }}</p>
Immuable
虽然它叫 @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();
每一次 renderView 后,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 }}
最终会使用 @let 还是 input ref 我不晓得,目前18.1.0-next.4好像有Bug。
也不应该撞组件属性
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" />
效果
里面有几个知识点:
子层可以访问父层的 @let
比如,在 ng-template 里可以使用 value1。
当然,反过来就不行,ng-temaplte 外不可以使用 value2。
父层和子层可以 declare 相同的 variable name,子层会胜出。
@let value1 = 'one';
<ng-template #template>
@let value1 = 'two';
<p>{{ value1 }}</p> <!-- will be two -->
</ng-template>
注:目前18.1.0-next.4 好像有 Bug。
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。
总结
在 App Template 里 declare @let。
renderView 阶段
它会创建 @let TNode,存放在 App TView.data 里。
它会创建 @let initial value,存放到 App LView 里。
refreshView 阶段
它会从 App 实例获取到 @let value,然后 update App LView 里的 @let value。(注:从这里也可以看出,假如 @let = complex formula 是会影响性能的,因为每一次 refreshView 它都会重跑 formula)
LView 里的 @let value 并不是给当前 LView binding 使用的,当前 binding 会直接用 const variable。
- 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'?
其实很好理解:
当 refreshView 的时候 @let value 会获得一个新的 signal,value 是 'default value'。
{{ value() }} 会渲染出 'default value'。
点击以后 value signal 会被赋值 'new value'。
同时会触发新一轮的 refreshView。
结果新一轮的 refreshView 又重新给 @let value 一个新的 signal,value 是 'default value'。
这样就轮回了,所以 {{ 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😊💻
上一篇: sessionStorage 能在多个标签页之间共享数据吗?
下一篇: 前端给后端传数据的几种方式
本文标签
深入理解 Angular Angular 教程 Angular 18 Angular 最新 (新) Angular 18+ 高级教程
声明
本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。