Signals • Overview • Angular
Signal是什么
Angular 从 16 开始,引入了 signal。
Angular Signals 是一个信号体系,能够细粒度地跟踪你的状态在整个应用中的使用方式和位置,从而使框架能够优化更新频率。
翻译成白话来说就是Signal可以定义一个状态,Angular会知道这个状态在系统中所有被使用的位置,当这个状态变化的时候,会更新页面。
Signal都有哪些功能
基础使用
默认情况下 Signal 的类型是 WritableSignal,也就是可写的 Signal。
const count: WritableSignal<number> = signal(0);
// Signals are getter functions - calling them reads their value.
console.log('The count is: ' + count());
// Output 0
// Setting the value directly
count.set(1);
// Output 1
// update by prev value
count.update(value => value + 1);
// Output 2
定义只读的 Signal
通过 asReadonly() 可以定义 signal 为只读的,此时 Signal 的类型为 Signal<number>, 它没有 set 和 update 属性,无法修改值。
readonly pageSize = signal(10).asReadonly();
Or
readonly pageSize:Signal<number>;
constructor() {
// You can get value from other sync code
this.pageSize = signal(value).asReadonly();
}
如果你需要从某个异步操作的结果设置默认值, 就只能放弃 readonly 修饰符。
pageSize!: Signal<number>;
constructor() {
Promise.resolve().then(() => {
this.pageSize = signal(10).asReadonly();
});
}
这种写法,你同样无法调用 this.pageSize 的 set 和 update 方法更新值,但是无法避免在代码中直接修改 pageSize 为新的 signal 对象,这点比较难受。
/// 在 xxx function 内部直接修改引用
this.pageSize = signal(10).asReadonly();
自定义 Compare 函数
你可以自定义一个返回 bool 值的函数,它会被用于检查新旧值是否不同。
data = signal(
{ id: 1, name: 'zhangsan' },
{
equal: (prev, cur): boolean => {
return prev.id === cur.id;
},
}
);
上面的例子中,只要对象的 id 是不变的,那么 Angular 就会始终认为 data 没有发生变化。默认情况下,Angular 只会比较引用是否相等。
计算属性 Computed
const count: WritableSignal<number> = signal(0);
const doubleCount: Signal<number> = computed(() => count() * 2);
当 count 变化时,angular 会自动更新 doubleCount, 并且计算属性是只读的。
计算属性是惰性并且支持缓存的,当你访问 doubleCount 时,计算函数才会被执行,并且缓存执行结果,在count 值不发生变化的前提下,后续访问 doubleCount 都会直接返回之前缓存的值。
计算属性还会根据代码逻辑动态推导应该追踪哪个 signal, 在下面的例子中,
- 如果
showCount的初始值为 true, 那么逻辑会读取count的值,把count也标记成依赖项,此时当showCount和count变化时,都会导致缓存的值无效,从而触发conditionalCount被重新计算. - 如果
showCount的初始值为 false, 代码逻辑不会读取count, 即使count值发生变化,也不会触发conditionalCount重新计算。 - 如果刚开始为 true,
count会被标记为依赖项, 之后如果把showCount的值修改为 false, 那么count将被移出依赖项,依赖项是动态推导的。
const showCount = signal(false);
const count = signal(0);
const conditionalCount = computed(() => {
if (showCount()) {
return `The count is ${count()}.`;
} else {
return 'Nothing to see here!';
}
});
我们应该尽量使用 computed 替换 getter
因为 compute 有缓存机制,而 getter 没有。而且你很难知道 getter 的依赖关系何时发生变化,Signal 则要求你显式的更新状态(call set() or update())。
Effects
每当一个或多个依赖的信号值发生变化时, Effects 都会被调用,与 computed 一样,它会动态跟踪依赖关系。但 effects 至少会运行一次(在初始化阶段)。
默认情况下,您只能在注入上下文(您可以访问注入函数)中创建 effect()。满足此要求的最简单方法是在组件、指令或服务构造函数中调用 effect
@Component({...})
export class EffectiveCounterComponent {
readonly count = signal(0);
constructor() {
// Register a new effect.
effect(() => {
console.log(`The count is: ${this.count()}`);
});
}
}
另外你还可以给 effects 分配一个属性
@Component({...})
export class EffectiveCounterComponent {
readonly count = signal(0);
private loggingEffect = effect(() => {
console.log(`The count is: ${this.count()}`);
});
}
除此之外,如果需要在构造函数之外创建 effects,可以通过配置传递 Injector 给 effects
@Component({...})
export class EffectiveCounterComponent {
readonly count = signal(0);
constructor(private injector: Injector) {}
initializeLogging(): void {
effect(() => {
console.log(`The count is: ${this.count()}`);
}, {injector: this.injector});
}
}
你创建的 effects 会随着组件被销毁时一起被销毁,如果你想要手动控制,可以使用 .destroy()手动销毁。
untracked
如果你不想某个信号被追踪,可以使用 untracked 包裹它,这样即使该信号的值发生变化,也不会触发 effect 调用。
effect(() => {
console.log(`User set to ${currentUser()} and the counter is ${untracked(counter)}`);
});
effect(() => {
const user = currentUser();
untracked(() => {
// If the `loggingService` reads signals, they won't be counted as
// dependencies of this effect.
this.loggingService.log(`User set to ${user}`);
// The counter will not be tracked.
console.log(`User set to ${user} and the counter is ${counter()}`);
});
});
cleanup
创建 effects 时,函数可以选择接受 onCleanup 函数作为其第一个参数。此 onCleanup 函数允许您注册一个回调,该回调在 effects 被销毁时调用。
effect((onCleanup) => {
const user = currentUser();
const timer = setTimeout(() => {
console.log(`1 second ago, the user became ${user}`);
}, 1000);
onCleanup(() => {
clearTimeout(timer);
});
});
effects 的使用场景
- 记录显示的数据及其变化情况,用于分析或作为调试工具。
- 保持数据与 window.localStorage 同步。
- 添加无法用模板语法表达的自定义 DOM 行为。
- 对 、图表库或其他第三方 UI 库执行自定义渲染。
- 用于异步场景
什么时候不能用 effects?
如果你的
effects代码会修改 signal 的状态,那么建议不要使用effect。这可能会导致 ExpressionChangedAfterItHasBeenChecked 错误、无限循环更新或不必要的更改检测周期。 由于这些风险,Angular 默认会阻止您在effects中设置signal的值。如果一定要改,可以在创建effets时设置allowSignalWrites标志来启用它。相反,可以使用
computed来模拟依赖于其他状态的状态。
tips:很多博客都在说一定避免使用 effects,但在很多场景下使用 effects 的代码比 computed 具有更好的可读性,感兴趣的可以查看这篇博客,写的很好,而且给出了一些适合使用 effects 的场景。不要盲目的使用 effects,但也不要直接摒弃它。
Angular's effect(): Use Cases & Enforced Asynchrony - Rainer Hahnekamp
使用基于 signal 的全新 input, output, viewQuery
在之前的版本,我们定义输入输出属性和视图查询,都是使用基于装饰器的: @Input, @Output, @ViewChild等,而现在你可以使用基于 Signal 实现的全新API。
Signal Input
默认情况下,Input 属性是可选的,你可以显式的指定一个初始值,否则 Angular 将默认使用使用 undefined。
import {Component, input} from '@angular/core';
@Component({...})
export class MyComponent {
// optional
firstName = input<string>(); // InputSignal<string|undefined>
// set initial value
age = input(0); // InputSignal<number>
// required
lastName = input.required<string>(); // InputSignal<string>
}
你也可以给 input 属性起别名
// child component
gender = input(18, { alias: 'studentGender' });
// parent component
<child [studentGender]="20"></child>
当然现在的 input 依然带有严格的类型检查。
signal input 是只读的,它的类型继承自 Signal
export class InputSignal<T> extends Signal<T> { ... }`.
所以它也支持计算属性
age = input(0);
ageText = computed(() => `The age of the students is ${this.age()}`);
但使用 computed 需要你重新定义一个属性,如果你不希望属性含义因为命名发生变化,想要使用原始属性名,或者需要对输入值进行强制解析和纠正,那么你可以配置 transform
class MyComp {
disabled = input(false, {
transform: (value: boolean|string) => typeof value === 'string' ? value === '' : value,
});
}
如果
transform会改变输入属性的含义,或者转换不是纯函数,则不应该使用transform。相反,对于具有不同含义的转换,请使用computed,对于不纯的代码,请使用effect,它在输入属性的值发生变化时运行。
双向绑定
双向绑定的写法也发生了变化,可以使用新的 model api。
@Component({ /* ... */ })
export class AppChild {
// Can be subscribed to using `(checkedChange)="handler()"` in the template.
checked = model(false);
}
上述代码,Angular会自动生成 checkedChange 事件,当 checked 的值发生变化,会自动 emit 最新的值。
使用时,
@Component({
/* ... */
// `value` is a model input.
// The parenthesis-inside-square-brackets syntax (aka "banana-in-a-box") creates a two-way binding
template: '<app-child [(checked)]="isChecked" />',
})
export class ParentComponent {
isChecked = signal(true);
}
注意这里直接用的 isChecked,而不是 isChecked()
Signal Output
output 的用法跟之前装饰器差不多,没什么特别的。
panelClosed = output<void>();
this.panelClosed.emit();
valueChanged = output<number>();
this.valueChanged.emit(1);
有个好玩的是 ouput 是可以通过 subscribe 订阅的,你可以直接在外部通过组件的实例去订阅它。
const someComponentRef: ComponentRef<SomeComponent> = viewContainerRef.createComponent(/*...*/);
someComponentRef.instance.someEventProperty.subscribe(eventData => {
console.log(eventData);
});
当 Angular 销毁带有订阅的组件时,它会自动清除事件订阅,当然你也可以手动取消订阅,因为它就是一个Observable 对象。
Signal View Query
整体的用法跟使用装饰器基本一致,只是语法的不同
child = viewChild(ChildComponent);
children = viewChildren(ChildComponent);
item = contentChild(CustomMenuItem);
items = contentChildren(CustomMenuItem);
但是基于 signal 的实现,让它可以结合 computed 使用
childText = computed(() => this.child()?.text);
itemTexts = computed(() => this.items().map(item => item.text));
由于目标组件可能被 @if 隐藏,导致无法找到该组件,因此 query 返回的值类型中会包含 undefined。
但是当你可以确保该组件始终存在时,可以使用 .required,此时返回的值类型就不会包含 undefined了。
header = viewChild.required(CustomCardHeader);
body = contentChild.required(CustomCardBody);
还有个以前没注意到的配置: descendants,ContentChild 只会找你的子组件,当你想找孙组件或者更深层次的组件时,就可以把 descendants 设为 true,这个配置同样也支持装饰器那种写法。
@Component({
selector: 'custom-expando',
/*...*/
})
export class CustomExpando {
toggle = contentChild(CustomToggle,{descendants: true});
}
@Component({
selector: 'user-profile',
template: `
<custom-expando>
<some-other-component>
<!-- custom-toggle 不会被发现,除非你配置了 descendants -->
<custom-toggle>Show</custom-toggle>
</some-other-component>
</custom-expando>
`
})
export class UserProfile { }
Angular 会按需延迟计算基于
signal的查询结果。这意味着除非你的代码显式的读取 Signal,否则不会收集查询结果。
迁移
在 Angular version 19中,全新的 input, output, queryView已经是稳定版本了, 并且 Angular 提供了脚本帮你从旧代码中迁移。
// 分别迁移
ng generate @angular/core:signal-input-migration
ng generate @angular/core:signal-queries-migration
ng generate @angular/core:output-migration
// 一起迁移
ng generate @angular/core:signals
或者在编辑器中手动转换。
linkedSignal
linkedSignal 的工作原理与 signal 类似,但 linkedSignal 不是传递默认值,而是传递一个计算函数,就像 computed 一样。当追踪的值改变时,linkedSignal 就会根据传递的计算函数,重新计算结果。
这里就不过多描述用法了,可以查看官网示例:Dependent state with linkedSignal • Angular ,目前属于开发者预览阶段,并不是稳定版本。
linkedSignal 的语法跟 computed 差不多,都接收一个箭头函数作为参数。官方的解释也是说 linkedSignal 会在依赖项发生变化时重新计算值。但它跟 computed 有一个最大的区别就是它是可写的,而计算信号是只读的。它返回的类似是 WritableSignal。
const shippingOptions = signal(['Ground', 'Air', 'Sea']);
const selectedOption = linkedSignal(() => shippingOptions()[0]);
console.log(selectedOption()); // 'Ground'
// linkedSignal 是可写的,可以 set 值
selectedOption.set(shippingOptions()[2]);
console.log(selectedOption()); // 'Sea'
// 当依赖项发生变化时,会重新根据传入的函数计算结果,所以此时读取它的值返回的是 Email
shippingOptions.set(['Email', 'Will Call', 'Postal service']);
console.log(selectedOption()); // 'Email'
它还支持:
- 自定义 compare 函数,用于检查依赖项是否发生变化,用法跟
effects的 compare 差不多。 - 可以获取到依赖项上一次的值和
linkedSignal前一次的计算结果,从而返回你期待的结果。
@Component({/* ... */})
export class ShippingMethodPicker {
shippingOptions: Signal<ShippingMethod[]> = getShippingOptions();
selectedOption = linkedSignal<ShippingMethod[], ShippingMethod>({
// 每当此值发生变化,就会调用用 computation 函数进行计算
source: this.shippingOptions,
computation: (newOptions, previous) => {
// 如果 newOptions 包含之前选中的选项,则保留该选择,否则返回第一个值
// previous包含两个属性,previous.source 是 source 的前一个值
// previous.value 是前一个计算结果
return newOptions.find(opt => opt.id === previous?.value?.id) ?? newOptions[0];
},
equal: (a, b) => a.id === b.id,
});
}
Resource - 将异步请求与 Signal 结合使用
大多数 Signal API 都是同步的 — signal、computed、input等。但是,应用程序通常需要处理异步数据。Resource 提供了一种将异步数据和 Signal 共用的办法。
你可以使用 Resource 执行任何类型的异步操作,最常见的用例是从服务器获取数据。下面的示例创建了一个 Resource 来获取一些用户数据。
userId = signal<number | undefined>(undefined);
fetchUserPromise = ({ id }: { id?: number }) => {
if (id === 1) {
return Promise.resolve({ firstName: 'John', id });
} else {
return Promise.resolve({ firstName: 'Doe', id });
}
};
// loader不接受 Observeable,使用该方法作为loader会报错。
fetchUserObservable = ({ id }: { id?: number }) =>
of({ firstName: 'John', id });
userResource = resource({
// 只要 userId 发生变化,就会调用 loader 重新请求值。
// 你也可以直接传递一个 computed属性给 request
request: () => ({ id: this.userId() }),
// 定义一个获取数据的异步 loader。
loader: ({ request }) => this.fetchUserPromise(request),
});
firstName = computed(() => this.userResource.value()?.firstName);
当 userId 发生变化时,会自动获取数据,除此之外你还可以通过 .reload() 手动获取。
this.userResource.reload();
loader 除了 request 之外,还有 previous, abortSignal 两个参数。
previous包含了上一次请求结果的状态的枚举值,注意是response status, 而不是response!一共有6个状态,成功,失败,加载中等等。abortSignal也并非给你提供了手动终止请求方法,而是让你可以订阅请求被终止的事件,获取被终止的原因等等。
除了 .value 外, Resource 实例本身还提供了一些类似于 isLoading,和 hasValue 的属性。
对这些感兴趣的可以查看官方文档:Async reactivity with resources • Angular
另外在实际使用中,发现一个问题,loader 允许的类型是 PromiseLike, 它居然不接受 Observable!
总的来说 Resource 给人一种半成品的感觉,不过目前仍然属于开发者预览阶段,后面应该会逐渐完善。
Signal 与 Rxjs 之间的转换
目前属于开发者预览阶段。
Observable to Signal
import { Component } from '@angular/core';
import { interval } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';
@Component({
template: `{{ counter() }}`,
})
export class Ticker {
counterObservable = interval(1000);
// initialValue 会在 Observable 没有发出值之前被使用,如果没有定义,默认返回 undefined
counter = toSignal(this.counterObservable, {initialValue: 0});
}
如果 Observable 发出错误,那么当你在读取 Signal 时,会抛出错误。
有一些 Observables 可以保证值是同步发出的,例如 BehaviorSubject,它拥有默认值。那么在这种情况下,可以指定 requireSync: true,这样你就不需要再设置 initialValue 了。
counter = toSignal(this.counterObservable, { requireSync: true });
toSignal 与 async 管道类似,它会立刻订阅 Observable, 当调用 toSignal 的组件或服务被销毁时,由 toSignal 创建的订阅会自动取消。如果你的 Observable 会自动完成,那么你可以通过配置 manualCleanup: true 来覆盖默认销毁策略。
另外 toSignal 默认情况下需要在注入上下文中运行。如果注入上下文不可用,您可以手动指定要使用的注入器,这点跟 effects 一致,没看懂的可以去看 effects 章节的开头部分。
Tips: toSignal 会创建一个订阅。您应避免对同一个 Observable 重复使用 toSignal,而应重用它返回的信号。
Signal to Observable
可以使用 toObservable 创建一个跟踪 Signal 值的 Observable。Signal 通过 effects 进行监控,当值发生变化时,该 effects 将值发送到 Observable。
import { Component, signal } from '@angular/core';
import { toObservable } from '@angular/core/rxjs-interop';
@Component(...)
export class SearchResults {
query: Signal<string> = signal(QueryService).query;
query$ = toObservable(this.query);
results$ = this.query$.pipe(
switchMap(query => this.http.get('/search?q=' + query ))
);
}
跟 toSignal 相同,toObservable 默认情况下也需要在注入上下文中运行。
在订阅时,第一个值(如果可用)会同步发出,而所有后续值都将异步发出。
与 Observable 不同的是即使多次更新信号的值,toObservable 也只会在信号稳定后发出该值。
const obs$ = toObservable(mySignal);
obs$.subscribe(value => console.log(value));
mySignal.set(1);
mySignal.set(2);
// 只有3会被log
mySignal.set(3);
扩展知识点
Signal 与 变更检测
Signal 的值发生变化时,Angular 会更新视图,实际上也是通过触发变更检测是实现的,可以看一下这个链接,当你使用 no ngzone 模式时,signal就不工作了。Angular 16 Signal update doesn't update view - Stack Overflow
针对 signal 触发的变更检测这里有必要举个例子解释一下。
假设有三个组件,分别是
signal-parent.component.tssignal.component.tssignal-child.component.ts
1. 正常 Case
我们在 app 的模板里面使用 signal-parent,然后在 signal-parent 中使用 signal,依此类推。
signal-parent 和 signal的代码是一致的,在 ngOnInit 中添加一个定时器,到时间后更新 name 属性为 xxx Name Changed,然后在模板中绑定 name 属性。 他们唯一的区别是 signal-child 和 signal-parent 定时器为 1s , signal 组件为 2s。
ngOnInit(): void {
setTimeout(() => {
this.name = 'Parent Name Changed';
}, 1000);
}
在正常情况下,2s后,三个组件的显示的 name 都会变成 name changed。
2. 把 Signal 组件设置为 OnPush策略
现在我们在 Signal 中做一些修改,在 signal 组件中设置
changeDetection: ChangeDetectionStrategy.OnPush,。
这就意味现在的 signal 组件只有如下几种情况才会触发变更检测:
- 组件的输入属性(@Input)发生变化。
- 组件内部触发事件(如按钮点击)。
- 使用
ChangeDetectorRef手动触发变更检测。
因为我们只是手动在 ngOnInit 中修改的 name 属性值,无法触发变更检测,所以此时再查看页面渲染,你会发现只有 signal-parent 的 name 属性被重新渲染了。
至于signal-child,由于 signal组件没有触发变更检测,那么作为子组件,它肯定也不会触发变更检测。Angular的变更检测是从组件树的根节点自上而下递归遍历的。
App=>Signal Parent=>Signal=>Signal Child。
3. 在 signal 组件中使用 signal 操作 name 属性
我们在 Signal 组件中添加使用 signal 定义的属性 signalName,然后在模板中绑定它。同时在定时器中修改 signalName 属性。
// signal.template.html
{{ signalName() }}
{{ name }}
// signal.component.ts
signalName = signal('Signal Name');
ngOnInit(): void {
setTimeout(() => {
this.signalName.set('Signal Name Changed');
this.name = 'Name Changed';
}, 2000);
}
这时候你会发现现在 signal 和 signal-child组件的 name 都被重新渲染了。这是因为 signal 的变化会触发变更检测,从而导致signal-child也重新渲染了。
4. 把 signal-parent 组件也设置为 OnPush策略
此时的结果为
signal-parent 的 name 属性并没有重新渲染,因为设置了 OnPush。
signal 触发了变更检测,但是并没有从根节点开始,而是从 signal 属性所属组件开始往下进行遍历。
此时变更检测的顺序为 Signal=>Signal Child。
5. 使用 dom 事件触发变更检测
为了验证这个猜测,我们在 signal 组件中添加一个 button, 通过click button来触发变更检测。
// signal.component.html
<button type="button" (click)="onClick()">Button</button>
// signal.component.ts
onClick(): void {}
现在 signal-parent 和 signal组件设置了 OnPush,signal-child 变更检测策略为默认值。
此时的渲染结果跟上一个case一样。
但当你点击 button 按钮,由于这次是组件内部触发 Dom Click事件,即使组件被设置为 OnPush策略,也会触发变更检测,所以这次从根节点开始,遍历组件树触发渲染更新,所有的 name 都被重新渲染了。
6. 添加 ngDoCheck 钩子,检查一下变更检测是不是按我们预期的触发
我们为所有组件添加 ngDoCheck 钩子,使用 console.log 来标记当前组件触发了变更检测,然后在定时器里面也添加 log。
ngDoCheck(): void {
console.log('Signal-xxx: DoCheck', this.name);
}
setTimeout(() => {
this.name = 'xxx Name Changed';
console.warn('signal-xxx: Timer called after 1s ----------');
}, 1000);
此时 signal-parent:
- timer 1s
- OnPush 策略
signal
- timer 2s
- OnPush 策略
signal-child
- timer 1s
- Default 策略
我们来看 Log 的结果:
你会发现有点反直觉:
为什么 SignalParent 设置了 OnPush,还是会频繁触发 DoCheck 呢?
为什么 Signal 组件的 DoCheck 没有触发,但是视图更新了呢?
我们先理清 log 触发的原因:
-
第一组
DoCheck被触发是因为 angular 初始化。 -
第二组是因为根组件(
AppComponent)渲染时,触发组件树遍历进行变更检测,但由于SignalParent设置了OnPush,所以只有App和SignaParent触发了,而Signal和SignalChild跳过本次检查。 -
第三组和第四组是因为
Timer回调导致的,但由于 SignaParent 设置了 OnPush,所以同上。 -
最后一组
Signal2s 触发Timer回调,所以App和SignaParent触发了DoCheck,而Signal组件由于 signalName 属性被修改,导致触发了变更检测,SignalChild是 default 策略,所以触发DoCheck。
那现在的情况是什么原因呢? 跟朋友讨论了一波,又查了一堆网上能找到的资料之后,来尝试理解一下。
为什么 SignalParent 设置了 OnPush,还是会频繁触发 DoCheck 呢?
目前有两种说法
-
第一种异步事件导致组件树触发了DoCheck,从 AppComponent 遍历到 SignalParent,即使它设置了 OnPush 策略, DoCheck 照样触发,但是不会标记当前组件为该视图需要更新。所以上面第四种case SignalParent 模板的 name 属性并没有被重新渲染。
-
第二种说法是 DoCheck 除了在变更检测之前运行,还会在angular 组件无法捕获自身变化时运行。由于
SignalParent设置了OnPush策略,导致从根组件开始依次进行的变更检测不能捕获自身的变化,所以触发了DoCheck勾子。
设置了 OnPush 策略的子孙组件是直接跳过异步事件导致的变更检测的,这点在两种说法下都被认可了。
目前第二种说法我没在官方文档上找到对应的介绍,持保守意见。
为什么 Signal 组件的 DoCheck 没有触发,但是视图更新了呢?
这是因为 signal 属性值更新会导致 angular 把当前组件标记为该视图需要更新。
由于 SignalParent 组件设置了 OnPush,所以 Signal 作为子组件会跳过 DoCheck,
但同样因为 signal 属性值更新,导致Angular会把当前组件当作组件树根节点,往下递归遍历进行变更检测,从而触发 SignalChild 的 DoCheck。
总结
signal 的特殊之处,就在于它会把当前组件标记为需要更新,并且会导致 angular 从当前组件层级开始往下进行检测和渲染更新,不会触发整个组件树。这也就是官方文档中提到的,细粒度地跟踪你的状态,使框架能够优化渲染更新。
如果当前组件还设置了其它的变更检测策略如 OnPush,也会按照该策略的默认行为去执行。
如果有大佬发现不对,请指正!上述 Case 在线代码地址 stackblitz
tips: Angular 的变更检测和页面重新渲染是两个不同的过程。
下期预告
@let user = user$ | async;
@if (user) {
<h1>Hello, {{user.name}}</h1>
<user-avatar [photo]="user.photo"/>
<ul>
@for (snack of user.favoriteSnacks; track snack.id) {
<li>{{snack.name}}</li>
}
</ul>
<button (click)="update(user)">Update profile</button>
}
-
Auto CSP - 根据 index.html 中的脚本自动生成基于哈希的严格 CSP 。