Angular中的全球事件


224

有没有相当于$scope.emit()$scope.broadcast()在角?

我知道该EventEmitter功能,但是据我了解,这只会向父HTML元素发出一个事件。

如果我需要在FX之间进行通信该怎么办。兄弟姐妹还是DOM根目录中的组件与嵌套在多个级别中的元素之间?


2
我有一个类似的问题,与创建一个可以从dom的任何位置访问的对话框组件有关: stackoverflow.com/questions/34572539/… 基本上,一种解决方案是将事件发射器放入服务中
brando

1
这是我使用RXJS实现的这种服务的实现,该服务允许在订阅时获取第n个最后一个值。stackoverflow.com/questions/46027693/…–
Codewarrior

Answers:


385

没有等同于$scope.emit()$scope.broadcast()来自AngularJS。组件内部的EventEmitter接近,但是正如您提到的,它只会向直接父组件发送一个事件。

在Angular中,我将在下面尝试解释其他替代方法。

@Input()绑定允许将应用程序模型连接到有向对象图(从根到叶)。组件的更改检测器策略的默认行为是将所有更改传播到来自任何已连接组件的所有绑定的应用程序模型。

除了:有两种类型的模型:视图模型和应用程序模型。应用程序模型通过@Input()绑定进行连接。视图模型只是组件属性(未使用@Input()装饰),该属性绑定在组件模板中。

要回答您的问题:

如果需要在兄弟组件之间进行通信怎么办?

  1. 共享应用程序模型:兄弟姐妹可以通过共享应用程序模型进行通信(就像angular 1一样)。例如,当一个同级对模型进行更改时,具有相同模型绑定的另一同级会自动更新。

  2. 组件事件:子组件可以使用@Output()绑定将事件发送给父组件。父组件可以处理事件,并操纵应用程序模型或它自己的视图模型。对应用程序模型的更改将自动传播到直接或间接绑定到同一模型的所有组件。

  3. 服务事件:组件可以订阅服务事件。例如,两个同级组件可以订阅相同的服务事件,并通过修改它们各自的模型进行响应。在下面的更多内容。

如何在根组件和嵌套多个级别的组件之间进行通信?

  1. 共享的应用程序模型:可以通过@Input()绑定将应用程序模型从Root组件传递到深层嵌套的子组件。任何组件对模型的更改都会自动传播到共享同一模型的所有组件。
  2. 服务事件:您还可以将EventEmitter移至共享服务,该服务允许任何组件注入服务并预订事件。这样,Root组件可以调用服务方法(通常会使模型发生变化),该服务方法继而发出事件。向下几层,也注入了服务并订阅了同一事件的孙组件可以处理它。任何更改共享应用程序模型的事件处理程序,都会自动传播到依赖它的所有组件。这可能与$scope.broadcast()Angular 1 最接近。

使用服务事件传播更改的可观察服务的示例

这是使用服务事件传播更改的可观察服务的示例。添加TodoItem时,该服务将发出一个事件,通知其组件订阅者。

export class TodoItem {
    constructor(public name: string, public done: boolean) {
    }
}
export class TodoService {
    public itemAdded$: EventEmitter<TodoItem>;
    private todoList: TodoItem[] = [];

    constructor() {
        this.itemAdded$ = new EventEmitter();
    }

    public list(): TodoItem[] {
        return this.todoList;
    }

    public add(item: TodoItem): void {
        this.todoList.push(item);
        this.itemAdded$.emit(item);
    }
}

以下是根组件如何订阅事件:

export class RootComponent {
    private addedItem: TodoItem;
    constructor(todoService: TodoService) {
        todoService.itemAdded$.subscribe(item => this.onItemAdded(item));
    }

    private onItemAdded(item: TodoItem): void {
        // do something with added item
        this.addedItem = item;
    }
}

嵌套在多个级别中的子组件将以相同的方式订阅事件:

export class GrandChildComponent {
    private addedItem: TodoItem;
    constructor(todoService: TodoService) {
        todoService.itemAdded$.subscribe(item => this.onItemAdded(item));
    }

    private onItemAdded(item: TodoItem): void {
        // do something with added item
        this.addedItem = item;
    }
}

这是调用服务以触发事件的组件(它可以驻留在组件树中的任何位置):

@Component({
    selector: 'todo-list',
    template: `
         <ul>
            <li *ngFor="#item of model"> {{ item.name }}
            </li>
         </ul>
        <br />
        Add Item <input type="text" #txt /> <button (click)="add(txt.value); txt.value='';">Add</button>
    `
})
export class TriggeringComponent{
    private model: TodoItem[];

    constructor(private todoService: TodoService) {
        this.model = todoService.list();
    }

    add(value: string) {
        this.todoService.add(new TodoItem(value, false));
    }
}

参考:角度变化检测


27
我已经在一些帖子中看到了可观察的或EventEmitter尾随的$,例如itemAdded$。那是RxJS约定吗?这是从哪里来的?
Mark Rajcok '16

1
好答案。您说:“对App模型的更改会自动传播到直接或间接绑定到同一模型的所有组件。” 我有一种直觉,认为这种方式无法正常工作(但我不确定)。Savkin的另一篇博客文章提供了一个更改street应用程序模型属性的组件示例,但是由于Angular 2通过身份/引用实现更改检测,因此不会传播(onChanges不调用)更改,因为应用程序模型引用未更改(续...)
Mark Rajcok

10
您可能需要更新答案,以在服务中使用Observable而不是EventEmitter。参见stackoverflow.com/a/35568924/215945stackoverflow.com/questions/36076700
Mark Rajcok

2
是的,后缀$是Cycle.js流行的RxJS约定。cycle.js.org/…–
乔迪·泰特(

4
您不应该手动订阅事件发射器。在最终版本中可能无法观察到!看到这个:bennadel.com/blog/…–
NetProvoke

49

以下代码作为使用共享服务处理事件来替换Angular 2中$ scope.emit()$ scope.broadcast()的示例。

import {Injectable} from 'angular2/core';
import * as Rx from 'rxjs/Rx';

@Injectable()
export class EventsService {
    constructor() {
        this.listeners = {};
        this.eventsSubject = new Rx.Subject();

        this.events = Rx.Observable.from(this.eventsSubject);

        this.events.subscribe(
            ({name, args}) => {
                if (this.listeners[name]) {
                    for (let listener of this.listeners[name]) {
                        listener(...args);
                    }
                }
            });
    }

    on(name, listener) {
        if (!this.listeners[name]) {
            this.listeners[name] = [];
        }

        this.listeners[name].push(listener);
    }

    off(name, listener) {
        this.listeners[name] = this.listeners[name].filter(x => x != listener);
    }

    broadcast(name, ...args) {
        this.eventsSubject.next({
            name,
            args
        });
    }
}

用法示例:

广播:

function handleHttpError(error) {
    this.eventsService.broadcast('http-error', error);
    return ( Rx.Observable.throw(error) );
}

听众:

import {Inject, Injectable} from "angular2/core";
import {EventsService}      from './events.service';

@Injectable()
export class HttpErrorHandler {
    constructor(eventsService) {
        this.eventsService = eventsService;
    }

    static get parameters() {
        return [new Inject(EventsService)];
    }

    init() {
        this.eventsService.on('http-error', function(error) {
            console.group("HttpErrorHandler");
            console.log(error.status, "status code detected.");
            console.dir(error);
            console.groupEnd();
        });
    }
}

它可以支持多个参数:

this.eventsService.broadcast('something', "Am I a?", "Should be b", "C?");

this.eventsService.on('something', function (a, b, c) {
   console.log(a, b, c);
});

这是做什么的?静态get parameters(){return [new Inject(EventsService)]; }
Beanwah's

在此示例中,我使用的是Ionic 2 Framework。调用构造函数方法时,将调用静态参数方法,该方法用于将依赖项提供给构造函数。这里说明stackoverflow.com/questions/35919593/...
jim.taylor.1974

1
做得很好。简单,并为整个应用程序提供了一个易于适应的通知系统,而不仅仅是一次性的。
Mike M

我刚刚创建了具有通配符支持的类似服务。希望能帮助到你。github.com/govorov/ng-radio
Stanislav E.

2
off(name, listener) { this.listeners[name] = this.listeners[name].filter(x => x != listener); }
太棒

16

我正在使用包装rxjs Subject(TypeScript)的消息服务

Plunker示例:消息服务

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
import 'rxjs/add/operator/filter'
import 'rxjs/add/operator/map'

interface Message {
  type: string;
  payload: any;
}

type MessageCallback = (payload: any) => void;

@Injectable()
export class MessageService {
  private handler = new Subject<Message>();

  broadcast(type: string, payload: any) {
    this.handler.next({ type, payload });
  }

  subscribe(type: string, callback: MessageCallback): Subscription {
    return this.handler
      .filter(message => message.type === type)
      .map(message => message.payload)
      .subscribe(callback);
  }
}

组件可以订阅和广播事件(发送者):

import { Component, OnDestroy } from '@angular/core'
import { MessageService } from './message.service'
import { Subscription } from 'rxjs/Subscription'

@Component({
  selector: 'sender',
  template: ...
})
export class SenderComponent implements OnDestroy {
  private subscription: Subscription;
  private messages = [];
  private messageNum = 0;
  private name = 'sender'

  constructor(private messageService: MessageService) {
    this.subscription = messageService.subscribe(this.name, (payload) => {
      this.messages.push(payload);
    });
  }

  send() {
    let payload = {
      text: `Message ${++this.messageNum}`,
      respondEvent: this.name
    }
    this.messageService.broadcast('receiver', payload);
  }

  clear() {
    this.messages = [];
  }

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

(接收者)

import { Component, OnDestroy } from '@angular/core'
import { MessageService } from './message.service'
import { Subscription } from 'rxjs/Subscription'

@Component({
  selector: 'receiver',
  template: ...
})
export class ReceiverComponent implements OnDestroy {
  private subscription: Subscription;
  private messages = [];

  constructor(private messageService: MessageService) {
    this.subscription = messageService.subscribe('receiver', (payload) => {
      this.messages.push(payload);
    });
  }

  send(message: {text: string, respondEvent: string}) {
    this.messageService.broadcast(message.respondEvent, message.text);
  }

  clear() {
    this.messages = [];
  }

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

返回rxjs 对象的subscribe方法,可以像这样取消订阅该对象:MessageServiceSubscription

import { Subscription } from 'rxjs/Subscription';
...
export class SomeListener {
  subscription: Subscription;

  constructor(private messageService: MessageService) {
    this.subscription = messageService.subscribe('someMessage', (payload) => {
      console.log(payload);
      this.subscription.unsubscribe();
    });
  }
}

另请参阅以下答案:https : //stackoverflow.com/a/36782616/1861779

Plunker示例:消息服务


2
非常有价值。感谢你的回答。我只是发现您无法使用这种方式与两个不同模块中的两个组件进行通信。为了实现此目标,我必须通过在其中添加提供程序在app.module级别注册MessageService。无论如何,这是一种非常酷的方式。
Rukshan Dangalla

这一切都过时了。特别是无法成功加载任何资源的插件。它们都是500个错误代码。
tatsu

我得到了Property 'filter' does not exist on type 'Subject<EventMessage>'.
Drew

@Drew,在较新的RxJS版本中使用this.handler.pipe(filter(...))。请参见可出租运营商
t.888

1
@ t.888谢谢,我知道了。更新的订阅功能如下所示:return this.handler.pipe( filter(message => message.type === type), map(message => message.payload) ).subscribe(callback);
Drew

12

不要将 EventEmitter用于服务通信。

您应该使用Observable类型之一。我个人喜欢BehaviorSubject。

简单的例子:

您可以传递初始状态,在这里我传递null

让subject = new BehaviorSubject(null);

当您想更新主题时

subject.next(myObject)

观察任何服务或组件,并在获得新更新时采取行动。

subject.subscribe(this.YOURMETHOD);

这是更多信息。


1
您能否详细说明做出此设计决定的原因?
mtraut

@mtraut该链接也有全面的说明。
Danial Kalbasi

对于更详细的解释如何使用BehaviourSubject请阅读这篇文章blog.cloudboost.io/...
rafalkasa

绝对是我所需要的。漂亮又简单:)
低至


2

我最喜欢的方法是在服务中使用行为主题或事件发射器(几乎相同)来控制我的所有子组件。

使用angular cli,运行ng gs创建新服务,然后使用BehaviorSubject或EventEmitter

export Class myService {
#all the stuff that must exist

myString: string[] = [];
contactChange : BehaviorSubject<string[]> = new BehaviorSubject(this.myString);

   getContacts(newContacts) {
     // get your data from a webservices & when you done simply next the value 
    this.contactChange.next(newContacts);
   }
}

当您这样做时,将您的服务用作提供者的每个组件都将知道更改。只需像使用eventEmitter一样订阅结果即可;)

export Class myComp {
#all the stuff that exists like @Component + constructor using (private myService: myService)

this.myService.contactChange.subscribe((contacts) => {
     this.contactList += contacts; //run everytime next is called
  }
}

1

我在这里创建了一个发布订阅示例:

http://www.syntaxsuccess.com/viewarticle/pub-sub-in-angular-2.0

这个想法是使用RxJs主题连接一个Observer和Observables,作为发出和订阅自定义事件的通用解决方案。在我的示例中,我使用客户对象进行演示

this.pubSubService.Stream.emit(customer);

this.pubSubService.Stream.subscribe(customer => this.processCustomer(customer));

这也是一个现场演示:http : //www.syntaxsuccess.com/angular-2-samples/#/demo/pub-sub


1

这是我的版本:

export interface IEventListenr extends OnDestroy{
    ngOnDestroy(): void
}

@Injectable()
export class EventManagerService {


    private listeners = {};
    private subject = new EventEmitter();
    private eventObserver = this.subject.asObservable();


    constructor() {

        this.eventObserver.subscribe(({name,args})=>{



             if(this.listeners[name])
             {
                 for(let listener of this.listeners[name])
                 {
                     listener.callback(args);
                 }
             }
        })

    }

    public registerEvent(eventName:string,eventListener:IEventListenr,callback:any)
    {

        if(!this.listeners[eventName])
             this.listeners[eventName] = [];

         let eventExist = false;
         for(let listener of this.listeners[eventName])
         {

             if(listener.eventListener.constructor.name==eventListener.constructor.name)
             {
                 eventExist = true;
                 break;
             }
         }

        if(!eventExist)
        {
             this.listeners[eventName].push({eventListener,callback});
        }
    }

    public unregisterEvent(eventName:string,eventListener:IEventListenr)
    {

        if(this.listeners[eventName])
        {
            for(let i = 0; i<this.listeners[eventName].length;i++)
            {

                if(this.listeners[eventName][i].eventListener.constructor.name==eventListener.constructor.name)
                {
                    this.listeners[eventName].splice(i, 1);
                    break;
                }
            }
        }


    }


    emit(name:string,...args:any[])
    {
        this.subject.next({name,args});
    }
}

用:

export class <YOURCOMPONENT> implements IEventListener{

  constructor(private eventManager: EventManagerService) {


    this.eventManager.registerEvent('EVENT_NAME',this,(args:any)=>{
       ....
    })


  }

  ngOnDestroy(): void {
    this.eventManager.unregisterEvent('closeModal',this)
  }

}

发射:

 this.eventManager.emit("EVENT_NAME");

0

我们实现了ngModelChange observable指令,该指令通过您在自己的组件中实例化的事件发射器发送所有模型更改。您只需要将事件发射器绑定到指令。

参见:https : //github.com/atomicbits/angular2-modelchangeobservable

在html中,绑定事件发射器(在此示例中为countryChanged):

<input [(ngModel)]="country.name"
       [modelChangeObservable]="countryChanged" 
       placeholder="Country"
       name="country" id="country"></input>

在您的打字稿组件中,对EventEmitter进行一些异步操作:

import ...
import {ModelChangeObservable} from './model-change-observable.directive'


@Component({
    selector: 'my-component',
    directives: [ModelChangeObservable],
    providers: [],
    templateUrl: 'my-component.html'
})

export class MyComponent {

    @Input()
    country: Country

    selectedCountries:Country[]
    countries:Country[] = <Country[]>[]
    countryChanged:EventEmitter<string> = new EventEmitter<string>()


    constructor() {

        this.countryChanged
            .filter((text:string) => text.length > 2)
            .debounceTime(300)
            .subscribe((countryName:string) => {
                let query = new RegExp(countryName, 'ig')
                this.selectedCountries = this.countries.filter((country:Country) => {
                    return query.test(country.name)
                })
            })
    }
}

0

服务事件:组件可以订阅服务事件。例如,两个同级组件可以订阅相同的服务事件,并通过修改其各自的模型进行响应。在下面的更多内容。

但是,请确保在取消父组件时取消订阅。

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.