การมอบหมาย: EventEmitter หรือสังเกตได้ในเชิงมุม


244

ฉันพยายามที่จะใช้งานบางอย่างเช่นรูปแบบการมอบหมายใน Angular เมื่อผู้ใช้คลิกที่ a 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!');
  }

}

คำถามสำคัญคือฉันจะทำให้องค์ประกอบการสังเกตสังเกตเหตุการณ์ที่เกิดขึ้นได้อย่างไร


คำตอบ:


450

อัพเดท 2016-06-27:แทนที่จะใช้ Observables ให้ใช้อย่างใดอย่างหนึ่ง

  • a BehaviorSubject ตามคำแนะนำโดย @Abdulrahman ในความคิดเห็นหรือ
  • ReplaySubject ตามที่แนะนำโดย @Jason Goemaat ในความคิดเห็น

เรื่องเป็นทั้งสังเกต (เพื่อให้เราสามารถsubscribe()อยู่กับมัน) และสังเกตการณ์ (เพื่อให้เราสามารถเรียกnext()มันจะออกมาเป็นค่าใหม่) เราใช้ประโยชน์จากคุณลักษณะนี้ หัวเรื่องให้ค่าเป็นมัลติคาสต์แก่ผู้สังเกตการณ์หลายคน เราไม่ใช้ประโยชน์จากคุณลักษณะนี้ (เรามีผู้สังเกตการณ์เพียงคนเดียวเท่านั้น)

BehaviorSubjectเป็นตัวแปรของหัวเรื่อง มันมีความคิดของ "มูลค่าปัจจุบัน" เราใช้ประโยชน์จากสิ่งนี้: ทุกครั้งที่เราสร้าง ObservingComponent มันจะได้รับค่ารายการการนำทางปัจจุบันจาก BehaviorSubject โดยอัตโนมัติ

รหัสด้านล่างและตัวเรียงลำดับใช้ BehaviorSubject

ReplaySubjectเป็นอีกหนึ่งตัวแปรของหัวเรื่อง หากคุณต้องการรอจนกว่าจะมีการสร้างค่าใช้งานReplaySubject(1)จริง ในขณะที่ BehaviorSubject ต้องการค่าเริ่มต้น (ซึ่งจะมีให้ในทันที) ReplaySubject ไม่ ReplaySubject จะให้ค่าล่าสุดเสมอ แต่เนื่องจากไม่มีค่าเริ่มต้นที่ต้องการบริการจึงสามารถดำเนินการ async บางอย่างก่อนส่งคืนค่าแรก มันจะยังคงทำงานทันทีเมื่อมีการโทรตามมาด้วยค่าล่าสุด หากคุณต้องการเพียงหนึ่งค่าใช้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 ดังนั้นฉันไม่แนะนำ แต่อาจเป็นคำแนะนำ)

ดังนั้นนี่คือการดำเนินการที่ใช้สังเกตแทน 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ünterZöchbauer

2
มีวิธีการเริ่มต้นบริการและปลดเหตุการณ์แรกจากภายในองค์ประกอบการนำทางในรหัสตัวอย่างด้านบนหรือไม่ ปัญหาคือว่า_observerวัตถุบริการอย่างน้อยไม่ได้เริ่มต้นในช่วงเวลาของngOnInit()องค์ประกอบนำทางที่ถูกเรียก
ComFreek

4
ฉันขอแนะนำให้ใช้BehaviorSubjectแทน Observable ได้ไหม มันใกล้กว่าEventEmitterเพราะมัน "ร้อน" หมายความว่ามันได้ "แชร์" อยู่แล้วมันถูกออกแบบมาเพื่อบันทึกค่าปัจจุบันและในที่สุดก็ใช้ทั้ง Observable และ Observer ซึ่งจะช่วยให้คุณประหยัดรหัสอย่างน้อยห้าบรรทัดและคุณสมบัติสองอย่าง
Abdulrahman Alsoghayer

2
@PanajParkar เกี่ยวกับ "เครื่องมือตรวจจับการเปลี่ยนแปลงรู้ได้อย่างไรว่าการเปลี่ยนแปลงเกิดขึ้นบนวัตถุที่สังเกตได้" - ฉันลบการตอบกลับความคิดเห็นก่อนหน้าของฉัน ฉันได้เรียนรู้เมื่อเร็ว ๆ นี้ว่า Angular ไม่ได้เป็นปะแก้ลิงsubscribe()ดังนั้นจึงไม่สามารถตรวจพบได้เมื่อมีการเปลี่ยนแปลงที่สังเกตได้ โดยทั่วไปมีเหตุการณ์ async บางอย่างที่เริ่มทำงาน (ในโค้ดตัวอย่างของฉันมันคือเหตุการณ์คลิกปุ่ม) และการติดต่อกลับที่เกี่ยวข้องจะเรียกถัดไป () บนสิ่งที่สังเกตได้ แต่การตรวจจับการเปลี่ยนแปลงทำงานเนื่องจากเหตุการณ์ async ไม่ใช่เพราะการเปลี่ยนแปลงที่สังเกตได้ ดูความเห็นของGünter: stackoverflow.com/a/36846501/215945
Mark Rajcok

9
หากคุณต้องการรอจนกว่าจะมีการสร้างมูลค่าจริง ๆ คุณสามารถReplaySubject(1)ใช้ได้ BehaviorSubjectต้องใช้ค่าเริ่มต้นที่จะได้รับทันที ReplaySubject(1)มักจะให้ค่าล่าสุด แต่ไม่ได้มีค่าเริ่มต้นที่จำเป็นเพื่อให้บริการสามารถดำเนินการ async บางส่วนก่อนที่จะกลับค่าแรกของมัน แต่ยังคงยิงทันทีในการโทรตามมาด้วยค่าที่ผ่านมา หากคุณสนใจเพียงค่าเดียวคุณสามารถใช้first()ในการสมัครสมาชิกและไม่ต้องยกเลิกการสมัครเพราะท้ายที่สุดจะเสร็จสมบูรณ์
Jason Goemaat

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นี้มีบางสิ่งที่ฉันไม่ชอบเกี่ยวกับวิธีการนี้:

  • การสังเกตส่วนประกอบต้องยกเลิกการเป็นสมาชิกเมื่อถูกทำลาย
  • เราต้องผ่านส่วนประกอบเพื่อsubscribe()ให้การthisตั้งค่าที่เหมาะสมเมื่อเรียกกลับ

อัปเดต: อีกทางเลือกหนึ่งที่แก้ปัญหา bullet ที่สองคือให้ ObservingComponent สมัครสมาชิกโดยตรงกับnavchangeคุณสมบัติ EventEmitter:

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

ฉันชอบโซลูชันนี้กับคำตอบของ Mr Zouabi แต่ฉันก็ไม่ได้เป็นแฟนของโซลูชันนี้เหมือนกัน ฉันไม่สนใจเกี่ยวกับการยกเลิกการสมัครสมาชิกในการทำลาย แต่ฉันเกลียดความจริงที่ว่าเราต้องผ่านส่วนประกอบเพื่อสมัครรับข้อมูลเหตุการณ์ ...
the_critic

จริง ๆ แล้วฉันคิดเกี่ยวกับเรื่องนี้และตัดสินใจที่จะไปด้วยวิธีนี้ ฉันชอบที่จะมีวิธีแก้ปัญหาที่สะอาดกว่านี้เล็กน้อย แต่ฉันไม่แน่ใจว่าเป็นไปได้
the_critic

จริงๆแล้วปัญหากระสุนที่สองคือการอ้างอิงถึงฟังก์ชั่นจะถูกส่งผ่านแทน เพื่อแก้ไข: this.subscription = this.navService.subscribe(() => this.selectedNavItem());และในการสมัคร:return this.navchange.subscribe(callback);
André Werlang

1

หากใครต้องการติดตามรูปแบบการเขียนโปรแกรมเชิงปฏิกิริยามากขึ้นแล้วแนวคิดของ "ทุกอย่างคือกระแส" แน่นอนเข้ามาในภาพดังนั้นให้ใช้ Observables เพื่อจัดการกับกระแสข้อมูลเหล่านี้ให้บ่อยที่สุด


1

คุณสามารถใช้:

  1. เรื่องพฤติกรรม:

BehaviorSubject เป็นประเภทของหัวเรื่องหัวเรื่องเป็นแบบพิเศษที่สามารถสังเกตได้และสังเกตการณ์คุณสามารถสมัครสมาชิกข้อความเช่นอื่น ๆ ที่สังเกตได้และเมื่อสมัครสมาชิกจะคืนค่าสุดท้ายของเรื่องที่ปล่อยออกมาจากแหล่งที่สังเกตได้:

ข้อได้เปรียบ: ไม่มีความสัมพันธ์เช่นความสัมพันธ์พ่อแม่และลูกที่ต้องการส่งผ่านข้อมูลระหว่างส่วนประกอบ

บริการ NAV

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 decorators พาเรนต์ผ่านข้อมูลไปยังคอมโพเนนต์ชายน์และชายด์แจ้งให้ทราบถึงองค์ประกอบหลัก

เช่นได้รับคำตอบจาก @Ashish Sharma


0

คุณต้องใช้องค์ประกอบการนำทางในเทมเพลตของ ObservingComponent (อย่าลืมเพิ่มตัวเลือกไปยังองค์ประกอบการนำทาง .. การนำทางด้วยองค์ประกอบสำหรับอดีต)

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

และใช้งาน onNavGhange () ใน ObservingComponent

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

สิ่งสุดท้าย .. คุณไม่จำเป็นต้องมีแอททริบิวเหตุการณ์ใน@Componennt

events : ['navchange'], 

นี่ขอเหตุการณ์สำหรับองค์ประกอบพื้นฐานเท่านั้น นั่นไม่ใช่สิ่งที่ฉันพยายามจะทำ ฉันอาจจะพูดอะไรบางอย่างเช่น (^ navchange) (เครื่องหมายรูปหมวกสำหรับการเดือดปุด ๆ ของเหตุการณ์) บนnav-itemแต่ฉันแค่ต้องการปล่อยเหตุการณ์ที่คนอื่นสามารถสังเกตเห็น
the_critic

คุณสามารถใช้ navchange.toRx () สมัครสมาชิก () .. แต่คุณจะต้องมีการอ้างอิงในการนำทางบน ObservingComponent
Mourad Zouabi

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 จริงๆ แต่ฉันคิดว่ามันจะดีที่สุดเมื่อแก้ไข async และ / หรือฟังก์ชั่นที่ซับซ้อน การใช้มันในแบบนั้นมันสวยเกินกว่าฉัน

สิ่งที่ฉันคิดว่าคุณกำลังมองหาคือการออกอากาศ แค่นั้น. และฉันพบวิธีแก้ปัญหานี้:

<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 component event handler sintax หรือใช้วิธีการสมัครอย่างชัดเจนแนวคิดก็ยังคงเหมือนเดิม ความคิดสุดท้ายของฉันคือถ้าคุณต้องการโซลูชันที่ชัดเจนสำหรับปัญหานั้นให้ใช้ Redux หรือใช้บริการกับตัวส่งเหตุการณ์
Nicholas Marcaccini Augusto

สมัครสมาชิกถูกต้องตราบเท่าที่เชิงมุมไม่ได้ลบความจริงที่ว่ามันเป็นที่สังเกตได้ .subscribe () ใช้ในคำตอบที่ดีที่สุด แต่ไม่ใช่ในวัตถุนั้น
Porschiey
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.