Angular 快速入门:组件间通信
Angular 快速入门:组件间通信
前两篇我们用 Todo 应用学完了模板、绑定、指令。但到目前为止,所有代码都写在 TodoComponent 这一个组件里。
真实项目里没人这么干。一个组件应该只负责一件事。比如:
AppComponent
└── TodoComponent(待办列表容器)
└── TodoItemComponent(单个待办项)
但问题来了:组件之间怎么"说话"?
- 父组件怎么把数据传给子组件?
- 子组件怎么通知父组件"用户点了删除"?
- 非父子关系的组件怎么共享数据?
这篇我们一个个解决。
1. 父传子:@Input
先从最简单的场景开始:TodoComponent 把每一条待办数据传给 TodoItemComponent。
第一步:创建子组件
ng g c todo-item
这会生成 app/todo-item/ 目录。
第二步:用 @Input 声明输入属性
打开 todo-item/todo-item.ts,加上 @Input 装饰器:
import { Component, Input } from '@angular/core'; // ← 导入 Input
@Component({
selector: 'app-todo-item',
standalone: true,
imports: [],
templateUrl: './todo-item.html',
styleUrls: ['./todo-item.css']
})
export class TodoItemComponent {
@Input() todo: any; // ← 声明一个输入属性
}
@Input() todo 的意思是:这个组件的 todo 属性,可以由父组件通过属性绑定传进来。
模板 todo-item.html 先简单展示数据:
<li>
{{ todo.text }} - {{ todo.done ? '已完成' : '未完成' }}
</li>
第三步:父组件传值
回到 TodoComponent,先导入子组件,然后在模板里使用:
修改 todo/todo.ts:
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { TodoItemComponent } from '../todo-item/todo-item'; // ← 导入子组件
@Component({
selector: 'app-todo',
standalone: true,
imports: [CommonModule, FormsModule, TodoItemComponent], // ← 加上子组件
templateUrl: './todo.html',
styleUrls: ['./todo.css']
})
export class TodoComponent {
// ... 之前的代码不变
}
修改 todo/todo.html,把原来的 <li> 换成自定义组件,用 [todo] 传值:
<ul *ngIf="todos.length > 0; else emptyState">
<app-todo-item
*ngFor="let item of todos"
[todo]="item">
</app-todo-item>
</ul>
看明白了吗?[todo]="item"——方括号是属性绑定,把 item 绑定到子组件的 @Input() todo 上。
跨框架对比:Vue 的
props: ['todo'],父组件传<Child :todo="item" />;React 直接<Child todo={item} />,子组件用props.todo接收。Angular 用@Input()装饰器显式声明,用的也是方括号[]绑定——跟前一篇学的属性绑定是同一个机制,只不过这次绑到的是子组件的输入属性,不是 HTML 原生属性。
给类型加上去
刚才写的是 @Input() todo: any,不够好。Angular 推荐用 TypeScript 接口定义数据结构。
在 app/ 下建一个 types.ts:
export interface Todo {
id: number;
text: string;
done: boolean;
}
然后在组件里用:
import { Todo } from '../types';
@Input() todo!: Todo;
! 号是 TypeScript 的"非空断言",意思是"这个属性一定会在使用前被赋值"(Angular 会在初始化时由父组件传入)。
2. 子传父:@Output + EventEmitter
现在数据传进去了。但用户点击待办项时,怎么通知父组件切换状态?
子组件自己改不了数据(数据在父组件的 todos 数组里)。它需要发射一个事件,让父组件来处理。
在子组件里声明事件
修改 todo-item/todo-item.ts:
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-todo-item',
standalone: true,
imports: [CommonModule], // ← 在模板里用 class 绑定需要 CommonModule
templateUrl: './todo-item.html',
styleUrls: ['./todo-item.css']
})
export class TodoItemComponent {
@Input() todo: any;
@Output() toggle = new EventEmitter<number>(); // ← 声明输出事件
}
EventEmitter<number>() 的意思是:这个事件发射时,会带一个 number 类型的值(待办的 id)。
在模板里触发事件
<li (click)="onToggle()" [class.done]="todo.done">
{{ todo.text }} - {{ todo.done ? '已完成' : '未完成' }}
</li>
对应的组件方法:
export class TodoItemComponent {
@Input() todo: any;
@Output() toggle = new EventEmitter<number>();
onToggle() {
this.toggle.emit(this.todo.id); // ← 发射事件,带上 id
}
}
父组件监听事件
回到 TodoComponent 的模板,用圆括号监听 toggle 事件:
<app-todo-item
*ngFor="let item of todos"
[todo]="item"
(toggle)="toggleDone($event)">
</app-todo-item>
(toggle)="toggleDone($event)"——圆括号是事件绑定,跟监听原生 click 事件一个道理。$event 就是子组件 emit 出来的值(如 id)。
跨框架对比:
- Vue:子组件
this.$emit('toggle', id),父组件@toggle="handleToggle"。- React:父传一个函数
onToggle={handleToggle},子组件调用props.onToggle(id)。- Angular:
@Output() toggle = new EventEmitter<T>(),父组件(toggle)="fn($event)"。Angular 和 Vue 的"事件发射"模式最像——都叫
emit。React 更直白,就是传回调函数。写法不同,本质一样:子组件发信号,父组件响应。
再加一个删除功能
子组件加一个删除按钮和对应事件:
<li (click)="onToggle()" [class.done]="todo.done">
{{ todo.text }} - {{ todo.done ? '已完成' : '未完成' }}
<button (click)="onDelete(); $event.stopPropagation()">✕</button>
</li>
注意到 (click) 里写了两个语句,用分号隔开:onDelete(); $event.stopPropagation()。
Angular 的事件绑定支持写多个语句,按顺序执行。 这里是先删除(onDelete()),再阻止事件冒泡($event.stopPropagation())。
为什么要阻止冒泡?<button> 嵌在 <li> 里,而 <li> 自己也有 (click) 事件。如果不阻止冒泡,点击删除按钮会同时触发 <li> 的 onToggle()——结果就是待办被删了,但 Angular 还是会尝试执行切换,可能报错。stopPropagation() 就是防止事件"冒泡"到父元素。
对比其他框架:
- Vue:用事件修饰符
@click.stop="onDelete",更简洁但不够灵活。- React:不支持模板里写多语句,通常在函数里自己调
e.stopPropagation()。- Angular:支持模板里写
fn(); $event.preventDefault()这种链式调用。分号左边执行逻辑,右边处理事件对象,是一种常见写法。
@Output() delete = new EventEmitter<number>();
onDelete() {
this.delete.emit(this.todo.id);
}
父组件模板加上 (delete):
<app-todo-item
*ngFor="let item of todos"
[todo]="item"
(toggle)="toggleDone($event)"
(delete)="deleteTodo($event)">
</app-todo-item>
父组件加删除方法:
deleteTodo(id: number) {
this.todos = this.todos.filter(item => item.id !== id);
}
@Input / @Output 完整图解
用一张图来理解:
父组件(TodoComponent)
│
├─ [todo]="item" → 传入数据 → 子组件的 @Input() todo
│
├─ (toggle)="fn($event)" ← 监听事件 ← 子组件的 @Output() toggle
│
└─ (delete)="fn($event)" ← 监听事件 ← 子组件的 @Output() delete
父传子用属性绑定 [],子传父用事件绑定 ()。 跟绑定原生 HTML 属性/事件用的是同一套语法。
3. @ViewChild — 父组件直接拿子组件
有时候父组件需要直接调用子组件的方法或访问它的属性。这时候用 @ViewChild。
比如给 TodoItemComponent 加一个高亮方法:
export class TodoItemComponent {
@Input() todo: any;
@Output() toggle = new EventEmitter<number>();
highlight() {
console.log(`高亮待办: ${this.todo.text}`);
}
}
父组件里用 @ViewChild 获取子组件实例:
import { Component, ViewChild } from '@angular/core';
import { TodoItemComponent } from '../todo-item/todo-item';
export class TodoComponent {
@ViewChild(TodoItemComponent) child!: TodoItemComponent;
someMethod() {
this.child.highlight(); // 直接调用子组件的方法
}
}
但这里有个坑:如果有多个同类型的子组件(*ngFor 渲染出来的),@ViewChild 只返回第一个。要拿所有的,用 @ViewChildren:
import { Component, ViewChildren, QueryList } from '@angular/core';
@ViewChildren(TodoItemComponent) children!: QueryList<TodoItemComponent>;
@ViewChild对比其他框架:React 的useRef和 Vue 的$refs类似——都是拿到子组件/子元素的引用。但 Angular 的@ViewChild在视图初始化后才能访问,不是一创建就能用。后面讲生命周期时再细说。
4. 模板引用变量 #var — 最简单的引用
如果只是想在模板里引用某个元素或组件,可以用 # 语法。
<input #newInput [(ngModel)]="newTodoText" placeholder="输入新的待办...">
<button (click)="addTodo(); newInput.focus()">添加</button>
#newInput 就是一个模板引用变量,直接在模板层面引用 <input> 元素,调用它的 focus() 方法。
也可以引用子组件:
<app-todo-item #item *ngFor="..." [todo]="item"></app-todo-item>
但模板引用变量只能在模板范围内用,组件类里拿不到。需要组件类里用的场景,就用 @ViewChild。
5. 非父子组件通信:Service
如果两个组件没有直接的父子关系——比如一个是 Header,一个是 Todo 列表——怎么通信?
答案是用 Service(服务)作为中介。这是 Angular 最推荐的方案。
这块内容比较多,下一篇讲依赖注入时会详细展开。这里先给你一个直觉:
组件A ──调用──→ Service(共享数据) ←──调用── 组件B
两个组件都注入同一个 Service,通过 Service 的数据或事件流来通信。Vue 里类似 Vuex/Pinia,React 里类似 Context/Redux,但 Angular 用 Service + RxJS 就能实现,不需要额外的状态管理库。
6.完整代码:重构后 Todo 应用
把上面拆出来的 TodoItemComponent 汇总一下。
todo-item/todo-item.ts:
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';
@Component({
selector: 'app-todo-item',
standalone: true,
imports: [CommonModule],
templateUrl: './todo-item.html',
styleUrls: ['./todo-item.css']
})
export class TodoItemComponent {
@Input() todo: any;
@Output() toggle = new EventEmitter<number>();
@Output() delete = new EventEmitter<number>();
onToggle() {
this.toggle.emit(this.todo.id);
}
onDelete() {
this.delete.emit(this.todo.id);
}
}
todo-item/todo-item.html:
<li (click)="onToggle()" [class.done]="todo.done">
{{ todo.text }} - {{ todo.done ? '已完成' : '未完成' }}
<button (click)="onDelete(); $event.stopPropagation()">✕</button>
</li>
todo-item/todo-item.css:
.done {
text-decoration: line-through;
color: #999;
}
todo/todo.ts 核心部分:
import { TodoItemComponent } from '../todo-item/todo-item';
@Component({
// ...
imports: [CommonModule, FormsModule, TodoItemComponent]
})
export class TodoComponent {
// ... title, newTodoText, todos ...
toggleDone(id: number) { /* ... */ }
addTodo() { /* ... */ }
deleteTodo(id: number) {
this.todos = this.todos.filter(item => item.id !== id);
}
}
todo/todo.html:
<h2>{{ title }}</h2>
<div>
<input [(ngModel)]="newTodoText" placeholder="输入新的待办...">
<button (click)="addTodo()">添加</button>
</div>
<ul *ngIf="todos.length > 0; else emptyState">
<app-todo-item
*ngFor="let item of todos"
[todo]="item"
(toggle)="toggleDone($event)"
(delete)="deleteTodo($event)">
</app-todo-item>
</ul>
<ng-template #emptyState>
<p>还没有待办,输入一条开始吧 🎉</p>
</ng-template>
这篇我们用 TodoItemComponent 的拆分,把组件间通信的几种方式都串了一遍。
7.本章总结
核心就三句话:
@Input:父组件传数据给子组件,用[]绑定@Output+EventEmitter:子组件发信号给父组件,用()监听@ViewChild/#var:引用子组件或 DOM 元素
下一章我们来讲服务与依赖注入——怎么把数据逻辑从组件里抽出来,变成一个可复用的 Service,以及组件之间通过 Service 通信的完整方案。
更多推荐



所有评论(0)