委托:EventEmitter或可观察到的Angular


243

我正在尝试在Angular中实现类似委托模式的功能。当用户单击时nav-item,我想调用一个函数,该函数然后发出一个事件,而该事件又应由侦听该事件的其他某些组件处理。

这是场景:我有一个Navigation组件:

import {Component, Output, EventEmitter} from 'angular2/core';

@Component({
    // other properties left out for brevity
    events : ['navchange'], 
    template:`
      <div class="nav-item" (click)="selectedNavItem(1)"></div>
    `
})

export class Navigation {

    @Output() navchange: EventEmitter<number> = new EventEmitter();

    selectedNavItem(item: number) {
        console.log('selected nav item ' + item);
        this.navchange.emit(item)
    }

}

这是观察部分:

export class ObservingComponent {

  // How do I observe the event ? 
  // <----------Observe/Register Event ?-------->

  public selectedNavItem(item: number) {
    console.log('item index changed!');
  }

}

关键问题是,如何使观察组件观察到相关事件?


Answers:


449

更新2016-06-27:代替使用Observables

  • @Abdulrahman在评论中建议的BehaviorSubject,或者
  • @Jason Goemaat在评论中建议的ReplaySubject

一个Subject既是一个Observable(因此我们可以subscribe()做到)又是Observer(因此我们可以调用next()它以发出新值)。我们利用此功能。主题允许将值多播到许多观察者。我们不会利用此功能(我们只有一个观察者)。

BehaviorSubject是Subject的变体。它具有“当前值”的概念。我们利用这一点:每当创建ObservingComponent时,它都会自动从BehaviorSubject获取当前的导航项值。

下面的代码和插件使用BehaviorSubject。

ReplaySubject是Subject的另一个变体。如果要等到实际生成一个值,请使用ReplaySubject(1)。BehaviorSubject需要一个初始值(将立即提供),而ReplaySubject则不需要。ReplaySubject将始终提供最新值,但是由于它没有必需的初始值,因此该服务可以在返回其第一个值之前执行一些异步操作。它仍将立即在具有最新值的后续调用中触发。如果只需要一个值,请first()在订阅上使用。如果使用,则不必取消订阅first()

import {Injectable}      from '@angular/core'
import {BehaviorSubject} from 'rxjs/BehaviorSubject';

@Injectable()
export class NavService {
  // Observable navItem source
  private _navItemSource = new BehaviorSubject<number>(0);
  // Observable navItem stream
  navItem$ = this._navItemSource.asObservable();
  // service command
  changeNav(number) {
    this._navItemSource.next(number);
  }
}
import {Component}    from '@angular/core';
import {NavService}   from './nav.service';
import {Subscription} from 'rxjs/Subscription';

@Component({
  selector: 'obs-comp',
  template: `obs component, item: {{item}}`
})
export class ObservingComponent {
  item: number;
  subscription:Subscription;
  constructor(private _navService:NavService) {}
  ngOnInit() {
    this.subscription = this._navService.navItem$
       .subscribe(item => this.item = item)
  }
  ngOnDestroy() {
    // prevent memory leak when component is destroyed
    this.subscription.unsubscribe();
  }
}
@Component({
  selector: 'my-nav',
  template:`
    <div class="nav-item" (click)="selectedNavItem(1)">nav 1 (click me)</div>
    <div class="nav-item" (click)="selectedNavItem(2)">nav 2 (click me)</div>`
})
export class Navigation {
  item = 1;
  constructor(private _navService:NavService) {}
  selectedNavItem(item: number) {
    console.log('selected nav item ' + item);
    this._navService.changeNav(item);
  }
}

Plunker


使用Observable的原始答案:(与使用BehaviorSubject相比,它需要更多的代码和逻辑,因此我不建议这样做,但可能有启发性)

因此,这是一个使用Observable 而不是EventEmitter的实现。与我的EventEmitter实现不同,此实现还将当前选定的内容存储navItem在服务中,以便在创建观察组件时,可以通过API调用检索当前值navItem(),然后通过navChange$Observable 通知更改。

import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/share';
import {Observer} from 'rxjs/Observer';

export class NavService {
  private _navItem = 0;
  navChange$: Observable<number>;
  private _observer: Observer;
  constructor() {
    this.navChange$ = new Observable(observer =>
      this._observer = observer).share();
    // share() allows multiple subscribers
  }
  changeNav(number) {
    this._navItem = number;
    this._observer.next(number);
  }
  navItem() {
    return this._navItem;
  }
}

@Component({
  selector: 'obs-comp',
  template: `obs component, item: {{item}}`
})
export class ObservingComponent {
  item: number;
  subscription: any;
  constructor(private _navService:NavService) {}
  ngOnInit() {
    this.item = this._navService.navItem();
    this.subscription = this._navService.navChange$.subscribe(
      item => this.selectedNavItem(item));
  }
  selectedNavItem(item: number) {
    this.item = item;
  }
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

@Component({
  selector: 'my-nav',
  template:`
    <div class="nav-item" (click)="selectedNavItem(1)">nav 1 (click me)</div>
    <div class="nav-item" (click)="selectedNavItem(2)">nav 2 (click me)</div>
  `,
})
export class Navigation {
  item:number;
  constructor(private _navService:NavService) {}
  selectedNavItem(item: number) {
    console.log('selected nav item ' + item);
    this._navService.changeNav(item);
  }
}

Plunker


另请参阅Component Interaction Cookbook示例,该示例Subject除了可观察对象之外还使用a 。尽管该示例是“父母与孩子之间的交流”,但相同的技术适用于无关的组件。


3
在对Angular2问题的评论中反复提到,不建议在EventEmitter输出之外的任何地方使用它。他们目前正在重写教程(服务器通信AFAIR),以免鼓励这种做法。
君特Zöchbauer

2
有没有办法初始化服务并从上面的示例代码中的Navigation组件中触发第一个事件?问题是_observer服务对象的至少在ngOnInit()调用Navigation组件时未初始化。
ComFreek

4
我可以建议使用BehaviorSubject而不是Observable。EventEmitter之所以接近,是因为它“很热”意味着它已经“共享”,它旨在保存当前值,并且最终实现了Observable和Observer,这将为您节省至少五行代码和两个属性
Abdulrahman Alsoghayer

2
@PankajParkar,关于“更改检测引擎如何知道可观察对象发生了更改” —我删除了先前的评论响应。我最近了解到Angular不会进行猴子补丁subscribe(),因此无法检测到何时可观察到的变化。通常,会触发一些异步事件(在我的示例代码中是按钮单击事件),并且关联的回调将在可观察对象上调用next()。但是,更改检测的运行是由于异步事件,而不是由于可观察到的更改。另请参阅Günter评论:stackoverflow.com/a/36846501/215945
Mark Rajcok,2016年

9
如果您要等到实际产生一个值,可以使用ReplaySubject(1)。A BehaviorSubject要求立即提供一个初始值。,ReplaySubject(1)它将始终提供最新的值,但不需要初始值,因此服务可以在返回其第一个值之前执行一些异步操作,但仍会立即在具有最后一个值的后续调用上触发。如果您仅对一个值感兴趣,则可以first()在订阅上使用,而不必在最后取消订阅,因为这样就可以完成。
杰森·古玛

33

最新消息:我添加了另一个答案,该答案使用Observable而不是EventEmitter。我建议在这个答案之上。实际上,在服务中使用EventEmitter是一种不好的做法


原始答案:(请勿这样做)

将EventEmitter放入服务中,该服务允许Obse​​rvingComponent直接订阅(和取消订阅)该事件

import {EventEmitter} from 'angular2/core';

export class NavService {
  navchange: EventEmitter<number> = new EventEmitter();
  constructor() {}
  emit(number) {
    this.navchange.emit(number);
  }
  subscribe(component, callback) {
    // set 'this' to component when callback is called
    return this.navchange.subscribe(data => call.callback(component, data));
  }
}

@Component({
  selector: 'obs-comp',
  template: 'obs component, index: {{index}}'
})
export class ObservingComponent {
  item: number;
  subscription: any;
  constructor(private navService:NavService) {
   this.subscription = this.navService.subscribe(this, this.selectedNavItem);
  }
  selectedNavItem(item: number) {
    console.log('item index changed!', item);
    this.item = item;
  }
  ngOnDestroy() {
    this.subscription.unsubscribe();
  }
}

@Component({
  selector: 'my-nav',
  template:`
    <div class="nav-item" (click)="selectedNavItem(1)">item 1 (click me)</div>
  `,
})
export class Navigation {
  constructor(private navService:NavService) {}
  selectedNavItem(item: number) {
    console.log('selected nav item ' + item);
    this.navService.emit(item);
  }
}

如果您尝试使用Plunker,则对于这种方法,我有些不满意的地方:

  • ObservingComponent在销毁时需要取消订阅
  • 我们必须将组件传递给,subscribe()以便this在调用回调时设置适当的值

更新:解决第二个项目符号的另一种方法是让ObservingComponent直接订阅navchangeEventEmitter属性:

constructor(private navService:NavService) {
   this.subscription = this.navService.navchange.subscribe(data =>
     this.selectedNavItem(data));
}

如果我们直接订阅,则不需要subscribe()NavService上的方法。

为了使NavService的封装更加紧密,可以添加一个getNavChangeEmitter()方法并使用该方法:

getNavChangeEmitter() { return this.navchange; }  // in NavService

constructor(private navService:NavService) {  // in ObservingComponent
   this.subscription = this.navService.getNavChangeEmitter().subscribe(data =>
     this.selectedNavItem(data));
}

我更喜欢这种解决方案,而不是Zouabi先生提供的答案,但老实说,我也不喜欢这种解决方案。我不在乎取消订阅,但我讨厌这样的事实,我们必须通过组件才能订阅该事件……
the_critic

我实际上考虑过这一点,因此决定采用此解决方案。我希望有一个稍微干净的解决方案,但我不确定是否有可能(或者我可能无法提出更优雅的方法)。
the_critic 2015年

实际上,第二个项目符号问题是改为传递对该函数的引用。要修复:this.subscription = this.navService.subscribe(() => this.selectedNavItem());和订阅:return this.navchange.subscribe(callback);
安德烈Werlang

1

如果要遵循一种面向响应式编程的风格,那么肯定“一切都是流”的概念应运而生,因此,请使用Observables尽可能多地处理这些流。


1

您可以使用以下任一方法:

  1. 行为主题:

BehaviorSubject是主题的一种,主题是可观察的一种特殊类型,它可以充当可观察对象,并且观察者可以像其他任何可观察对象一样订阅消息,并且在订阅后,它返回源可观察对象发出的主题的最后一个值:

优点:不需要在组件之间传递数据的诸如父子关系之类的关系。

导航服务

import {Injectable}      from '@angular/core'
import {BehaviorSubject} from 'rxjs/BehaviorSubject';

@Injectable()
export class NavService {
  private navSubject$ = new BehaviorSubject<number>(0);

  constructor() {  }

  // Event New Item Clicked
  navItemClicked(navItem: number) {
    this.navSubject$.next(number);
  }

 // Allowing Observer component to subscribe emitted data only
  getNavItemClicked$() {
   return this.navSubject$.asObservable();
  }
}

导航组件

@Component({
  selector: 'navbar-list',
  template:`
    <ul>
      <li><a (click)="navItemClicked(1)">Item-1 Clicked</a></li>
      <li><a (click)="navItemClicked(2)">Item-2 Clicked</a></li>
      <li><a (click)="navItemClicked(3)">Item-3 Clicked</a></li>
      <li><a (click)="navItemClicked(4)">Item-4 Clicked</a></li>
    </ul>
})
export class Navigation {
  constructor(private navService:NavService) {}
  navItemClicked(item: number) {
    this.navService.navItemClicked(item);
  }
}

观察组件

@Component({
  selector: 'obs-comp',
  template: `obs component, item: {{item}}`
})
export class ObservingComponent {
  item: number;
  itemClickedSubcription:any

  constructor(private navService:NavService) {}
  ngOnInit() {

    this.itemClickedSubcription = this.navService
                                      .getNavItemClicked$
                                      .subscribe(
                                        item => this.selectedNavItem(item)
                                       );
  }
  selectedNavItem(item: number) {
    this.item = item;
  }

  ngOnDestroy() {
    this.itemClickedSubcription.unsubscribe();
  }
}

第二种方法是 Event Delegation in upward direction child -> parent

  1. 使用@Input和@Output装饰器将父级的数据传递给子级组件并通知子级父级组件

例如@Ashish Sharma给出的回答。


0

您需要在ObservingComponent的模板中使用Navigation组件(别忘了将选择器添加到Navigation component .. navigation-component for ex)

<navigation-component (navchange)='onNavGhange($event)'></navigation-component>

并在ObservingComponent中实现onNavGhange()

onNavGhange(event) {
  console.log(event);
}

最后一件事..您不需要@Componennt中的events属性

events : ['navchange'], 

这只会为基础组件连接一个事件。那不是我想做的。我本可以在上说(^ navchange)(插入符号用于事件冒泡),nav-item但是我只想发出一个其他人可以观察到的事件。
the_critic

您可以使用navchange.toRx()。subscribe()..,但您需要在ObservingComponent上有关于navchange的参考
Mourad Zouabi 2015年

0

您可以如上所述使用BehaviourSubject,或者还有另一种方法:

您可以像这样处理EventEmitter: 首先添加选择器

import {Component, Output, EventEmitter} from 'angular2/core';

@Component({
// other properties left out for brevity
selector: 'app-nav-component', //declaring selector
template:`
  <div class="nav-item" (click)="selectedNavItem(1)"></div>
`
 })

 export class Navigation {

@Output() navchange: EventEmitter<number> = new EventEmitter();

selectedNavItem(item: number) {
    console.log('selected nav item ' + item);
    this.navchange.emit(item)
}

}

现在您可以处理此事件,就像 让我们假设Observer.component.html是Observer组件的视图一样

<app-nav-component (navchange)="recieveIdFromNav($event)"></app-nav-component>

然后在ObservingComponent.ts中

export class ObservingComponent {

 //method to recieve the value from nav component

 public recieveIdFromNav(id: number) {
   console.log('here is the id sent from nav component ', id);
 }

 }

-2

我找到了针对这种情况的另一种解决方案,而没有使用Reactivex两种服务。我实际上很喜欢rxjx API,但是我认为它在解决异步和/或复杂功能时效果最好。以这种方式使用它,它对我来说已经超出了。

我想您正在寻找的是广播。只是。我发现了这个解决方案:

<app>
  <app-nav (selectedTab)="onSelectedTab($event)"></app-nav>
       // This component bellow wants to know when a tab is selected
       // broadcast here is a property of app component
  <app-interested [broadcast]="broadcast"></app-interested>
</app>

 @Component class App {
   broadcast: EventEmitter<tab>;

   constructor() {
     this.broadcast = new EventEmitter<tab>();
   }

   onSelectedTab(tab) {
     this.broadcast.emit(tab)
   }    
 }

 @Component class AppInterestedComponent implements OnInit {
   broadcast: EventEmitter<Tab>();

   doSomethingWhenTab(tab){ 
      ...
    }     

   ngOnInit() {
     this.broadcast.subscribe((tab) => this.doSomethingWhenTab(tab))
   }
 }

这是一个完整的工作示例: https //plnkr.co/edit/xGVuFBOpk2GP0pRBImsE


1
查看最佳答案,它也使用订阅方法。实际上,如今,我建议使用Redux或其他状态控制来解决组件之间的通信问题。尽管它增加了额外的复杂性,但它比任何其他解决方案都好得多。使用Angular 2组件事件处理程序sintax或显式使用subscription方法的概念都相同。我的最终想法是,如果要使用Redux来解决此问题的最终解决方案,否则将服务与事件发射器一起使用。
Nicholas Marcaccini Augusto

订阅是有效的,只要angular不能消除它是可观察的事实。.subscribe()用于最佳答案,而不用于该特定对象。
Porschiey
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.