前言
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 😊💻