3. 组件通信与父子组件

组件通信是 Angular 面试必考点,也是构建复杂应用的基础。面试中主要考察四种方式:@Input/@Output(父子直接传递)、EventEmitter(事件绑定)、ViewChild(模板引用获取实例)、服务通信(跨层级共享状态)。掌握每种方式的适用场景、实现步骤和潜在陷阱即可从容应答。

组件通信方式 @Input / @Output 父子直接传值/事件 EventEmitter 自定义事件异步通信 ViewChild 获取子组件实例/方法 服务通信 共享服务 + Subject 适用关系:父子组件(Input/Output/ViewChild) vs 任意组件(服务) 记忆口诀:父子用装饰器,跨级依赖注入,视图用 ViewChild,事件用 EventEmitter 面试加分:主动说出“按需选择,避免过度耦合”体现架构意识
💡 核心原则:通信方式的选择取决于组件层级关系和数据流向。同层级或跨层级优先考虑服务通信;父子关系使用 Input/Output 更直观;需要调用子组件方法时走 ViewChild。

📌 组件通信方式一览表

方式方向核心 API典型场景
@Input/@Output父→子 / 子→父@Input 接收值,@Output 发出事件表单控件、数据展示组件
EventEmitter子→父new EventEmitter() + .emit()按钮点击、列表行操作
ViewChild父→子(反向调用)@ViewChild() + 子组件引用调用子组件方法(如打开弹窗)
服务通信任意方向Service + Subject/Observable用户状态、购物车、跨模块共享

🎯 面试问答 1

面试问题:请解释 @Input 和 @Output 是如何工作的?在实际项目中你如何选择使用它们?

面试官考察点:是否理解单向数据流与事件驱动的设计原则;能否区分输入输出场景;是否能结合项目说明使用规范。

回答框架:定义 → 方向 → 代码示例 → 适用场景 → 注意事项(命名规范、类型检查、变更检测)。

// 子组件
@Input() userName: string = '';
@Output() userChange = new EventEmitter<string>();

onUpdate(value: string) {
  this.userChange.emit(value);
}

// 父组件模板
<app-child [userName]="currentUser" 
           (userChange)="handleChange($event)"></app-child>

简短示例回答(约 90 字):@Input 从父组件接收数据,@Output 配合 EventEmitter 向父组件发射事件,遵循单向数据流。我通常在表单控件、列表行操作等明确父子关系中使用,且会为 @Output 使用 onXxx 命名,避免与原生事件冲突。

可能追问:如何对 @Input 做 setter 拦截?@Input 的变更检测如何触发?如果需要同时传多个值怎么办?

⚠️ 避坑提醒:不要在 @Input setter 中做复杂的异步操作,可能影响变更检测性能。另外 @Output 必须使用 EventEmitter 类型,否则 Angular 编译会报错。

🎯 面试问答 2

面试问题:EventEmitter 在 Angular 中的角色是什么?它和普通的 DOM 事件有何不同?

面试官考察点:对 Angular 自定义事件机制的理解;是否知道 EventEmitter 继承自 Subject;能否区分事件发射与订阅。

回答框架:定义 → 属于 RxJS Subject → 仅在 @Output 中使用 → emit 触发 → 父组件通过 (event) 绑定 → 对比原生事件:可携带任意类型数据、支持异步。

// 子组件
private notify = new EventEmitter<string>();

sendMessage(msg: string) {
  this.notify.emit(msg);
}

简短示例回答(约 85 字):EventEmitter 本质是一个 RxJS Subject,常用于 @Output 发射自定义事件。相比 DOM 事件,它可以传递任意类型数据,且发射时机完全由组件逻辑控制。在实际项目中,我会用它实现子组件“通知父组件执行操作”的模式,比如订单状态变更后的刷新回调。

可能追问:EventEmitter 为什么必须是泛型?可以手动调用 unsubscribe 吗?如果父组件不订阅会怎样?

💡 加分点:主动指出 EventEmitter 是 AsyncAction 驱动的,发射后立即被调度执行,无需担心异步顺序问题。同时提及 Angular 内置了防止内存泄漏的保护机制(组件销毁时自动完成)。

🎯 面试问答 3

面试问题:ViewChild 和服务通信分别适用于什么场景?请举例说明。

面试官考察点:能否区分模板引用与依赖注入两种模式的适用边界;是否理解 ViewChild 仅用于父子,服务通信是跨组件最佳实践。

回答框架:分两点阐述 → ViewChild:获取子组件实例、调用方法、访问属性,生命周期用 ngAfterViewInit → 服务通信:创建共享服务 + Subject,通过订阅实现任意组件数据联动 → 对比:一个强耦合但直接,一个松耦合但需要管理订阅。

// ViewChild 示例
@ViewChild('childRef') childComp!: ChildComponent;

ngAfterViewInit() {
  this.childComp.someMethod();
}

// 服务通信示例
@Injectable()
export class SharedService {
  private data$ = new Subject<string>();
  sendData(v: string) { this.data$.next(v); }
  getData() { return this.data$.asObservable(); }
}

简短示例回答(约 95 字):ViewChild 用于父组件直接调用子组件的公共方法,比如打开子组件的模态框;服务通信则用于不相关的兄弟组件或跨层级组件,比如用户登录状态共享。我会优先用服务通信实现松耦合,只在确实需要调用子组件方法时才用 ViewChild。

可能追问:ViewChild 为什么必须在 ngAfterViewInit 之后才能访问?服务通信中 Subject 和 BehaviorSubject 的选择?如果组件销毁订阅未取消会怎样?

⚠️ 关键避坑:ViewChild 配合 *ngIf 或动态组件时需设置 { static: false },否则可能拿不到实例。服务通信务必在 ngOnDestroy 中取消订阅,避免内存泄漏。
组件通信追问路径图 你用了哪种通信? @Input/@Output 深入 setter & 变更检测 EventEmitter 对比 Subject / 内存泄漏 服务通信 Subject vs BehaviorSubject 反问面试官常见陷阱:异步时序、组件销毁、变更检测策略、注入层级