Angular 快速入门:Signals 响应式编程与生命周期

前四篇文章我们从零搭了一个 Todo 应用,做了组件拆分,用 Service 管理数据,甚至还试了 RxJS 和 Signal 两种响应式方案。

但之前提到 Signals 时,只是把它作为"状态管理的另一种选择"快速带过。这一篇我们彻底搞明白:Signal 到底是什么?它解决了什么问题?怎么用好它?

顺便把另一个绕不开的话题也讲了——组件的生命周期。这两个东西放在一起讲是有原因的。你一会儿就明白了。

1. 为什么需要 Signals?

Zone.js 的"笨办法"

在讲 Signal 之前,得先知道 Angular 原来是怎么检测变化的。

Angular 用的是一套叫 Zone.js 的机制。它的工作方式很简单粗暴:

Zone.js "猴子补丁"了所有浏览器 API——setTimeoutaddEventListenerXMLHttpRequest……只要这些东西一执行,Zone.js 就通知 Angular:“嘿,可能发生了点什么,你去检查一下吧!”

Angular 收到通知后就从根组件开始遍历整个组件树,检查每个属性的值变了没有。

这个过程叫变更检测(Change Detection)。

用户点击按钮
  ↓
Zone.js 捕获到点击事件
  ↓
Angular 遍历整个组件树,检查所有绑定
  ↓
发现有变化,更新 DOM

这个方案的好处是开发者几乎不用操心——你只管改数据,Angular 帮你搞定后续一切。这也是 Angular 一直以来的核心卖点:"magic"一样的变化检测。

坏处也很明显——做了很多无用功

你的应用有 100 个组件,用户就点了一下按钮,Angular 却要检查全部 100 个组件有没有变化。大多数时候什么都没变,但还是检查了一遍。

打个比方:Zone.js 的变更检测就像你每隔几分钟就去检查冰箱里的菜新不新鲜。你打开门、看一眼、关上。确实能发现坏了的菜,但大多数时候啥也没变,白费功夫。

Signals 的"巧办法"

Signals 换了个思路:

不主动检查了。谁变了,谁主动通知。

signal 的值变了
  ↓
signal 自动通知所有"依赖它"的地方
  ↓
只有依赖了这个 signal 的组件才更新

这就从"全量检查"变成了"精准推送"。

对小程序来说,两者区别不大。但对大型应用来说,Signals 能省掉大量无意义的变更检测。

继续刚才那个比方:

  • Zone.js 模式:你每隔几分钟打开冰箱检查。
  • Signals 模式:冰箱里的菜坏了,菜自己喊你:“快来!我不行了!”

跨框架视角

  • React:每次 setState 重新执行整个组件函数(以及所有子组件),除非你用 memo/useMemo 手动优化。React 18+ 的 useMemo 其实跟 computed 有点像,但机制不同。
  • Vue 3:Vue 3 的 ref()reactive() 本身就是 Signal 的近亲。事实上 Angular 的 Signal 设计在很大程度上借鉴了 Vue 的响应式系统。所以如果你用过 Vue 3 的 ref(),下面这些你会感觉很眼熟。
  • Angular Zone.js:你不用手动声明"哪些数据是响应式的",所有属性默认都被追踪。省事,但代价是每次都要全量检查。

严格来说,Angular 也一直在优化 Zone.js 变更检测——比如 OnPush 策略可以跳过部分检查。但 Signals 是从根本上换了一种更高效的方式。

好,理论讲完了。接下来看怎么用。

2. signal() — 响应式数据的基石

创建和读取

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

// 创建
const count = signal(0);

// 读取——调用它!
console.log(count());  // 0

就这么简单。signal(0) 创建了一个初始值为 0 的信号。count() 就是读取当前值。

注意是函数调用count() 不是 count。Signal 是一个函数,读取值需要调用它。这跟 Vue 的 ref.value 读取不一样,但跟 Solid.js 一样。

修改值

// 直接赋值
count.set(5);
console.log(count());  // 5

// 基于原值更新
count.update(v => v + 1);
console.log(count());  // 6

set 是你知道新值是什么的时候用。update 是你需要基于旧值算出新值的时候用——传一个函数,函数的参数就是当前值,返回值是新值。

对比一下:

  • Angular Signalcount.set(5) / count.update(v => v + 1)
  • Vue refcount.value = 5 / count.value++
  • React useStatesetCount(5) / setCount(v => v + 1)

Angular 和 React 都明确区分"设置新值"和"基于旧值更新"两种操作。Vue 因为依赖 Proxy 的赋值操作,看起来更"自然"——但底层原理完全不同。

只读版本

有时候你希望外部能读取数据,但不能修改它:

private items = signal<TodoItem[]>([]);

// 暴露只读版本
readonlyItems = this.items.asReadonly();

外部通过 readonlyItems() 能读到值,但不能调用 .set().update()。这是封装的好习惯。

在模板中使用

在组件里定义一个 signal:

export class TodoComponent {
  count = signal(0);

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

在模板里,直接用 () 取值:

<p>当前计数:{{ count() }}</p>
<button (click)="increment()">+1</button>

看到没有?不需要 async pipe,不需要 .subscribe。Signal 比 RxJS Observable 更"原生"——Angular 的模板系统本身就认识 Signal,它会自动追踪依赖,当 signal 变化时只更新这个组件。

这一点跟 Vue 的 ref 在模板中自动解包非常像。Vue 里你写 {{ count }},Angular 里你写 {{ count() }}——就差一对括号。

在 Service 中使用

Signal 不只是组件里能用,在 Service 里用更常见:

@Service()
export class TodoService {
  private items = signal<TodoItem[]>([]);

  getItems() {
    return this.items.asReadonly();
  }

  addTodo(text: string) {
    this.items.update(list => [...list, { id: Date.now(), text, done: false }]);
  }
}

组件注入后,直接调用 service.getItems() 拿到只读 signal,模板里用 items() 取值。

前面第 4 篇我们已经写过类似的代码了,现在你应该明白:为什么 Service 里的 signal 变了,组件能自动更新?

因为组件的模板里用了 items(),Angular 知道"这个组件依赖了那个 signal"。signal 变了,Angular 精准更新这个组件,而不是检查所有组件。

3. computed() — 计算属性

基本用法

computed 是基于其他 signal 自动计算的值:

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

const price = signal(100);
const quantity = signal(2);
const total = computed(() => price() * quantity());

console.log(total());  // 200

price.set(50);
console.log(total());  // 150 —— 自动重新计算

total 依赖 pricequantity 两个 signal。只要任何一个变了,total() 的值就会自动更新。

这就是"计算属性"的含义——你声明它怎么算,剩下的 Angular 帮你搞定。

惰性求值

computed惰性求值的。意思是:

  • 如果没人读 total(),就算 price 变了,computed 也不会重新计算。
  • 只有当有人读取 total() 时,它才检查依赖有没有变。变了就重算,没变就返回缓存的值。

这跟 Vue 的 computed 一模一样——也是惰性的。React 的 useMemo 虽然也是缓存计算结果,但它是在组件渲染时主动计算的,不是真正的"惰性"。

实用的例子

在 Todo 应用里:

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

@Service()
export class TodoService {
  private items = signal<TodoItem[]>([
    { id: 1, text: '学习 Angular 基础', done: true },
    { id: 2, text: '写一个 Todo 应用', done: true },
    { id: 3, text: '对比 React 和 Vue 的差异', done: false },
  ]);

  // 计算属性:已完成数量
  completedCount = computed(() =>
    this.items().filter(t => t.done).length
  );

  // 计算属性:未完成数量
  pendingCount = computed(() =>
    this.items().filter(t => !t.done).length
  );

  // 计算属性:进度百分比
  progress = computed(() => {
    const total = this.items().length;
    if (total === 0) return 0;
    return Math.round((this.completedCount() / total) * 100);
  });
}

在某个组件注入该全局服务:

@Component(...)
export class TodoComponent {
   todoService = inject(TodoService);
}

在模板里直接用:

<p>进度:{{ todoService.progress() }}% ({{ todoService.completedCount() }}/{{ todoService.items().length }})</p>

每次 items 变化时,completedCountpendingCountprogress 都会自动更新。不需要手动调用任何方法。

对比 Vue 的 computed(() => ...) 和 React 的 useMemo(() => ..., [...]),Angular 的 computed 跟 Vue 的几乎一样——都是自动收集依赖,不需要你手动声明依赖数组。React 的 useMemo 则需要你手动列出依赖,漏了或多了都可能出 bug。

4. effect() — 当数据变化时干点啥

基本用法

signal 存数据,computed 算数据。那数据变了之后想执行一些操作(比如存本地缓存、打日志、发请求)怎么办?

effect

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

const count = signal(0);

effect(() => {
  console.log(`count 变成了: ${count()}`);
});
// 立即执行一次,打印: count 变成了: 0

count.set(1);
// 打印: count 变成了: 1

count.set(2);
// 打印: count 变成了: 2

effect 自动追踪它里面用到的所有 signal。任何一个 signal 变了,它自动重新执行。

而且要注意:effect 会立即执行一次(用来收集依赖)。第一次执行时,Angular 记下了"这个 effect 依赖了 count",后面 count 变化时才会触发。

这就是"副作用"(side effect)的意思——它不是计算数据,而是做跟数据变化相关的额外操作。类似于:

  • Vue 的 watchEffect / watch
  • React 的 useEffect

Vue 的 watchEffect 跟 Angular 的 effect 行为最像——也是自动收集依赖、立即执行一次。React 的 useEffect 则不同:它不是在"数据变化时执行",而是在"组件渲染后执行",并且依赖数组需要手动指定。

使用场景

@Service()
export class TodoService {
  private items = signal<TodoItem[]>([]);

  constructor() {
    // 场景1:数据持久化——变化时存到 localStorage
    effect(() => {
      localStorage.setItem('todos', JSON.stringify(this.items()));
    });
  }

  addTodo(text: string) {
    this.items.update(list => [...list, { id: Date.now(), text, done: false }]);
  }
}

其他常见场景:

  • 日志追踪:数据变了打日志,用于调试或分析
  • 数据同步:变化时自动同步到后端
  • 本地缓存:如上面的 localStorage
  • UI 联动:数据变化时触发一些 UI 操作(非模板绑定的场景)

清理

如果 effect 里有定时器或者订阅,需要手动清理:

effect(() => {
  const id = setInterval(() => {
    console.log('当前数据:', this.items());
  }, 5000);

  // 返回清理函数
  return () => {
    clearInterval(id);
  };
});

返回的清理函数会在两种情况下执行:

  1. effect 重新运行前(清理旧依赖的副作用)
  2. 组件/Service 销毁时(防止内存泄漏)

这跟 React useEffect 的 cleanup 函数是一个道理。

需要注意的地方

effect 必须在注入上下文中调用。什么意思呢?

// ✅ 正确:在 constructor 里
@Service()
export class TodoService {
  constructor() {
    effect(() => { /* ... */ });
  }
}

// ✅ 正确:在组件初始化时
export class TodoComponent {
  constructor() {
    effect(() => { /* ... */ });
  }
}

// ❌ 错误:在任意函数里
function someFunction() {
  effect(() => { /* ... */ }); // 报错!没有注入上下文
}

如果需要在外部的函数里创建 effect,可以用 inject(EffectRef) 或者把创建逻辑放到类的初始化过程中。

这一点跟 Vue 完全一样——watchEffect 也需要在 setup()script setup 中调用。React 的 useEffect 更严格,只能在组件顶层调用。

5. 用 Signal 升级 Todo 组件

前面第 3 篇我们写了 TodoItemComponent,用的还是 @Input / @Output。现在用 Signal 的方式重新写一遍。

input() — 替代 @Input

import { Component, input, output } from '@angular/core';
import { CommonModule } from '@angular/common';
import { TodoItem } from '../types';

@Component({
  selector: 'app-todo-item',
  standalone: true,
  imports: [CommonModule],
  templateUrl: './todo-item.html',
  styleUrls: ['./todo-item.css']
})
export class TodoItemComponent {
  // 旧写法:@Input() todo!: TodoItem;
  // 新写法:input() 返回一个 signal
  todo = input.required<TodoItem>();

  // 旧写法:@Output() toggle = new EventEmitter<number>();
  toggle = output<number>();

  // 带默认值的 input
  // 旧写法:@Input() label = '默认标签';
  label = input('默认标签');

  onToggle() {
    this.toggle.emit(this.todo().id);
  }
}

关键区别:

  • input() 返回一个信号(signal),所以在模板里用 todo() 而不是 todo
  • input.required<T>() 表示"这个输入是必须的,不给就报编译错误"
  • output() 替代 @Output + EventEmitter

你可能会问:“这跟 @Input 有啥本质区别?”

区别不在用法,在底层机制

@Input 的值变了,Zone.js 需要跑一轮变更检测才知道。而 input() 返回的是 signal,变了之后精准通知依赖这个输入的组件。如果你的应用大量使用 input(),变更检测的开销会大幅降低。

模板里的变化

模板几乎一样,只是 todo 变成了 todo()

<li (click)="onToggle()" [class.done]="todo().done">
  {{ todo().text }} - {{ todo().done ? '已完成' : '未完成' }}
</li>

注意 todo().done[class.done]="todo().done"——todo 是一个 signal,所以需要调用它拿到值,再访问属性。

父组件不变

父组件传值的方式没有变化:

<app-todo-item
  *ngFor="let item of items(); trackBy: trackById"
  [todo]="item"
  (toggle)="toggleDone($event)">
</app-todo-item>

[todo]="item"——[todo] 绑定到的是子组件的 input(),而 item 是父组件遍历 signal 数组拿到的普通对象。

trackBy — 列表渲染优化

看到上面代码里多了一个 trackBy: trackById 吗?

用 signal 加 *ngFor 时,推荐加上 trackBy。因为 signal 每次 update 都创建新数组,没有 trackBy 的话,Angular 会销毁所有 DOM 元素重新创建。加上 trackBy,它就知道"哦,id 为 2 的元素还是那个元素,只是数据变了,更新内容就行"。

export class TodoComponent {
  trackById = (index: number, item: TodoItem) => item.id;
}

这跟 React 的 key 属性是同一个道理。Vue 的 v-for 也推荐加 :key

完整代码一览

整合一下 Signal 版 Todo 应用的核心部分:

todo.ts

import { Service, signal, computed } from '@angular/core';
import { TodoItem } from './types';

@Service()
export class TodoService {
  private items = signal<TodoItem[]>([]);

  readonly completedCount = computed(() =>
    this.items().filter(t => t.done).length
  );

  constructor() {
    this.items.set([
      { id: 1, text: '学习 Angular Signal', done: false },
      { id: 2, text: '理解 computed', done: false },
      { id: 3, text: '学会 effect', done: true }
    ]);
  }

  getItems() {
    return this.items.asReadonly();
  }

  addTodo(text: string) {
    if (!text.trim()) return;
    this.items.update(list => [...list, { id: Date.now(), text, done: false }]);
  }

  toggleDone(id: number) {
    this.items.update(list =>
      list.map(item =>
        item.id === id ? { ...item, done: !item.done } : item
      )
    );
  }

  deleteTodo(id: number) {
    this.items.update(list => list.filter(item => item.id !== id));
  }
}

todo.ts

import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TodoItemComponent } from '../todo-item/todo-item';
import { TodoService } from '../todo.service';
import { TodoItem } from '../types';

@Component({
  selector: 'app-todo',
  standalone: true,
  imports: [CommonModule, FormsModule, TodoItemComponent],
  templateUrl: './todo.html',
  styleUrls: ['./todo.css']
})
export class TodoComponent {
  private todoService = inject(TodoService);
  items = this.todoService.getItems();
  newTodoText = '';

  trackById = (index: number, item: TodoItem) => item.id;

  toggleDone(id: number) {
    this.todoService.toggleDone(id);
  }

  addTodo() {
    this.todoService.addTodo(this.newTodoText);
    this.newTodoText = '';
  }

  deleteTodo(id: number) {
    this.todoService.deleteTodo(id);
  }
}

todo.html

<h2>Todo 列表 ({{ todoService.completedCount() }}/{{ items().length }})</h2>

<div>
  <input [(ngModel)]="newTodoText" placeholder="输入新的待办...">
  <button (click)="addTodo()">添加</button>
</div>

<ul *ngIf="items().length > 0; else emptyState">
  <app-todo-item
    *ngFor="let item of items(); trackBy: trackById"
    [todo]="item"
    (toggle)="toggleDone($event)">
  </app-todo-item>
</ul>

<ng-template #emptyState>
  <p>还没有待办,输入一条开始吧</p>
</ng-template>

注意模板里的 items()——signal 在模板里需要调用,另外 for 循环时,给每个Item添加了trackBy(类似key)。

跟第 4 篇 RxJS 版本的对比

  • RxJS 版本:items$ 是 Observable,模板用 items$ | async
  • Signal 版本:items 是 signal,模板用 items()
  • RxJS 每次修改要手动 .next(),Signal 每次修改用 .set() / .update()
  • Signal 版本少导入一个 AsyncPipe,代码更少

6. 组件生命周期

终于到了生命周期。什么是生命周期?

一个组件从"出生"到"死亡"的过程。

具体来说:

组件类被创建 → 属性初始化 → 模板渲染 → 数据变化 → 组件销毁

Angular 在这个过程的几个关键节点上安了"钩子"(hooks),你可以在这些节点上插入自己的逻辑。

有哪些钩子?

按执行顺序排列:

钩子 触发时机 常用场景
ngOnChanges @Input 属性变化时(包括第一次) 响应输入变化
ngOnInit 第一次 ngOnChanges 之后 初始化数据、调用 Service
ngDoCheck 每次变更检测时 自定义变更检测
ngAfterContentInit 内容投影初始化后 处理投影内容
ngAfterContentChecked 每次投影内容变更检测后
ngAfterViewInit 组件视图初始化后 操作 DOM、@ViewChild
ngAfterViewChecked 每次视图变更检测后
ngOnDestroy 组件销毁前 清理订阅、定时器

看起来挺多,但常用的只有四个ngOnChangesngOnInitngAfterViewInitngOnDestroy。其他的了解就行。

使用方式

先从@angular/core库中导入生命周期的接口(如下面代码导入了 OnInit 和 OnDestroy 接口),然后在组件中实现这两个接口及其方法即可。

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

@Component({
  selector: 'app-demo',
  standalone: true,
  template: `<p>{{ message }}</p>`
})
export class DemoComponent implements OnInit, OnDestroy {
  message = '';

  // 构造函数:最先执行,此时组件还没完成输入绑定
  constructor() {
    console.log('1. 构造函数');
    // 注意:这里还不能依赖 @Input 的值
  }

  // 第一次 ngOnChanges 之后执行
  ngOnInit() {
    console.log('2. ngOnInit');
    // 可以安全地使用 @Input 属性的值了
    this.message = '组件已初始化';
  }

  // 组件即将销毁
  ngOnDestroy() {
    console.log('3. ngOnDestroy');
    // 清理工作:取消订阅、清除定时器
  }
}

注意:Angular 的生命周期钩子和 React/Vue 的 Hooks 完全是两回事。

  • React HooksuseStateuseEffect):是用来"在函数组件中使用状态和副作用"的函数。它们是响应式的工具
  • Vue 组合式 APIonMountedonUnmounted):是组件生命周期的回调。
  • Angular 生命周期钩子:是组件类可以实现的可选接口。它们是组件生命周期的回调

Angular 没有 useState 这种东西——类属性的变化由变更检测自动追踪。Signals 出现后,Angular 的响应式方式跟 Vue 3 更接近了,但生命周期钩子的机制没变。

执行顺序图解

假设组件树是 AppComponent → ChildComponent

AppComponent 构造函数
  AppComponent.ngOnInit
    ChildComponent 构造函数
    ChildComponent.ngOnInit
  ChildComponent.ngAfterViewInit
  AppComponent.ngAfterViewInit  ← 最后才执行!

注意这里的细节:父组件的 ngOnInit 在子组件的 constructor 之前执行。Angular 不是"先把所有组件构建完再初始化",而是父组件先执行自己的初始化,在渲染模板的过程中才去创建子组件。所以父 ngOnInit 在前,子 constructor 在后。

顺序的规律很简单:

  • 从上到下(父先→子后):构造函数、ngOnInit——父组件先初始化,再初始化子组件
  • 从下到上(子先→父后):ngAfterViewInit——子组件的视图先就绪,父组件等所有子组件都就绪了才算数;ngOnDestroy 也是子先销毁,父后销毁

你只需要记住:ngAfterViewInit 是子组件先执行,父组件最后执行。因为父组件的视图包含了子组件,所以必须等所有子组件视图就绪,父组件才有资格说"我的视图初始化完毕了"。

这跟 Vue 的 onMounted 执行顺序类似——子组件先 mounted,父组件后 mounted。React 的 useEffect 则相反:父组件先执行 effect,子组件后执行。

用生命周期的实际例子

在 Todo 应用中,用 ngOnInit 加载数据,用 ngOnDestroy 清理:

import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { TodoService } from './todo.service';

@Component({
  selector: 'app-stats',
  standalone: true,
  template: `<p>{{ message }}</p>`
})
export class StatsComponent implements OnInit, OnDestroy {
  private todoService = inject(TodoService);
  private intervalId: any;
  message = '';

  ngOnInit() {
    // ✅ 初始化时加载数据
    this.updateStats();

    // 定时刷新统计(示例:每 30 秒)
    this.intervalId = setInterval(() => {
      this.updateStats();
    }, 30000);
  }

  private updateStats() {
    const items = this.todoService.getItems()();
    this.message = `总计: ${items.length}, 已完成: ${items.filter(i => i.done).length}`;
  }

  ngOnDestroy() {
    // ✅ 组件销毁时清理定时器,防止内存泄漏
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
  }
}

另一个常见场景:在 ngAfterViewInit 中操作 DOM:

@Component({
  selector: 'app-todo',
  standalone: true,
  template: `<input #inputRef />`
})
export class TodoComponent implements AfterViewInit {
  @ViewChild('inputRef') inputEl!: ElementRef<HTMLInputElement>;

  ngAfterViewInit() {
    // 视图初始化后,自动聚焦输入框
    this.inputEl.nativeElement.focus();
  }
}

为什么不能在 ngOnInit 里操作 DOM? 因为 ngOnInit 执行时,模板还没渲染到 DOM 上,@ViewChild 还是 undefined。必须等到 ngAfterViewInit

Vue 里等价的场景:onMounted(() => { /* DOM 可用 */ })。React 里:useEffect(() => { /* DOM 可用 */ }, [])。Angular 的 ngAfterViewInit 跟 Vue 的 onMounted 最像。

effect 和 ngOnDestroy 的配合

前面我们用了 effect 来自动监控数据变化。如果 effect 里有额外的清理逻辑,ngOnDestroy 里还需要做什么吗?

@Component({...})
export class TodoComponent implements OnDestroy {
  constructor() {
    // effect 会自动在组件销毁时清理
    effect(() => {
      console.log('数据变了:', this.items());
    });
  }

  ngOnDestroy() {
    // 手动清理非 signal 相关的东西,比如 setTimeout
    // effect 自己会处理自己的清理
  }
}

effect 会自己清理(它返回的清理函数在组件销毁时自动执行)。ngOnDestroy 主要用于清理非 Angular 资源——比如上面例子里的 setInterval、第三方库的实例、WebSocket 连接等。

生命周期的跨框架对比

操作 Angular Vue 3 React
组件创建 constructor setup() 函数体
输入属性变化 ngOnChanges watch(() => props.x) useEffect + deps
初始化 ngOnInit onMounted useEffect([], [])
DOM 可用 ngAfterViewInit onMounted useEffect([], [])
数据变化 effect watch / watchEffect useEffect + deps
销毁清理 ngOnDestroy onUnmounted useEffect cleanup

7. 本章总结

这一篇信息量不小,来串一下:

Signals 核心三件套

  • signal(value):创建响应式数据,用 () 读取,set/update 修改
  • computed(fn):计算属性,自动追踪依赖,惰性求值
  • effect(fn):监听信号变化执行副作用,自动清理

组件通信的 Signal 升级

  • input() / input.required():替代 @Input
  • output():替代 @Output + EventEmitter

生命周期(记四个就够了)

钩子 时机 干吗用
ngOnInit 组件初始化完成 调 Service 拿数据(Http)
ngAfterViewInit 视图渲染完毕 操作 DOM、操作子组件
ngOnChanges @Input 变化时 响应父组件传值变化
ngOnDestroy 组件销毁前 清理定时器/订阅/监听器

下一篇我们来路由——让你的 Angular 应用从"一个页面"变成"多个页面",并且在页面之间自由切换。

更多推荐