首页
/ Angular 组件输出(output)完全指南:用 output() 函数打通子组件到父组件的通信链路

Angular 组件输出(output)完全指南:用 output() 函数打通子组件到父组件的通信链路

2026-09-07 12:03:55作者:范靓好Udolf

组件之间的通信是 Angular 应用开发的核心基本功:当子组件内部发生了"点击了按钮""列表新增/移除了某项""某个重要状态更新了"等事件时,需要把这一信息向上通知给父组件。本文基于 Angular 官方入门教程 Learn Angular 的 Component output properties(第 9 步),完整讲解用 output() 函数定义组件输出属性、用 emit() 派发事件、用模板事件绑定接收事件的完整链路,并结合本仓库中 output() 的真实实现源码,带你从"会写"进阶到"懂原理"。

读完后你将掌握:如何在子组件中声明类型安全的输出事件、如何在父组件模板中通过 (eventName)="handler($event)" 语法订阅事件、事件数据在两层组件间的流转方式,以及 output() 底层 OutputEmitterRef 的工作机制与命名、别名、编程式订阅等进阶技巧。

为什么需要组件输出(Output)

在父子组件结构中,数据与事件的流向是分层的:

  • 父 → 子(属性下行):通过 input()@Input 把数据传给子组件(对应 Learn Angular 第 8 步 Component input);
  • 子 → 父(事件上行):子组件不能直接修改父组件的状态,只能通过"发射事件"通知父组件"某件事发生了",由父组件决定如何响应。实现这条上行通道的方式就是 output(输出属性)

Learn Angular 教程 的组件基础学习路径中,第 9 步(adev/src/content/tutorials/learn-angular/steps/9-output/README.md)正是负责讲解这一环——它是 8 步 input(数据流入)、9 步 output(事件流出)之后整个组件通信基础链路的收官,完成后即具备了组件交互的全部基础能力。

核心概念:output() 函数与自定义事件

Angular 使用 output() 函数来开启"子组件通知父组件"的能力。它替代了旧版 @Output + EventEmitter 的声明方式,使用起来更简洁、更符合现代组件编写的习惯。

创建一个输出属性的基本形式是:用 output 函数初始化一个类属性(class property),并声明该事件将要携带的数据类型:

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

@Component({...})
class Child {
  incrementCountEvent = output<number>();
}

<number> 是事件的泛型类型。不带泛型时等价于 output<void>(),表示"只通知事件发生了,不携带数据"。

创建完成后,子组件便可以在内部任意位置"触发"事件,触发方式是调用输出属性上的 emit 方法:

class Child {
  count = 0;

  onClick() {
    this.count++;
    this.incrementCountEvent.emit(this.count);
  }
}

emit 派发的事件携带的数据类型与声明输出时的泛型一致——在本例中,父组件监听到事件后收到的 $event 就是 number 类型的计数值。

output() 的声明式来源:从源码看真实签名

output() 并非黑盒魔法。查看其真实实现 packages/core/src/authoring/output/output.ts

export function output<T = void>(opts?: OutputOptions): OutputEmitterRef<T> {
  ngDevMode && assertInInjectionContext(output);
  return new OutputEmitterRef<T>();
}

从源码可以确认三个事实:

  1. 泛型默认值是 voidoutput()output<void>() 完全等价,只通知不传值;
  2. 支持可选的 OutputOptions:目前该类型只含一个可选字段 alias,用于自定义事件在模板中的外部名称(见下文"自定义事件名");
  3. 必须在注入上下文中调用:源码第一行 assertInInjectionContext(output) 表明,output() 应当在组件/指令的字段初始化位置(即类构造期间)调用。如果在非注入上下文(例如普通函数里)调用,开发模式下会直接报错,从而避免运行时意外。

上手实战:让"添加海龟"按钮打通父子通信

教程中的第 9 步提供了一个可编辑的练习(config.json 将任务打开为 child.tsapp.ts 两个文件)。场景是:App(父)展示一个列表,Child(子)渲染一个 "Add Item" 按钮,点击按钮后父组件需要把新项加入列表。开始时 Child 还没有任何输出能力,addItem() 方法为空、模板里的 <app-child /> 也没有事件监听,一切都等待你来补齐。

练习按以下三个步骤依次完成。

第 1 步:在子组件中添加 output() 属性

修改 child.ts,添加一个名为 addItemEvent 的输出属性,并把输出类型设置为 string

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

@Component({
  selector: 'app-child',
  styles: `
    .btn {
      padding: 5px;
    }
  `,
  template: ` <button class="btn" (click)="addItem()">Add Item</button> `,
})
export class Child {
  readonly addItemEvent = output<string>();

  addItem() {}
}

建议加上 readonly 修饰:输出属性只允许由组件自身调用 emit,不允许外部重新赋值,readonly 能在编译期就约束这一语义(最终解答 answer/src/app/child.ts 正是如此声明)。

第 2 步:完成 addItem 方法并派发事件

让按钮点击真正产生一个携带字符串数据的输出事件:

addItem() {
  this.addItemEvent.emit('🐢');
}

模板里的 (click)="addItem()" 一旦被触发,addItem() 就会调用 addItemEvent.emit('🐢'),向父组件发出一个值为字符串 '🐢' 的事件。这就是本教程约定的"新增一个列表项"的信号。

第 3 步:在父组件模板中监听事件

修改 app.ts 的模板,为 <app-child /> 添加事件绑定:

<app-child (addItemEvent)="addItem($event)" />

这里用到了 Angular 模板中监听自定义事件的标准语法:圆括号包裹事件名 = 回调函数addItemEvent 是子组件暴露的输出属性名,addItem($event) 是父组件提供的处理方法,$event 中装的就是子组件 emit 出来的 '🐢' 字符串。

配合父组件既有的处理逻辑:

import {Component} from '@angular/core';
import {Child} from './child';

@Component({
  selector: 'app-root',
  template: `
    <app-child (addItemEvent)="addItem($event)" />
    <p>🐢 all the way down {{ items.length }}</p>
  `,
  imports: [Child],
})
export class App {
  items = new Array();

  addItem(item: string) {
    this.items.push(item);
  }
}

至此整条链路已打通:每点击一次 "Add Item" 按钮,Child.addItem() 便 emit 一个字符串;父组件的 addItem 方法把收到的值 pushitems 数组;页面上的 {{ items.length }} 会随事件驱动自动更新渲染。这正是事件驱动的 Angular 应用中典型的"内部状态变化 → 事件通知 → 父组件响应"模式。

$event 与事件数据:类型安全的双向约定

注意到上例中父子双方形成了一种隐式约定:

  • 子组件声明 output<string>(),保证 emit 出去的值永远是 string
  • 父组件用 addItem(item: string) 接收,保证拿到的 $eventstring 处理。

这层约定是类型安全的:如果某天 output<string> 被改成 output<number>,TypeScript 编译器会立即提示两处需要同步调整的地方,避免"改了一端、另一端悄悄坏掉"的隐患。如果确实不需要传数据,直接声明 output() 即可,父组件侧也无需使用 $event

深挖原理:OutputEmitterRef 内部是如何工作的

output() 返回的对象类型是 OutputEmitterRef<T>,它的实现位于 packages/core/src/authoring/output/output_emitter_ref.ts。阅读这份实现,可以直观理解一次事件从订阅到派发的完整生命周期:

  • 监听器列表:内部用 listeners: Array<(value: T) => void> | null 保存所有订阅者,模板绑定、编程式 subscribe 最终都会注册为这里的回调;
  • 自动清理:构造时通过 destroyRef.onDestroy(...) 注册清理逻辑,组件/指令销毁时监听器列表被置空并标记 destroyed,避免内存泄漏与"对已销毁组件发事件"的诡异问题;
  • emit 的健壮性emit 会逐个调用监听器,单个监听器抛错会被注入的 ErrorHandler 捕获而不影响其余监听器执行;对已销毁的 ref 再 emit 只会输出一条开发警告而不会崩溃;
  • emit 期间退订:为了防止在遍历监听器的过程中修改数组导致错乱,实现采用"先把退订位置置为 null,派发结束后再统一剔除"的策略(removeNullValues)——这是源码中值得学习的边界处理细节;
  • 与信号机制的关系emit 执行期间会通过 setActiveConsumer(null) 暂时脱离信号响应上下文,保证事件监听回调里对信号值的读取不会被误追踪为副作用依赖。

这些实现细节意味着:对使用者来说,output() 声明的事件天然具备"随组件生命周期自动管理、单监听器出错不影响整体派发、类型安全"等优良特性,无需自己手写一套发布订阅。

进阶扩展:事件名、别名与编程式订阅

官方组件指南 guide/components/outputs 对输出属性提供了更完整的延伸用法,在这里一并整理。

自定义事件名(别名)

output() 的可选参数 alias 允许你在不改动 TypeScript 属性名的前提下,为模板中的事件换一个对外名称:

@Component({...})
export class CustomSlider {
  changed = output({alias: 'valueChanged'});
}
<custom-slider (valueChanged)="saveVolume()" />

alias 只影响模板中的绑定名,TypeScript 代码中仍然使用属性名 changed 调用 emit。别名主要用在两种场景:为重构后的新属性保留旧名称以兼容既有模板,或避免与原生 DOM 事件(如 clickchange)撞名造成混淆。

以编程方式订阅输出

除了模板事件绑定,output() 返回的 OutputRef 还暴露了 subscribe 方法,适合动态创建组件后以命令式方式监听事件:

const someComponentRef: ComponentRef<SomeComponent> = viewContainerRef.createComponent(/*...*/);

someComponentRef.instance.someEventProperty.subscribe((eventData) => {
  console.log(eventData);
});

订阅会在宿主组件销毁时由 Angular 自动清理;也可以手动退订——subscribe 返回的 OutputRefSubscription 带有 unsubscribe 方法:

const eventSubscription = someComponent.someEventProperty.subscribe((eventData) => {
  console.log(eventData);
});

// 需要提前结束时手动退订
eventSubscription.unsubscribe();

这正是 OutputEmitterRefsubscribe/unsubscribe 实现(见 output_emitter_ref.ts)对外暴露的能力。

事件命名的三条建议

指南中对输出事件命名给出明确约定,值得作为团队规范:

  1. 避免与原生 DOM 事件撞名(如 clickkeydown),否则模板中无法区分事件属于组件还是 DOM 元素;
  2. 不需要像组件选择器那样加前缀——一个元素只能承载一个组件,其上的自定义事件理应全部归属于该组件;
  3. 统一使用 camelCase,且不要加 on 前缀,如 valueChangeditemRemoved

传统方案 @Output + EventEmitter 依然可用

若你的项目仍在使用旧式装饰器风格,官方指南明确说明 @Output 写法完全受支持,只是新项目推荐优先使用 output() 函数:

@Component(/* ... */)
export class ExpandablePanel {
  @Output() panelClosed = new EventEmitter<void>();
}

相应的别名写法为 @Output('valueChanged') changed = new EventEmitter<number>()。此外,无论哪种声明方式,都可以通过 @Component 装饰器中的 outputs 数组属性来声明输出,这在"子类继承基类中的输出属性"场景下特别有用——具体示例同样见 guide/components/outputs

小结:组件基础通信的收官一步

本步位于 Learn Angular 教程第 9 步(9-output),与其前一课 8-input 共同构成了"输入数据 + 输出事件"的组件通信闭环。完整解答代码可在 answer/src/app/child.tsanswer/src/app/app.ts 中对照校验。

回顾本课核心要点:

环节 关键 API 作用
声明输出 output<T>() 定义类型安全的输出属性,返回 OutputEmitterRef<T>
派发事件 ref.emit(value) 向所有监听者广播数据,value 类型与 T 一致
模板订阅 (name)="handler($event)" 父组件模板中监听子组件事件并接收 $event
编程式订阅 ref.subscribe(cb) / unsubscribe() 动态场景下命令式监听与手动退订

至此,组件基础能力(输入、输出、模板、数据流)已经集齐。后续课程将在此基础上继续探索延迟视图、路由、表单与服务注入等进阶主题,而这些上层能力无一不是建立在"input 下行数据、output 上行事件"这套扎实的通信模型之上的。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388