Angular 17+ 高级教程 – Signals

这篇具有很好参考价值的文章主要介绍了Angular 17+ 高级教程 – Signals。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言

在上一篇 Change Detection 中, 我们有提到 MVVM 监听 ViewModel 变化的难题.

当年 AngularJS 和 Knockout.js (下面简称 KO) 各自选了不同的道路.

但如今, 事过境迁, Angular 最终也走向了 KO 的道路. 这就是这篇的主角 Signal。

 文章来源地址https://www.toymoban.com/news/detail-837832.html

把 variables 变成 function 

在 JavaScript, 值类型 variable 无法被监听, Signal 的做法是把它们都变成函数。

看看 KO 的代码

const count = ko.observable('default value'); // 通过 observable 方法 delcare variable
const value = count(); // count 是一个 getter 方法
count('new value'); // count 也是一个 setter 方法

变成函数后,我们就可以把监听代码写到 setter 方法中。

虽然 KO 已经退出前端舞台多年,但这个 Signal 概念依然沿用至今,许多库/框架都可以看见。

Angular 17+ 高级教程 – Signals

缺点

Signal 最大的缺点是代码的读写。这也是为什么 Angular 一直坚守 Zone.js + tick。

原本用 1 个 variable + variable assign operator 来描述的代码。

变成了 2 个 variable methods, read and assign 都变成了 method call。

// before
const value = 0; // declare variable
const value2 = value; // passing variable
value = 1; // assign value to variable
value++ // other assign operator

// after
const [getValue, setValue] = declare(0);
const value2 = getValue();
setValue(1);
setValue(getValue()++);

这种写法在其它语言都很少见,或者一看就感觉是为了性能优化特地改的写法。 

总之严重影响 DX。

与众不同的 Svelte 5

Svelte 5 的 Signal 应该是所有 framework 里 DX 最好的 (比 Angular 好很多)。

Angular 17+ 高级教程 – Signals

只需要在最源头做 declaration 就可以了,count 不会变成恶心的 getter 和 setter,它依然是 variable 的使用方式,但它却有了 Signal 的功能。

显然 Svelte 又在 compile 阶段加了很多黑魔法让其工作,但我觉得符合直觉也是很重要的,getter 和 setter 明显就是种妥协。

Angular 也爱搞黑魔法,也有 compiler,为什么不像 Svelte 那样呢?

答案在:Sub-RFC 2: Signal APIs

Angular 17+ 高级教程 – Signals

这段指出,Svelte 的黑魔法无法实现统一语法,在跨组件共享 Signals 的时候写法需要不一致,我不确定这是不是真的。

Angular 认为既然无法做到所有代码统一干净,那就干脆做到所有代码统一不干净吧,至少能统一嘛。这听上去就不太合理,我看他们就是想偷工😒。

  

Why Angular need Signal?

Zone.js + tick 是 DX 最好的。Signal 没有的比。

所以 Signal 比的是性能优化版本的 Zone.js + OnPush + markForCheck 或者 Zone.js + Rxjs Stream + AsyncPipe 方案。

  1. 比 DX,两者都不太好,可能 Zone.js 稍微好一点点。

  2. 比性能,两者都可以,但 Signal 还可以做到更极致(虽然目前可能没有这个需求)

  3. 比心智,Zone.js 特立独行,手法间接,复杂不直观。相反 Signal 已经相当普及,手法也简单直接。

总结:Signal 明显利大于弊,但 Angular 一定会保留 Zone.js 很长很长一段事件。因为 Zone.js + tick 真的是最好的 DX。

 

Signal

目前 v17.0,Signal 还不完整,只有部分功能可用。

Signal 可以单独使用, 不需要搭配任何 Angular 的东西(比如组件、依赖注入都不需要)

但是偌想用 Signal 取代 Zone.js + tick,那就需要结合 Signal-based Components,而这部分还没有开放测试。

所以这里只能先介绍部分功能。

declare, assign, set, update

const value = signal(0);
// 相等于
// let value = 0;

通过调用 signal 函数来 declare 一个 variable。0 是初始值。

返回的 value 是一个 signal  函数(同时它也是一个对象)

const value = signal(0);
console.log(value());
// 相等于
// console.log(value);

读取 value 的方式是函数调用。这个对初学者是很不习惯的,一不小心忘了放括弧,可能就出 bug 了。

const value = signal(0);
value.set(5);
// 相等于
// value = 5;

赋值需要通过 .set 方法。

const value = signal(0);
value.update(curr => curr + 5);
// 相等于
// value += 5;

update 和 set 都是用来修改 value 的, 区别是 update 带有一个 current value 的参数,方便我们做累加之类的操作。

set 和 update 都需要返回一个新的 value,而且这个 value 最好是 immuable 的。

const obj = { name: 'Derrick', age: 11 };
const objSignal = signal(obj);
obj.age = 20;
objSignal.set(obj);

如果返回的 value 是相同引用,Signal 将认为 value 没有改变,不会触发任何 trigger。

在创建 Signal 的时候,我们可以自定义它判断相同值的方式。

const objSignal = Signal(obj, { equal: (a, b) => a === b });

computed

const firstName = signal('Derrick');
const lastName = signal('Yam');
const fullName = computed(() => firstName() + ' ' + lastName());
console.log(fullName()); // Derrick Yam
firstName.set('Richard');
console.log(fullName()); // Richard Yam

如果一个 variable 依赖其它 variable,可以用 computed 来维护这条关系连。

类似于 RxJS 的 combineLatest。

注:computed 是在 fullName() 的时候去获取所有依赖的当前值,而不是像 RxJS combineLatest 那样搞缓存。

effect

export class AppComponent {
  constructor() {
    const firstName = signal('Derrick');
    effect(() => {
      console.log(firstName());
    });
    firstName.set('Stefanie');
  }
}

effect 比较特殊,它有点类似于 RxJS 的 combineLatest + subscribe。

当 effect 内任何一个 Signal value 变化时,整个 effect 会被触发(包括第一次 assign value)

另外 effect 的触发时机不是同步的, 它是 async micro task。

所以像上面的例子,console 只会触发一次,value 是 Stefanie。

注: effect 只能用于 Injection Context。

调用 effect 会返回 effectRef。

const value = signal(0);
const effectRef = effect(() => console.log(value()));
effectRef.destroy();

它让我们可以停止监听。类似于 RxJS 的 unsubscribe。

effect 内部也有 on dispose 功能。

const effectRef = effect((onCleanup) => {
  console.log(value());
onCleanup(()
=> { console.log('effect destroyed'); }); }); effectRef.destroy();

effect 内所有 Signal getter 都会被依赖追踪,如果我们想 skip 掉一些,可以这样声明

effect(() => {
  console.log(value1());
  untracked(() => {
    console.log(value2());
  });
});

当 value2 改变时,effect 不会触发,因为我们 untracked 了。

 

Signal & RxJS

在上一篇的结尾,我们有提到,Signal 是用来取代 RxJS 的。

强调:只是取代在 Change Detection 范围下的 RxJS。而不是整个项目的 RxJS 哦。所以只是很小的一个范围而已。

另外,取代 RxJS 是因为它的 DX 和 LX (learning experience) 不够好,而不是它功能不够好。

正是因为这些原因,Signal 和 RxJS 长的挺像的,而且它们好像本来就师出同门哦。

  1. signal 像 BehaviorSubject

  2. computed 像 combineLatest

  3. effect 像 combineLatest + subscribe

强调:只是像而已。

switch between each other

既然长得像,那就可以转换咯。

toObservable

import { toObservable, toSignal } from '@angular/core/rxjs-interop';

const value = signal(0);
const obs = toObservable(value);
obs.subscribe(v => console.log(v)); // 5
value.set(5);

toSignal

constructor() {
  const firstName = new BehaviorSubject('Derrick');
  const lastName = signal('Yam');
  const firstNameSignal = toSignal(firstName);
  const fullName = computed(() => firstNameSignal() + ' ' + lastName());
  firstName.next('Richard');
  console.log(fullName()); // Richard Yam
}

注:toSignal 只能用于 Injection Context。

 

Signal in Component

Angular v17 Signal-based Components 还在孕育中。但我们已经可以在组件里使用部分 Signal 功能了。

app.component.ts

export class AppComponent {
  firstName = signal('Derrick');
  lastName = signal('Yam');
  fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
}

app.component.html

<p>{{ fullName() }}</p>
<button (click)="firstName.set('Alex')">update first name</button>
<button (click)="lastName.set('Lee')">update last name</button>

效果

Angular 17+ 高级教程 – Signals

Signal and refreshView

Angular 文档有提到,Signal 是可以搭配 ChangeDetectionStrategy.OnPush 使用的。

Angular 17+ 高级教程 – Signals

但是有一点我要 highlight,当 Signal 值改变的时候,当前的 LView 并不会被 markForCheck。

Angular 用了另一套机制来处理 Signal 和 refresh LView 的关系。

逛一逛 Signal 和 refresh LView 的源码

如果你对 Angular TView,LView,bootstrapApplication 过程不熟悉的话,请先看 Change Detection 文章。

场景:

有一个组件,ChangeDetectionStrategy.OnPush,它有一个 Signal 属性,binding 到 Template。

组件内跑一个 setTimeout 后修改 Signal 的值,但不做 markForCheck,结果 DOM 依然被更新了。

提问:

1. Signal 值改变,Angular 怎么得知?

2. Angular 是怎样更新 DOM 的?使用 tick、detechChanges 还是 refreshView?

回答:

首先,不要误会,Angular 并没有暗地里替我们 markForCheck,它采用了另一套机制。

这套机制依然需要 NgZone,当 Zone.js 监听事件后,依然是跑 tick。

tick 会从 Root LView 开始往下遍历。到这里,按理说我们没有 markForCheck 任何 LView,遍历根本跑不下去。

所以 Angular 新加了一个往下遍历的条件。

change_detection.ts 源码

Angular 17+ 高级教程 – Signals

detectChangesInViewWhileDirty 是判断要不要往下遍历。

Angular 17+ 高级教程 – Signals

HasChildViewsToRefresh 意思是当前 LView 或许不需要 refresh,但是其子孙 LView 需要,所以得继续往下遍历。

Angular 17+ 高级教程 – Signals

那这个 HasChildViewsToRefresh 是谁去做设定的呢?自然是 Signal 咯。

我们先大概熟悉一下 Signal 源码。

Angular 17+ 高级教程 – Signals

创建 Signal 我们会得到一个 getter 函数(当然它也是一个对象,里面存了维护一切的 SignalNode)。

当 getter 被调用的时候,会跑 producerAccessed 函数。

Angular 17+ 高级教程 – Signals

它会把自己的 SignalNode 添加给 activeConsumer。

这个 activeConsumer 类似一个依赖收集器。它是一个全局变量。

比如说,我现在想要收集依赖

Angular 17+ 高级教程 – Signals

那我就 set 一个 activeConsumer。然后跑代码,代码中会调用多个 Signal 的 getter。

那这些 getter 就会把各自的 SignalNode 添加进去 activeConsumer。

当代码跑完以后,这个 activeConsumer 就收集完它依赖的所有 SignalNode 了,然后就可以监听这些 SignalNode 值变化,来做各种事情。

好,回到来 LView。当 Angular 在 refreshView 时

Angular 17+ 高级教程 – Signals

在 enterView 以后,它 set 了 activeConsumer,这时开始收集依赖。

在 leaveView 之前,它把 activeConsumer set 回去。停止依赖收集。

所以,如果模板 binding 中有执行任何 Signal getter,都会被 LView 收集起来。

那当这些 Signal 值改变后,它会执行 markAncestorsForTraversal

Angular 17+ 高级教程 – Signals

顾名思义,就是把祖先 mark as HasChildViewsToRefresh。

Angular 17+ 高级教程 – Signals

总结

Angular 不是通过 markForCheck 来让 Signal 同步模板的。它搞了一个新机制。

新机制比 markForCheck 好,markForCheck 会造成祖先一定要 refreshView,但 HasChildViewsToRefresh 则不会。

 

Signal-based Input (a.k.a Signal Inputs)

Angular v17.1.0 版本 release 了 Signal-based Input,只有 Input 而且,Output 还没有推出。

Input Signal 的作用就是自动把 @Input 转换成 Signal,这样既可以利用 Signal Change Detection 机制,也可以用来做 Signal Computed 等等,非常方便。

下面是一个 Input Signal

export class SayHiComponent implements OnInit {
  inputWithDefaultValue = input('default value');

  computedValue = computed(() => this.inputWithDefaultValue() + ' extra value');

  ngOnInit(): void {
    console.log(this.inputWithDefaultValue()); // 'default value'
    console.log(this.computedValue()); // 'default value extra value'
  }
}

除了变成 Signal 以外,其它机制和传统的 @Input 没有太多区别,比如一样是在 OnInit Hook 时才可用。

还有一点要注意,这个 Input Signal 是 readonly 的,不是 WritableSignal,这其实是合理的,以前 @Input 可以被修改反而很危险。

required 的写法

inputRequired = input.required<string>();

为了更好的支持 TypeScript 类型提示,Angular 把 requried 做成了另一个方法调用,而不是通过 options。

如果它是 required 那就不需要 default value,相反如果它不是 required 那就一定要放 default value。

也因为 required 没有 default value 所以需要通过泛型声明类型。

alias 和 transform 的写法

inputRequiredWithAlias = input.required<string>({ alias: 'inputRequiredAlias' });
inputRequiredWithTransform = input.required({
  transform: booleanAttribute,
});

transform 之所以不需要提供类型是因为它从 boolAttribute 中推断出来了。

我们要声明也是可以的

inputWithTransform = input.required<unknown, boolean>({
  transform: booleanAttribute,
});

optional alias 和 transform 的写法

inputOptionalWithAlias = input('defualt', { alias: 'inputOptionalAlias' });
inputOptionalWithTransform = input(undefined, { transform: booleanAttribute });

第一个参数是 initial value,一定要放,哪怕是放 undefined 也行,因为它只有三种重载。

Angular 17+ 高级教程 – Signals

 

Signal-based Two-way Binding (a.k.a Signal Models)

Angular v17.2.0 版本 release 了 Signal-based Two-way Binding,请看这篇 Component 组件 の Template Binding Syntax # Signal-based Two-way Binding

 

Signal-based Query (a.k.a Signal Queries)

Angular v17.2.0 版本 release 了 Signal-based Query,请看这篇 Component 组件 の Query Elements Query Elements # Signal-based Query

 

Signal-based Component

TODO... 目前还不能测试...

 

目录

上一篇 Angular 17+ 高级教程 – Change Detection

下一篇 Angular 17+ 高级教程 – Component 组件 の Dependency Injection & NodeInjector

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

 

到了这里,关于Angular 17+ 高级教程 – Signals的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Angular 17+ 高级教程 – Change Detection

    虽然 Angular 正在把大部分 Change Detection 概念换成 Signal,但是最快也要 1 年后,所以还是有必要认真学习一下的。   MVVM 框架的开发方式是这样的: 写 HTML 写 ViewModel 在 HTML 里加入 binding syntax。 在 HTML 里加入 listening syntax,在事件发生时修改 ViewModel。 MVVM 的宗旨是 \\\"不要直接

    2024年03月09日
    浏览(41)
  • Angular 17+ 高级教程 – 学以致用

    读这么多原理,到底为了什么?真实项目中真的会用得到吗? 你正在疑惑 \\\"知识的力量\\\" 吗? 本篇会给一个非常非常好的案例,让你感悟 -- 知识如何用于实战。 记住,我的目的是让你感悟,而不是要你盲目相信知识。   下面是我在 2020-11-06 记入的一个问题。 一模一样的问题

    2024年04月22日
    浏览(38)
  • Angular 17+ 高级教程 – Angular 的局限 の Query Elements

    熟悉 Angular 的朋友都知道,Angular 有非常多的局限,许多事情它都做不好,打开 Github 一堆 2016 - 2017 的 Issues,时至今日都没有解决。 原因也很简单 -- Angular 团队的不作为😔。 通常我会把常见的 Angular 的局限记入在这篇 Angular 的局限和 Github Issues,但由于本篇要讲的问题篇幅

    2024年04月24日
    浏览(42)
  • Angular 17+ 高级教程 – Dependency Injection 依赖注入

    本来是想先介绍 Angular Component 的,但 Component 里面会涉及到一些 Dependency Injection (简称 DI) 的概念,所以还是先介绍 DI 吧。 温馨提醒:如果你对 JS class、prototype 不太熟悉的话,建议你先看这篇 JavaScript – 理解 Object, Class, This, Prototype, Function, Mixins   首先我们有一个 class Ser

    2024年03月09日
    浏览(61)
  • Angular 17+ 高级教程 – Component 组件 の Query Elements

    Angular 是 MVVM 框架。 MVVM 的宗旨是 \\\"不要直接操作 DOM\\\"。 在 Component 组件 の Template Binding Syntax 文章中,我们列举了一些常见的 DOM Manipulation。 Template Binding Syntax 替代了上面许多的 DOM Manipulation,但任然有些 DOM Manipulation 是它没有覆盖到的。 比如说 Query Child Elements e.g. document.

    2024年03月09日
    浏览(39)
  • Angular 17+ 高级教程 – Component 组件 の Control Flow

      Control Flow 是 Angular v17 版本后推出的新模板语法,用来取代 NgIf、NgForOf、NgSwitch 这 3 个 Structure Directive。 Structure Directive 的好处是比较灵活,原理简单,但是即便用了微语法,它看上去还是相当繁琐,而且不够优雅。 Conrol Flow 的好处是它的语法够美,缺点是不必 Structure Di

    2024年03月11日
    浏览(50)
  • Angular 17+ 高级教程 – Component 组件 の ng-template

    上一篇 Dynamic Component 我们有提到,作为 MVVM 框架的 Angular 需要有方法替代掉 2 个 DOM Manipulation: document.createElement  template.content.clone Dynamic Component 便是替代 document.createElement 的方案。 而这篇我们要讲的 ng-template 则是 template.content.clone 的替代方案。   我们从浅入深,一个一

    2024年03月09日
    浏览(52)
  • Angular 17+ 高级教程 – Component 组件 の Dependency Injection & NodeInjector

    在 Dependency Injection 依赖注入 文章中,我们学习了 50% 的 Angular DI 知识,由于当时还不具备组件知识,所以我们无法完成另外 50% 的学习。 经过了几篇组件教程后,现在我们已经具备了基础的组件知识,那这一篇我们便来完成 Angular DI 所有内容吧。   Angular in Depth – A Deep

    2024年03月09日
    浏览(43)
  • Angular 17+ 高级教程 – Component 组件 の 生命周期钩子 (Lifecycle Hooks)

    之前在 Component 组件 の Angular Component vs Custom Elements 文章中,我们有学习过几个基础的 Lifecycle Hooks。 比如 OnChanges、OnInit、AfterViewInit、OnDestroy,但那篇只是微微带过而已。 这篇让我们来深入理解 Angular 的 Lifecycle Hooks。   在 Component 组件 の Dependency Injection NodeInjector 文章中

    2024年03月09日
    浏览(50)
  • Angular 17+ 高级教程 – Component 组件 の Structural Directive (结构型指令) & Syntax Reference (微语法)

    在 Attribute Directives 属性型指令 文章中,我们学习过了指令。指令是没有 HTML 和 CSS 的组件,它单纯用于封装 JS 的部分。 这一篇我们将继续学习另一种指令 -- Structural Directive 结构型指令。 就代码而言,Structural Directive 和 Attribute Directives 是完全一样的,只是用途不同,因此

    2024年03月10日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包