Phái đoàn: Sự kiện hoặc Người quan sát trong Góc


243

Tôi đang cố gắng thực hiện một cái gì đó giống như một mô hình đoàn trong Angular. Khi người dùng nhấp vào a nav-item, tôi muốn gọi một chức năng phát ra một sự kiện mà lần lượt sẽ được xử lý bởi một số thành phần khác lắng nghe sự kiện.

Đây là kịch bản: Tôi có một Navigationthành phần:

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

}

Đây là thành phần quan sát:

export class ObservingComponent {

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

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

}

Câu hỏi chính là, làm thế nào để tôi làm cho thành phần quan sát quan sát sự kiện trong câu hỏi?


Câu trả lời:


449

Cập nhật 2016-06-27: thay vì sử dụng Đài quan sát, hãy sử dụng một trong hai

  • một BehaviorSubject, theo khuyến nghị của @Abdulrahman trong một bình luận, hoặc
  • một ReplaySubject, như được đề xuất bởi @Jason Goemaat trong một bình luận

Một Chủ đề vừa là một Quan sát (vì vậy chúng ta có thể subscribe()đối với nó) và một Người quan sát (vì vậy chúng ta có thể gọi next()nó để phát ra một giá trị mới). Chúng tôi khai thác tính năng này. Một Chủ đề cho phép các giá trị được phát đa hướng cho nhiều Người quan sát. Chúng tôi không khai thác tính năng này (chúng tôi chỉ có một Người quan sát).

BehaviorSubject là một biến thể của Chủ đề. Nó có khái niệm "giá trị hiện tại". Chúng tôi khai thác điều này: bất cứ khi nào chúng tôi tạo ra một ObservingComponent, nó sẽ tự động nhận được giá trị mục điều hướng hiện tại từ BehaviorSubject.

Mã dưới đây và plunker sử dụng BehaviorSubject.

ReplaySubject là một biến thể khác của Chủ đề. Nếu bạn muốn đợi cho đến khi một giá trị thực sự được sản xuất, hãy sử dụng ReplaySubject(1). Trong khi một BehaviorSubject yêu cầu một giá trị ban đầu (sẽ được cung cấp ngay lập tức), ReplaySubject thì không. ReplaySubject sẽ luôn cung cấp giá trị gần đây nhất, nhưng vì nó không có giá trị ban đầu bắt buộc, dịch vụ có thể thực hiện một số thao tác không đồng bộ trước khi trả về giá trị đầu tiên. Nó vẫn sẽ kích hoạt ngay lập tức các cuộc gọi tiếp theo với giá trị gần đây nhất. Nếu bạn chỉ muốn một giá trị, sử dụng first()trên thuê bao. Bạn không phải hủy đăng ký nếu bạn sử dụng 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


Câu trả lời gốc sử dụng một Observable: (nó đòi hỏi nhiều mã và logic hơn so với sử dụng BehaviorSubject, vì vậy tôi không khuyên bạn nên dùng nó, nhưng nó có thể mang tính hướng dẫn)

Vì vậy, đây là một triển khai sử dụng một Observable thay vì EventEuctor . Không giống như triển khai EventEuctor của tôi, việc triển khai này cũng lưu trữ navItemdịch vụ hiện được chọn trong dịch vụ, để khi một thành phần quan sát được tạo, nó có thể truy xuất giá trị hiện tại thông qua lệnh gọi API navItem()và sau đó được thông báo về các thay đổi thông qua navChange$Quan sát.

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


Xem thêm ví dụ về Sách tương tác thành phần , trong đó sử dụng Subjectthêm vào các vật quan sát. Mặc dù ví dụ là "giao tiếp giữa cha mẹ và con cái", kỹ thuật tương tự được áp dụng cho các thành phần không liên quan.


3
Nó đã được đề cập nhiều lần trong các bình luận về các vấn đề Angular2 mà nó không khuyến khích sử dụng ở EventEmitterbất cứ đâu ngoại trừ đầu ra. Họ hiện đang viết lại các hướng dẫn (giao tiếp máy chủ AFAIR) để không khuyến khích thực hành này.
Günter Zöchbauer 23/2/2016

2
Có cách nào để khởi tạo dịch vụ và khởi động một sự kiện đầu tiên từ bên trong thành phần Điều hướng trong mã mẫu ở trên không? Vấn đề là _observerđối tượng dịch vụ ít nhất không được khởi tạo tại thời điểm ngOnInit()thành phần Điều hướng được gọi.
ComFalet

4
Tôi có thể đề nghị sử dụng BehaviorSubject thay vì Observable. Nó gần hơn EventEmitterbởi vì "nóng" nghĩa là nó đã được "chia sẻ", nó được thiết kế để lưu giá trị hiện tại và cuối cùng nó thực hiện cả Quan sát và Quan sát sẽ giúp bạn tiết kiệm ít nhất năm dòng mã và hai thuộc tính
Abdulrahman Alsoghayer

2
@PankajParkar, liên quan đến "làm thế nào để công cụ phát hiện thay đổi biết rằng sự thay đổi đã xảy ra trên đối tượng quan sát được" - Tôi đã xóa phản hồi nhận xét trước đó của mình. Gần đây tôi đã biết rằng Angular không vá khỉ subscribe(), vì vậy nó không thể phát hiện khi thay đổi có thể quan sát được. Thông thường, có một số sự kiện không đồng bộ kích hoạt (trong mã mẫu của tôi, đó là các sự kiện nhấp vào nút) và cuộc gọi lại liên quan sẽ gọi next () trên một quan sát. Nhưng phát hiện thay đổi chạy vì sự kiện không đồng bộ, không phải vì sự thay đổi có thể quan sát được. Xem thêm bình luận của Günter: stackoverflow.com/a/36846501/215945
Mark Rajcok

9
Nếu bạn muốn đợi cho đến khi một giá trị thực sự được tạo ra, bạn có thể sử dụng ReplaySubject(1). A BehaviorSubjectyêu cầu một giá trị ban đầu sẽ được cung cấp ngay lập tức. Di ReplaySubject(1)chúc sẽ luôn cung cấp giá trị gần đây nhất, nhưng không có giá trị ban đầu cần thiết để dịch vụ có thể thực hiện một số thao tác không đồng bộ trước khi trả lại giá trị đầu tiên, nhưng vẫn kích hoạt ngay các cuộc gọi tiếp theo với giá trị cuối cùng. Nếu bạn chỉ quan tâm đến một giá trị, bạn có thể sử dụng first()trên đăng ký và không phải hủy đăng ký ở cuối vì điều đó sẽ hoàn tất.
Jason Goemaat

33

Tin tức mới: Tôi đã thêm một câu trả lời khác sử dụng Đài quan sát thay vì Trình phát sự kiện. Tôi đề nghị câu trả lời trên cái này. Và thực tế, sử dụng EventEuctor trong một dịch vụ là một thực tế tồi .


Câu trả lời gốc: (không làm điều này)

Đặt EventEuctor vào một dịch vụ, cho phép ObservingComponent đăng ký trực tiếp (và hủy đăng ký) cho sự kiện :

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

Nếu bạn thử Plunker, có một vài điều tôi không thích về phương pháp này:

  • ObservingComponent cần hủy đăng ký khi nó bị phá hủy
  • chúng ta phải truyền thành phần để subscribe()đặt đúng thiskhi gọi lại

Cập nhật: Một giải pháp thay thế để giải quyết viên đạn thứ 2 là có ObservingComponent đăng ký trực tiếp vào thuộc tính navchangeEventEuctor:

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

Nếu chúng tôi đăng ký trực tiếp, thì chúng tôi sẽ không cần subscribe()phương thức trên NavService.

Để làm cho NavService được gói gọn hơn một chút, bạn có thể thêm một getNavChangeEmitter()phương thức và sử dụng phương thức đó:

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

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

Tôi thích giải pháp này cho câu trả lời do ông Zouabi cung cấp, nhưng tôi cũng không phải là người hâm mộ giải pháp này. Tôi không quan tâm đến việc hủy đăng ký phá hủy, nhưng tôi ghét thực tế là chúng ta phải vượt qua thành phần này để đăng ký tham gia sự kiện ...
the_critic

Tôi thực sự nghĩ về điều này và quyết định đi với giải pháp này. Tôi muốn có một giải pháp sạch hơn một chút, nhưng tôi không chắc là có thể (hoặc có lẽ tôi không thể nghĩ ra thứ gì đó thanh lịch hơn tôi nên nói).
the_critic

thực ra vấn đề viên đạn thứ 2 là một tham chiếu đến Hàm đang được thông qua thay thế. để sửa chữa: this.subscription = this.navService.subscribe(() => this.selectedNavItem());và đăng ký:return this.navchange.subscribe(callback);
André Werlang

1

Nếu một người muốn theo một phong cách lập trình theo hướng Reactive hơn, thì chắc chắn khái niệm "Mọi thứ là một luồng" xuất hiện và do đó, hãy sử dụng Observables để xử lý các luồng này thường xuyên nhất có thể.


1

Bạn có thể sử dụng một trong hai:

  1. Chủ đề hành vi:

BehaviorSubject là một loại chủ đề, một chủ đề là một loại đặc biệt có thể quan sát được, có thể đóng vai trò là người quan sát và bạn có thể đăng ký các tin nhắn như bất kỳ thứ gì có thể quan sát được khác và khi đăng ký, nó trả về giá trị cuối cùng của chủ đề được phát ra bởi nguồn có thể quan sát được:

Ưu điểm: Không có mối quan hệ như mối quan hệ cha-con cần thiết để truyền dữ liệu giữa các thành phần.

DỊCH VỤ 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();
  }
}

THÀNH PHẦN DI CHUYỂN

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

THÀNH PHẦN QUAN SÁT

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

Cách tiếp cận thứ hai là Event Delegation in upward direction child -> parent

  1. Sử dụng @Input và @Output trang trí cha mẹ truyền dữ liệu đến thành phần con và thành phần cha thông báo con

ví dụ: Được trả lời bởi @Ashish Sharma.


0

Bạn cần sử dụng thành phần Điều hướng trong mẫu của ObservingComponent (đừng quên thêm bộ chọn vào thành phần Điều hướng .. thành phần điều hướng cho ex)

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

Và triển khai onNavGhange () trong ObservingComponent

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

Điều cuối cùng .. bạn không cần thuộc tính sự kiện trong @Componennt

events : ['navchange'], 

Điều này chỉ nối một sự kiện cho các thành phần cơ bản. Đó không phải là những gì tôi đang cố gắng làm. Tôi có thể vừa nói một cái gì đó như (^ navchange) (dấu mũ dành cho sự kiện sủi bọt) trên nav-itemnhưng tôi chỉ muốn phát ra một sự kiện mà người khác có thể quan sát.
the_critic

bạn có thể sử dụng navchange.toRx (). subscribe () .. nhưng bạn sẽ cần phải có một tài liệu tham khảo về điều hướng trên ObservingComponent
Mourad Zouabi

0

bạn có thể sử dụng BehaviourSubject như được mô tả ở trên hoặc có một cách khác:

bạn có thể xử lý EventEuctor như thế này: trước tiên hãy thêm bộ chọn

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

}

Bây giờ bạn có thể xử lý sự kiện này như chúng ta giả sử observer.component.html là chế độ xem của thành phần Observer

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

sau đó trong 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

Tôi đã tìm ra một giải pháp khác cho trường hợp này mà không cần sử dụng Reactivex. Tôi thực sự yêu thích API rxjx tuy nhiên tôi nghĩ rằng nó hoạt động tốt nhất khi giải quyết một hàm không đồng bộ và / hoặc hàm phức tạp. Sử dụng nó theo cách đó, nó khá vượt xa tôi.

Những gì tôi nghĩ rằng bạn đang tìm kiếm là cho một phát sóng. Chỉ vậy thôi. Và tôi đã tìm ra giải pháp này:

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

Đây là một ví dụ hoạt động đầy đủ: https://plnkr.co/edit/xGVuFBOpk2GP0pRBImsE


1
Nhìn vào câu trả lời tốt nhất, nó cũng sử dụng phương thức đăng ký .. Thực tế ngày nay tôi sẽ khuyên bạn nên sử dụng Redux hoặc một số điều khiển trạng thái khác để giải quyết vấn đề giao tiếp này giữa các thành phần. Nó là vô cùng tốt hơn nhiều so với bất kỳ giải pháp nào khác mặc dù nó thêm phức tạp. Hoặc sử dụng trình xử lý sự kiện Angular 2 sintax hoặc sử dụng rõ ràng phương thức đăng ký, khái niệm này vẫn giữ nguyên. Suy nghĩ cuối cùng của tôi là nếu bạn muốn một giải pháp dứt khoát cho vấn đề đó, hãy sử dụng Redux, nếu không thì sử dụng các dịch vụ với trình phát sự kiện.
Nicholas Marcaccini Augusto

đăng ký là hợp lệ miễn là góc cạnh không loại bỏ thực tế là nó có thể quan sát được. .subscribe () được sử dụng trong câu trả lời tốt nhất, nhưng không phải trên đối tượng cụ thể đó.
Porschiey
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.