3

前言

在最近的项目中,团队从 Angular 15 升级到 Angular 19,中间了跨越了挺多个版本的,这些版本中的变化,特别是语法上的调整,这就需要对一些新语句调整上进行重新学习。

Signal

官网介绍

image.png

signal是对一个值的包装,当该值发生变化时,它会通知感兴趣的用户。信号可以包含任何值,从基本类型到复杂的数据结构。

你可以通过调用getter函数来读取信号的值,这允许Angular跟踪信号在哪里被使用。

信号可以是可写的,也可以是只读的。

Angular 中的 Signals 是一个响应式值,允许开发者以受控的方式更改值,并且跟踪值
的变化。

信号的类型

1. 可写信号(Writable Signal)

定义:可写信号是可以直接更新其中值的信号,通过使用.set() 或 .update() 方法,进行修改

创建方式: 使用signal() 函数创建一个可写信号,并设置初始化值

示例:

const count = signal(0);  // 创建可写信号,初始值为 0
console.log(count());      // 读取信号的值,输出:0

count.set(5);             // 设置新的值为 5
console.log(count());      // 读取信号的值,输出:5

count.update(value => value + 1);  // 将信号值加 1
console.log(count());      // 读取信号的值,输出:6

2. 计算信号(Computed Signal)

定义:计算信号是只读信号,通过其他信号进行计算,计算信号的值不能被修改,依靠全其他间接修改

创建方式:使用 computed() 函数来创建计算信号,并指定一个计算函数,这个函数返回信号的派生值

示例:

const count = signal(0);  // 创建可写信号,初始值为 0
const doubleCount = computed(() => count() * 2);  // 创建计算信号,计算 count 的两倍

console.log(doubleCount());  // 输出:0,计算值为 count 的两倍

count.set(5);                // 修改 count 的值
console.log(doubleCount());  // 输出:10,计算值会自动更新为 count 的两倍

从这个案例我们可以看到当 count(可写信号)发生变化时,Angular 会检测到这个变化,并触发相关的计算信号(如 doubleCount)重新计算其值。

3.Effects函数

effect 函数用于创建 副作用(effects),这也是新特性,主要的作用是允许你自动执行一个回调函数,当你检测的信号值变更,这个回调就会进行执行

示例:

import { signal, effect } from '@angular/core';

const count = signal(0);

effect(() => {
  console.log('The count has changed to:', count());  // 每当 count 发生变化时,这条消息会被打印
});

// 修改 count,触发 effect 执行
count.set(5);  // 输出:The count has changed to: 5
count.set(10); // 输出:The count has changed to: 10

从上面我可以看到,当我们创建一个effect(),这个函数接受一个回调函数,每次信号的值发生变更的时候,effect会重新执行这个回调函数。

注意点

effect 函数通常是与 signal 一起使用的。effect 用来创建副作用,它会监听和响应一个或多个 signal 的变化。当 signal 的值发生变化时,effect就会检测到变更之后执行相应的操作

RxJS与Angular Signal的互操作

Signal 能通过与RXJS进行结合,简化和增加应用的响应式特性

@angular/rxjs-interop 包提供了一些 API,帮助你将 RxJS 和 Angular 信号进行集成

创建信号从 RxJS Observable 使用 toSignal

使用ToSignal函数用于把RXJS Observable 创建一个对象

@Component({
  selector: 'app-root',
  imports: [CommonModule],
  standalone: true,
  template: `{{ counter() }}`,
  styleUrl: './app.component.css'
})
export class AppComponent {
  counterObservable = interval(1000);
  counter = toSignal(this.counterObservable, {initialValue: 0});
}

这里会进行批量从 0 一直开始增加

image.png

默认情况下,当创建 Observable 的组件或服务被销毁时, toSignal会自动取消订阅 Observable。

也可以自己进行手动取消订阅Observable

使用toObservable从信号创建 RxJS Observale

可以使用Signal 来存储查询条件,并通过 toObservable 将其转换为 Observable,每当Signal查询条件变化时,会发起新的 HTTP 请求并显示结果。

@Component({
  selector: 'app-root',
  imports: [CommonModule],
  standalone: true,
  template: `{{ counter() }}`,
  styleUrl: './app.component.css'
})
export class AppComponent {
 query: Signal<string> = signal(''); 
  query$: any; // 用于将 Signal 转换成 Observable
  results$: any; 

  constructor(private http: HttpClient) {
    // 将 Signal 转换为 Observable
    this.query$ = toObservable(this.query);
    
    // 根据查询信号值发起 HTTP 请求
    this.results$ = this.query$.pipe(
      switchMap(query => this.http.get('/search?q=' + query))
    );
  }

  // 更新查询条件
  updateQuery(newQuery: string) {
    this.query.set(newQuery); // 设置新的查询条件
  }
}

为什么使用Signal

使用 原始值(传统的 Angular 变更检测)

在传统的angular中,我们通常是使用普通的组件属性来存储状态,并依靠Angular的变更检测来更新

import { Component } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
   <div>
    <p>当前计数:{{ count }}</p>
    <button (click)="increment()">增加</button>
    </div>
  `
})
export class CounterComponent {
  count = 0;

  increment() {
    this.count++;
  }
}

变更检测机制(原始值)

当我们按下按钮的时候,count的值就会发生变化。
angular就会触发变更检测,检测组件的数据是否有变化。

变更检测原理:

Angular 使用 Zone.js 来自动检测异步操作(例如点击事件)的变化,并通知 Angular 启动变更检测。

默认情况下,当我们的组件中某个值发生了变化触发了变化检测,那么Angular会从上往下检查所有的组件。

image.png

使用 Signal(新机制)

import { Component, signal } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <div>
      <p>当前计数:{{ count() }}</p>
      <button (click)="increment()">增加</button>
    </div>
  `
})
export class CounterComponent {
  // 使用 signal 定义一个响应式的 count 状态
  count = signal(0);

  increment() {
    this.count.update(count => count + 1);
  }
}

变更检测机制

count 是一个 Signal,而不是普通的属性。Signal 是一个响应式对象,可以直接跟踪它的状态变化。
使用 count() 获取当前值,count.update() 方法用于更新状态。

变更检测原理:

Signal 允许精确地跟踪和更新状态。当信号的值发生变化时,只有与该信号绑定的部分会被更新。
Angular 会根据信号的变化自动更新视图,而不需要进行全局的变更检测。

总结:

1.没有 Signal 的传统 Angular:
比如你有一个计数器,点击按钮时会增加一个数字。
即使你只改变了一个数字,Angular 会遍历整个组件,检查是否需要更新页面中的所有绑定。

2.有 Signal 的 Angular:
计数器用 Signal 来追踪数字变化。
当数字变化时,只有与这个数字绑定的部分会被更新。Angular 不需要检查页面中的其他内容。

引入 @for 和 @if

在Angular17版本的时候,模版就应该有了比较大的变化,特别是引人了@for和@if语句的引入,使模版变得简洁,相比之前的版本使用ngfor和ngIf,新语法也比较容器理解

Angular 15 中的写法

<!-- Angular 15: 使用 *ngFor 和 *ngIf -->
<div *ngIf="users.length > 0; else noUsers">
  <div *ngFor="let user of users">
    {{ user.name }}
  </div>
</div>

<ng-template #noUsers>
  <p>暂无用户</p>
</ng-template>

*ngIf 和 *ngFor 是常用的结构性指令,用来根据条件渲染内容。

<ng-template [ngIf]="users.length > 0">
  <ng-template ngFor let-user [ngForOf]="users">
    <div>{{ user.name }}</div>
  </ng-template>
</ng-template>

<ng-template #noUsers>
  <p>暂无用户</p>
</ng-template>

这里ngIf和ngFor是简单的指令。然后每个ng-template都会生成一个“视图”。ng-template 是一个隐藏的模板,它的内容只有在条件满足时才会被显示出来,而 ngIf 和 ngFor 就是控制这些模板何时显示的工具。

*ngIf 的工作原理:

当你使用 ngIf 时,Angular 会根据 ngIf 表达式的值来决定是否在 真实 DOM 中创建或销毁该元素的视图。比如,当 *ngIf 条件为 true 时,Angular 会动态创建该元素的视图,并将其插入到真实 DOM 中。

当 *ngIf 条件为 false 时,Angular 会从真实 DOM 中移除该视图,而不仅仅是隐藏它。这就意味着页面上不再显示相关元素,也不会占用任何空间。

Angular 17 及之后的写法

从Angular 17 开始,官方引入了@for和@if语法,这种新写法少了对ng-template得嵌套,
Angular 有个概念叫做ViewContainer。ViewContainer是Angular的一个内部API。一个ViewContainer就像一个盒子,您可以在其中插入/删除子视图。

<!-- Angular 17+:使用 @for 和 @if -->
@if (users.length > 0) {
  @for (let user of users; track user.id) {
    <p>{{ user.name }}</p>
  }
} @else {
  <p>暂无用户</p>
}

类似JavaScript的语法

*ngIf, *ngFor:由于需要嵌套 ng-template,这会让模板的结构变得较为复杂,尤其当逻辑较多时,阅读起来可能会感到不太直观。

使用@if, @for 新语法更接近 JavaScript 语法,学习起来比较友好。

性能提升

*ngIf, *ngFor:每次使用 *ngIf 和 *ngFor 时,Angular 会为每个条件渲染或循环渲染生成一个 ng-template,这在复杂的应用中可能影响性能,特别是在处理大量数据时。

@if, @for: 减少了不必要的 ng-template 实例化,提高了性能,减少了不必要的渲染,这种方法比直接操作 DOM 要更高效,因为 Angular 只会关注那些真正改变的部分,而不会每次都重绘整个页面

增量 DOM

增量 DOM 的概念就是在每次数据或视图变化时,通过指令来动态生成和更新 DOM,并与当前的真实 DOM 进行差异比较。

image.png

组件编译为指令:增量 DOM 将每个组件编译成一组指令,这些指令定义了如何创建和更新 DOM。指令是增量 DOM 的核心,它们控制着 DOM 的渲染和更新过程。

生成新的 DOM 树:每当组件的输入(数据)发生变化时,增量 DOM 会使用这些指令重新生成一个新的 DOM 片段,表示新的视图。

与原始 DOM 比对:新的 DOM 片段会与原来的真实 DOM 进行比对,找出差异。这是增量 DOM 和虚拟 DOM 的一个主要区别,虚拟 DOM 会先生成完整的 DOM 树再与真实 DOM 比对,而增量 DOM 直接生成新的 DOM,并对差异部分进行更新。

最小化 DOM 更新:与传统的虚拟 DOM 比较不同,增量 DOM 只更新需要改变的部分,而不需要完全重新渲染整个 DOM 结构。这样可以提高性能,减少不必要的渲染操作。

虚拟 DOM

虚拟 DOM 是前端框架(如 React、Vue)中常用的一种高效更新视图的技术,虚拟 DOM 是一个以 JavaScript 对象形式存在的 DOM 树的抽象表示。

工作流程

创建虚拟 DOM:根据初始状态,创建一个虚拟 DOM 树(JavaScript 对象)。
状态更新:当状态发生变化时,生成新的虚拟 DOM 树。
当用户 UI 发生变化时,将整个用户 UI 渲染到虚拟 DOM 中。
Diff 算法:比较新旧两个虚拟 DOM 树,找出变化部分。
更新真实 DOM:将变化应用到真实 DOM,仅更新需要变动的部分。

image.png

总结

总体来说Angular到17版本变化挺大,开始偏向vue的方式发展,总归到底还是为了解决大项目大导致的性能问题

参考地址
https://angular.dev/overview


kexb
519 声望16 粉丝

引用和评论

0 条评论