先记住这个答案
父组件在模板中使用属性绑定 [childProp]="parentValue" 将数据传给子组件,子组件通过 @Input() 声明接收。子组件可通过 @Output() someEvent = new EventEmitter<T>() 定义事件,并在合适时机调用 emit(value) 发送通知;父组件在模板中通过 (someEvent)="handler($event)" 绑定处理函数,Angular 会在事件触发时自动调用 handler。
- 输入属性单向传值,修改需由父组件处理。
- 输出事件通过 EventEmitter.emit 同步触发。
- 父组件用事件绑定接收并对 $event 处理。
@Input 与 @Output 的运行机制
@Input() 装饰器将属性标记为可被父组件静态绑定。Angular 在变更检测时发现父组件的绑定值变化,就会从父组件更新到子组件的属性。注意这种传递总是单向的,子组件内部若给输入属性赋值,不会同步回父组件,只会影响自身当前实例。因此输入属性的变化只能在父组件侧引起,子组件需要修改时应该通过事件通知父组件来改。
@Output() 装饰器定义的属性必须是 EventEmitter 类实例,通常命名时避免以 on 开头。当子组件调用 emitter.emit(value) 时,Angular 会同步触发父组件模板中绑定的处理函数,$event 就是传入的 value。这一过程基于 Angular 的事件绑定机制,不是 DOM 事件冒泡,处理函数是在父组件的上下文中执行的。
如果想给事件附加额外信息,可直接在 emit 参数中传对象。父组件处理函数若需要访问当前组件,可使用箭头函数或在类中定义方法,但 Angular 已确保 this 正确指向父组件。
购物车数量选择器的实践
假设父组件显示商品信息,子组件是带有加减按钮的数量选择器。父组件将库存上限 stockUpperLimit 传给子组件的 @Input max。子组件内部维护当前数量 count,用户点击加号时若 count < max 才允许增加,随后通过 @Output() countChange = new EventEmitter<number>() 把新 count 传给父组件。父组件模板中写 (countChange)="onCountChange($event)"。
父组件的 onCountChange 收到新值后,先校验不越界,再调用购物车服务更新后端购物车项,同时修改父组件中的总价等派生数据。由于子组件不会直接改父组件的输入对象,而是通过事件请求修改,保证了数据流的清晰和单向性。当商品被移除或库存变化时,父组件会重新绑定一个新的 max,子组件 ngAfterViewChecked 或 ngOnChanges 中需处理限制。
何时容易失效及避免措施
当把数组或对象作为输入传给子组件时,子组件若直接修改其属性(例如 items.push)会同时改变父组件中的对象,因为共享引用。这会让输入数据流变得难以追踪,也容易造成不一致。正确做法是子组件用 Object.assign 或解构创建副本,只修改自己的副本,然后通过 emit 把新数据交给父组件,由父组件替换整个对象。
另一个失效点是输出事件触发后,父组件处理函数中对父子进行同步修改可能触发循环变更检测错误。Angular 的差异比较器在开发模式下检测到值循环变化时会报错。通常可把发送事件放到 setTimeout 中异步触发,或调整数据结构使父组件更新不引起子组件输出再次变化。
容易答错的地方
- 认为 @Output 可以像方法调用一样返回值
- @Output 是事件发射器,调用 emit 只发送通知,没有返回值。若需要父子组件间的参数回传,应让父组件将回调函数通过 @Input 传入,或通过双向绑定语法(如 [(value)])配合升级事件,而不是依赖事件返回值。
- 误以为 EventEmitter emit 是异步的
- EventEmitter.emit 会同步调用已注册的监听器,包括父组件绑定的事件处理函数。所有监听器执行完毕后,emit 才返回。如果需要在处理函数后执行某些逻辑,必须通过 Promise.resolve 或 setTimeout 手动制造异步点,而不是依赖 emit 本身的特性。
面试官还会怎么问?
Angular 中输出事件名要不要加 on 前缀?
推荐不加 on。Angular 官方指南中虽然允许,但事件名应描述发生了什么,例如 countChange 而不是 onCountChange。组件模板中绑定事件时通常读起来更自然,也与内置事件如 (click) 保持一致。
如果子组件要同步父组件的输入,哪种方式最稳妥?
最稳妥的是子组件不直接修改输入属性,而是通过输出事件把新值传给父组件,父组件负责更新模型。若只是本地临时显示,可把输入赋值给内部属性,通过 ngOnChanges 监听变化再同步,避免直接写回原属性。
@Input 的值在子组件中何时可以访问?
在 ngOnInit 中已可用,但在 constructor 中不可访问,因为 Angular 尚未将绑定值赋值。更早的时序可在 ngOnChanges 中首次被调用时访问,此时输入属性已成功传入。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。