首页 文章

委托:Angular中的EventEmitter或Observable

提问于
浏览
190

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

}

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

6 回答

  • 386

    Update 2016-06-27: 而不是使用Observables,使用其中之一

    • 一个BehaviorSubject,由@Abdulrahman在评论中推荐,或者

    • 一个ReplaySubject,由@Jason Goemaat在评论中推荐

    Subject既是一个Observable(所以我们可以 subscribe() )和一个Observer(所以我们可以在它上面调用 next() 来发出一个新值) . 我们利用此功能 . Subject允许将值多播到许多观察者 . 我们没有利用这个功能(我们只有一个Observer) .

    BehaviorSubject是Subject的变体 . 它有"the current value"的概念 . 我们利用这个:每当我们创建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


    Original answer that uses an Observable: (它需要比使用BehaviorSubject更多的代码和逻辑,所以我不推荐它,但它可能是有益的)

    所以,这是一个使用Observable instead of an 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 example,除了observable之外还使用 Subject . 虽然该示例是"parent and children communication,",但相同的技术适用于不相关的组件 .

  • 31

    Breaking news: 我添加了使用Observable而不是EventEmitter的another answer . 我建议回答这个问题 . 实际上,在服务中使用EventEmitter是bad practice .


    原答案:(不要这样做)

    将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));
    }
    
  • 0

    如果想要遵循更加反应式的编程风格,那么“Everything is a stream”的概念肯定会出现,因此,使用Observables尽可能频繁地处理这些流 .

  • -2

    你可以使用如上所述的BehaviourSubject或 there is one more way:

    你可以像这样处理EventEmitter: first add a selector

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

    Now you can handle this event like let us suppose observer.component.html is the view of Observer component

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

    then in the 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

    您需要在ObservingComponent的模板中使用Navigation组件(不要忘记向导航组件添加 selector ..导航组件为ex)

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

    并在ObservingComponent中实现onNavGhange()

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

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

    events : ['navchange'],
    
  • 1

    我在没有使用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

相关问题