委托:在Angular中使用EventEmitter还是Observable?

282

我正在尝试在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!');
  }

}

关键问题是,我如何使观察组件观察到所讨论的事件?


根据文档:
EventEmitter类扩展自Observable。
- halllo
7个回答

491

更新于2016-06-27: 不要使用Observables,而是使用以下两种方法:

  • 按照@Abdulrahman的评论建议使用BehaviorSubject;
  • 按照@Jason Goemaat的评论建议使用ReplaySubject。

Subject既是Observable(因此我们可以对其进行subscribe()),也是Observer(因此我们可以调用next()来发出新值)。 我们利用了这个特性。 Subject允许将值广播到多个观察者。我们没有利用这个特性(我们只有一个观察者)。

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

下面的代码和plunker使用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


此外,还可以参考组件交互菜谱示例,该示例除了使用可观察对象外,还使用了一个Subject。虽然示例是关于“父子通信”,但同样的技术也适用于不相关的组件。


3
在Angular2问题的评论中,反复提到除了用于输出之外,不建议在任何地方使用EventEmitter。他们目前正在重写教程(与服务器通信AFAIR),以不鼓励这种做法。 - Günter Zöchbauer
2
在上面的示例代码中,有没有一种方法可以在导航组件内初始化服务并触发第一个事件?问题在于,在调用导航组件的ngOnInit()时,服务对象的_observer至少尚未初始化。 - ComFreek
5
我建议使用[BehaviorSubject](http://reactivex.io/rxjs/manual/overview.html#behaviorsubject)代替Observable。它更接近于`EventEmitter`,因为它是“热”类型即已经“共享”,被设计用于保存当前值,最终实现了Observable和Observer,这将至少为您节省五行代码和两个属性。 - Abdulrahman Alsoghayer
2
@PankajParkar,关于“变更检测引擎如何知道可观察对象发生了变化”的问题 - 我删除了之前的评论回复。我最近了解到Angular不会猴子补丁subscribe(),因此无法检测到可观察对象的更改。通常,会有一些异步事件触发(在我的示例代码中,是按钮单击事件),相关的回调将在可观察对象上调用next()。但是变更检测是由于异步事件而运行的,而不是由于可观察对象的更改。请参见Günter的评论:http://stackoverflow.com/a/36846501/215945 - Mark Rajcok
10
如果您想要等待一个值被实际产生,您可以使用 ReplaySubject(1)BehaviorSubject 需要提供一个初始值,这个值将会被立即提供。ReplaySubject(1) 始终提供最新的值,但不需要初始值,因此服务可以在返回第一个值之前执行一些异步操作,但仍然能够在后续调用中立即提供上一次的值。如果您只对一个值感兴趣,您可以在订阅上使用 first(),而不必在结束时取消订阅,因为那样会使其完成。 - Jason Goemaat
显示剩余24条评论

33

最新消息:我已添加另一个答案,它使用Observable而不是EventEmitter。我建议您选择那个答案。实际上,在服务中使用EventEmitter是不良实践


原始答案:(不要这样做)

将EventEmitter放入服务中,这允许ObservingComponent直接订阅(和取消订阅)事件

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 直接订阅 navchange EventEmitter 属性:

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

如果我们直接订阅,那么就不需要在NavService上使用subscribe()方法。
为了使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
实际上第二个问题是传递了对函数的引用。解决方法是:this.subscription = this.navService.subscribe(() => this.selectedNavItem()); 并在订阅时使用:return this.navchange.subscribe(callback); - André Werlang

3

您可以使用以下两种方式:

  1. Behavior Subject:

BehaviorSubject是一种特殊类型的observable,它可以作为observable和observer。您可以像订阅其他observable一样订阅消息,并在订阅时返回源observable发出的subject的最后一个值:

优点:无需父子关系即可在组件之间传递数据。

导航服务

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();
  }
}

第二种方法是使用向上的事件委托,即子组件->父组件

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

例如 @Ashish Sharma 给出的答案。


1
你可以按照上述方式使用BehaviorSubject,或者还有一种方法: 你可以像这样处理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);
 }

 }

1
如果想要遵循更具反应性的编程风格,那么“一切皆为流”的概念肯定会出现,因此尽可能使用Observables来处理这些流。

0

你需要在ObservingComponent的模板中使用Navigation组件(不要忘记为Navigation组件添加选择器,例如navigation-component)

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

在 ObservingComponent 中实现 onNavGhange()

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

最后一件事……你不需要在@Component中使用events属性。
events : ['navchange'], 

这只是为底层组件挂接一个事件。这不是我想要做的。我本可以像 (^navchange) 这样说一些关于 nav-item 的内容(插入符号用于事件冒泡),但我只想发出其他人可以观察到的事件。 - the_critic
你可以使用 navchange.toRx().subscribe(),但是你需要在 ObservingComponent 上有一个 navchange 的引用。 - Mourad Zouabi

-2
我找到了另一个解决方案,可以解决这个问题,而不需要使用Reactivex和service。我确实喜欢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
看一下最佳答案,它也使用了subscribe方法。实际上,我现在会建议使用Redux或其他状态控制来解决组件之间的通信问题。尽管它增加了额外的复杂性,但它比任何其他解决方案都要好得多。无论是使用Angular 2组件事件处理程序语法还是显式地使用subscribe方法,概念都是相同的。我的最终想法是,如果您想要该问题的明确解决方案,请使用Redux;否则,请使用带有事件发射器的服务。 - Nicholas Marcaccini Augusto
只要 Angular 不删除它的可观察对象属性,subscribe 就是有效的。.subscribe() 在最佳答案中被使用,但不是在那个特定的对象上。 - Porschiey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接