【问题标题】:Asynchronous call in angular using event emitter and services for cross-component communication使用事件发射器和服务进行跨组件通信的角度异步调用
【发布时间】:2020-03-22 05:42:22
【问题描述】:

无法将从 subscribe 方法接收到的值存储在模板变量中。

照片细节组件

import { Component, OnInit, Input } from "@angular/core";
import { PhotoSevice } from "../photo.service";
import { Photo } from "src/app/model/photo.model";

@Component({
  selector: "app-photo-detail",
  templateUrl: "./photo-detail.component.html",
  styleUrls: ["./photo-detail.component.css"]
})
export class PhotoDetailComponent implements OnInit {

  url: string;

  constructor(private photoService: PhotoSevice) {
    this.photoService.photoSelected.subscribe(data => {
      this.url = data;
      console.log(this.url);
    });
    console.log(this.url);
  }

  ngOnInit() {

  }
}

外部console.log给出未定义,视图中没有呈现任何内容,但在subscibe方法中我可以看到值。那么,我怎样才能在我的视图中显示它?

照片组件

import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Params, Router } from "@angular/router";
import { FnParam } from "@angular/compiler/src/output/output_ast";
import { AlbumService } from "../service/album.service";
import { Photo } from "../model/photo.model";
import { PhotoSevice } from "./photo.service";

@Component({
  selector: "app-photos",
  templateUrl: "./photos.component.html",
  styleUrls: ["./photos.component.css"]
})
export class PhotosComponent implements OnInit {
  selectedAlbumId: string;
  photoList: Photo[] = [];
  photoSelected: Photo;
  isLoading: Boolean;
  constructor(
    private rout: ActivatedRoute,
    private albumService: AlbumService,
    private router: Router,
    private photoService: PhotoSevice
  ) { }

  ngOnInit() {
    this.isLoading = true;
    this.rout.params.subscribe((params: Params) => {
      this.selectedAlbumId = params["id"];
      this.getPhotos(this.selectedAlbumId);
    });
  }

  getPhotos(id: string) {
    this.albumService.fetchPhotos(this.selectedAlbumId).subscribe(photo => {
      this.photoList = photo;
      this.isLoading = false;
    });
  }

  displayPhoto(url: string, title: string) {

    console.log(url);
    this.photoService.photoSelected.emit(url);
    this.router.navigate(["/photo-detail"]);
  }
}

请解释一下它是如何工作的以及如何解决它,以便我可以在模板视图中存储和显示从订阅和异步调用接收到的值。

这是两个组件的视图---

photo.component.html

<div *ngIf="isLoading">
  <h3>Loading...</h3>
</div>

<div class="container" *ngIf="!isLoading">
  <div class="card-columns">
    <div *ngFor="let photo of photoList" class="card">
      <img
        class="card-img-top"
        src="{{ photo.thumbnailUrl }}"
        alt="https://source.unsplash.com/random/300x200"
      />
      <div class="card-body">
        <a
          class="btn btn-primary btn-block"
          (click)="displayPhoto(photo.url, photo.title)"
          >Enlarge Image</a
        >
      </div>
    </div>
  </div>
</div>

photo-detail.component.ts

<div class="container">
  <div class="card-columns">
    <div class="card">
      <img class="card-img-top" src="{{ url }}" />
    </div>
  </div>
</div>

photo.service.ts

import { Injectable } from "@angular/core";
import { EventEmitter } from "@angular/core";

@Injectable({ providedIn: "root" })
export class PhotoSevice {
  photoSelected = new EventEmitter();
 // urlService: string;
}

这里是我的 github 存储库的链接,我将代码保存在 cmets 中并在那里使用了不同的方法。 如果您检查相册组件,我还订阅了 http 请求并在相册组件的模板变量中分配了值。 也有值作为未定义的 subscibe 方法出现,但我可以在模板中访问它。

https://github.com/Arpan619Banerjee/angular-accelerate

这里是相册组件和服务的详细信息 请将此与事件发射器案例进行比较,并解释一下有什么区别- albums.component.ts

import { Component, OnInit } from "@angular/core";
import { AlbumService } from "../service/album.service";
import { Album } from "../model/album.model";

@Component({
  selector: "app-albums",
  templateUrl: "./albums.component.html",
  styleUrls: ["./albums.component.css"]
})
export class AlbumsComponent implements OnInit {
  constructor(private albumService: AlbumService) {}
  listAlbums: Album[] = [];
  isLoading: Boolean;

  ngOnInit() {
    this.isLoading = true;
    this.getAlbums();
  }

  getAlbums() {
    this.albumService.fetchAlbums().subscribe(data => {
      this.listAlbums = data;
      console.log("inside subscibe method-->" + this.listAlbums); // we have data here
      this.isLoading = false;
    });
    console.log("outside subscribe method----->" + this.listAlbums); //empty list==== but somehow we have the value in the view , this doesn t work
    //for my photo and photo-detail component.
  }
}

albums.component.html

<div *ngIf="isLoading">
  <h3>Loading...</h3>
</div>
<div class="container" *ngIf="!isLoading">
  <h3>Albums</h3>
  <app-album-details
    [albumDetail]="album"
    *ngFor="let album of listAlbums"
  ></app-album-details>
</div>

album.service.ts

import { Injectable } from "@angular/core";
import { HttpClient, HttpParams } from "@angular/common/http";
import { map, tap } from "rxjs/operators";
import { Album } from "../model/album.model";
import { Observable } from "rxjs";
import { UserName } from "../model/user.model";

@Injectable({ providedIn: "root" })
export class AlbumService {
  constructor(private http: HttpClient) {}
  albumUrl = "http://jsonplaceholder.typicode.com/albums";
  userUrl = "http://jsonplaceholder.typicode.com/users?id=";
  photoUrl = "http://jsonplaceholder.typicode.com/photos";

  //get the album title along with the user name
  fetchAlbums(): Observable<any> {
    return this.http.get<Album[]>(this.albumUrl).pipe(
      tap(albums => {
        albums.map((album: { userId: String; userName: String }) => {
          this.fetchUsers(album.userId).subscribe((user: any) => {
            album.userName = user[0].username;
          });
        });
        // console.log(albums);
      })
    );
  }

  //get the user name of the particular album with the help of userId property in albums
  fetchUsers(id: String): Observable<any> {
    //let userId = new HttpParams().set("userId", id);
    return this.http.get(this.userUrl + id);
  }

  //get the photos of a particular album using the albumId
  fetchPhotos(id: string): Observable<any> {
    let selectedId = new HttpParams().set("albumId", id);
    return this.http.get(this.photoUrl, {
      params: selectedId
    });
  }
}

我已按照 cmets 中的说明在偶数发射器中添加了控制台日志,这是我得到的预期行为。

【问题讨论】:

  • 可以显示PhotoSevice吗?在这种情况下,您不需要事件发射器。你需要一个Subject
  • 是的,我已经附加了照片服务,是的,我通过简单地在照片服务中定义一个变量并将其设置在照片组件中来满足要求,然后我从我的照片细节组件中访问它。有效。请检查我的问题一次,我添加了更多细节。请将该行为与 http 调用和事件发射器进行比较,并向我解释发生了什么。
  • 我已经发布了答案。将来请尝试从问题中删除代码的非必要部分。

标签: javascript angular asynchronous components angular-event-emitter


【解决方案1】:

问题分为两部分。

第 1 部分 - 照片和照片细节组件

  1. EventEmitter 用于将带有@Output 装饰器的变量从子组件(不是服务)发送到父组件。然后它可以被它的模板中的父组件绑定。一个简单而好的例子可以在here 找到。注意应用组件模板中的(notify)="receiveNotification($event)"

  2. 对于您的情况,使用SubjectBehaviorSubject 是一个更好的主意。它们之间的区别可以在我的另一个答案here 中找到。试试下面的代码

photo.service.ts

import { Injectable } from "@angular/core";
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: "root" })
export class PhotoSevice {
  private photoSelectedSource = new BehaviorSubject<string>(undefined);

  public setPhotoSelected(url: string) {
    this.photoSelectedSource.next(url);
  }

  public getPhotoSelected() {
    return this.photoSelectedSource.asObservable();
  }
}

photos.component.ts

export class PhotosComponent implements OnInit {
  .
  .
  .
  displayPhoto(url: string, title: string) {
    this.photoService.setPhotoSelected(url);
    this.router.navigate(["/photo-detail"]);
  }
}

照片细节.component.ts

  constructor(private photoService: PhotoSevice) {
    this.photoService.getPhotoSelected().subscribe(data => {
      this.url = data;
      console.log(this.url);
    });
    console.log(this.url);
  }

照片细节.component.html

<ng-container *ngIf="url">
  <div class="container">
    <div class="card-columns">
      <div class="card">
        <img class="card-img-top" [src]="url"/>
      </div>
    </div>
  </div>
</ng-container>

第 2 部分 - 相册组件和服务

调用 this.albumService.fetchAlbums() 返回一个 HTTP GET 响应 observable。您正在订阅它并更新成员变量值并在模板中使用它。

根据您对另一个答案的评论:

我了解这种行为以及为什么外部 console.log 是 不足,因为执行上下文对于异步调用是不同的 它首先执行同步代码,然后执行异步代码

恐怕同步和异步调用的区别并不是那么简单。请参阅here 了解它们之间的区别。

albums.components.ts

  getAlbums() {
    this.albumService.fetchAlbums().subscribe(data => {
      this.listAlbums = data;
      console.log("inside subscibe method-->" + this.listAlbums); // we have data here
      this.isLoading = false;
    });
    console.log("outside subscribe method----->" + this.listAlbums); //empty list==== but somehow we have the value in the view , this doesn t work
    //for my photo and photo-detail component.
  }

albums.component.html

<div *ngIf="isLoading">
  <h3>Loading...</h3>
</div>
<div class="container" *ngIf="!isLoading">
  <h3>Albums</h3>
  <app-album-details
    [albumDetail]="album"
    *ngFor="let album of listAlbums"
  ></app-album-details>
</div>

问题是解释为什么尽管console.log("outside subscribe method-----&gt;" + this.listAlbums); 打印undefined,模板仍显示相册。简而言之,当您在控制台日志之外进行操作时,this.listAlbums 实际上是undefined,因为它还没有被初始化。但是在模板中,有一个加载检查*ngIf="!isLoading"。从控制器代码来看,isLoading 仅在 listAlbums 被赋值时设置为 false。因此,当您将isLoading 设置为false 时,可以确保listAlbums 包含要显示的数据。

【讨论】:

  • 好的,我明白你的意思,但我看到了一段代码,其中在服务中使用了事件发射器......
  • 它会起作用,因为EventEmitterSubject 的简单扩展。但它违背了AngularEventEmitter 的基本用例。来自 Angular 文档:在带有 @Output 指令的组件中使用以同步或异步发出自定义事件。那么那段代码是用EventEmitter错了。
  • 在您的情况下,如果您继续使用EventEmitter,照片详细信息将永远不会加载到photo-detail.component.ts。因为您首先发出 url,然后再订阅它。如前所述,EventEmitter 在内部使用Subject。所以当你订阅 observable 时,在它发出下一个值之前什么都不会发生。这就是没有加载图像的原因。对于您的用例,您需要使用BehaviorSubject
  • 那是因为 Angular 知道 listAlbums 的值何时改变。这超出了这里解释的问题范围。你可以阅读它here
  • 不同之处在于我的答案第 1 部分的第 2 点。更多信息在这里:stackoverflow.com/q/43348463/6513921
【解决方案2】:

我认为您正在尝试在照片详细信息组件中显示选定的图像,该组件使照片从服务中显示。

问题没有提到您是如何创建照片详细信息组件的。

  1. 组件是在用户选择要显示的照片后创建的吗?
  2. 组件是否在用户选择要显示的照片之前创建?

我认为第一个是你想要做的。 如果是这样,有两件事......

  1. 当您在构造函数中订阅时,订阅中的代码会在 observable 发出一段时间后运行。与此同时,订阅后的代码,即console.log(url)(外部代码)将运行,因此它将是未定义的。

  2. 如果订阅是在事件发出之后发生的,即你已经用 url 发出了事件,但到那时组件还没有订阅服务事件。所以事件丢失了,你什么也得不到。为此,您可以做一些事情

    一个。将要显示详细信息的照片添加到url,并在照片详细信息组件中获取。

    b.将服务中的主题/事件发射器转换为行为主题。这将确保即使您在稍后的某个时间点订阅,您仍然会收到上次发出的事件。

    c。如果照片详细信息组件在照片组件的模板内,则将 url 作为输入参数发送(@Input() 绑定)。

希望对你有帮助

【讨论】:

  • 我已经编辑了我的问题并粘贴了一些额外的详细信息,以帮助您更好地理解我的问题,这是我的 cmets 关于您的观点--1) 是的,我理解这种行为以及为什么外部 console.log 是不足,因为执行上下文是异步调用的差异,它首先执行同步代码,然后是异步代码,如果发生这种情况,那么专辑组件中的 http 调用会发生什么,请比较这两个并解释我。跨度>
  • 数据在视图中可用的原因是因为当时数据是从相册服务到达的
  • 好的,但是为什么它在照片细节组件视图中不可用??
  • 在照片详细信息组件中,该组件是在事件发出后创建的,因此它无法按照我在实际答案中指定的方式获取它。为了便于理解,在您发出值时放置一个日志,并在照片细节组件构造函数的构造函数中放置一个日志。您将看到首先发出事件,然后记录内部构造函数。
猜你喜欢
  • 1970-01-01
  • 2019-05-18
  • 1970-01-01
  • 1970-01-01
  • 2018-01-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多