使用变量修改器修改 RxJS observable - Angular

Mutating a RxJS observable with variable mutators - Angular

我做了一个从远程源获取一些用户数据的服务。该服务是一种获取多个用户的方法,一种获取特定用户的方法。 来自这两个方法的可观察对象 returned 通过 map() 获得 .pipe(ed) 以便能够在用户对象被使用之前改变它们。

我想要的是只为多用户流和单用户流定义一次修改器,但我 运行 遇到了我当前方法的范围问题。

请注意,我称用户为“英雄”。这是一个设计方面。 以下是我的 HeroService class:

中的相应方法
export class HeroService {
  // [...]

  getHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>(this.heroesUrl).pipe(
      map((heroes: Hero[]) => this.mutateHeroes(heroes, this.addSkillsToHero)),
      map((heroes: Hero[]) => this.mutateHeroes(heroes, this.splitName))
    );
  }

  getHero(id): Observable<Hero> {
    return this.http.get<Hero>(this.heroesUrl + "/" + id).pipe(
      map((hero: Hero) => this.addSkillsToHero(hero)),
      map((hero: Hero) => this.splitName(hero))
    );
  }

  private mutateHeroes(heroes: Hero[], mutator) {
    heroes.forEach((hero: Hero) => {
      hero = mutator(hero);
    });
    return heroes;
  }

  private splitName(hero: Hero) {
    let heroNames: string[] = hero.name.split(" ");

    hero.firstname = heroNames.splice(0, 1).join(" ");
    hero.lastname = heroNames.splice(heroNames.length - 1, 1).join(" ");
    hero.middlename = heroNames.join(" ");

    return hero;
  }

  private addSkillsToHero(hero: Hero) {
    hero.skills = this.getSkills(Math.ceil(Math.random() * 10));
    return hero;
  }

  private getSkills(count: number): string[] {
    let skills: string[] = [];
    let i;

    for (i = 0; i < count; i++) {
      skills.push(this.getRandomSkill());
    }

    return skills;
  }

  private getRandomSkill(): string {
    return SKILL_TAGS[Math.floor(Math.random() * SKILL_TAGS.length)];
  }
  // [...]
}

Observable 的 catchError() return:Cannot read property 'getSkills' of undefined 我怀疑增变器没有在 class 范围内被调用,因此无法找到。

我如何在 JS 中做这样的事情?

可以在以下位置检查整个项目:

问题出在这里:

 getHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>(this.heroesUrl).pipe(
      map((heroes: Hero[]) => this.mutateHeroes(heroes, this.addSkillsToHero)),
      map((heroes: Hero[]) => this.mutateHeroes(heroes, this.splitName))
    );
  }

这样传递 this.addSkillsToHero,不会保留 this 引用。您正在传递对函数的引用,this 是在调用时确定的。您要么需要将该函数绑定到您需要的 this,要么使用箭头函数为您捕获 this。您可以在此处找到更多详细信息:JavaScript function binding (this keyword) is lost after assignment or https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this

您可以这样尝试:

 getHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>(this.heroesUrl).pipe(
      map((heroes: Hero[]) => this.mutateHeroes(heroes, this.addSkillsToHero.bind(this))),
      map((heroes: Hero[]) => this.mutateHeroes(heroes, this.splitName.bind(this)))
    );
  }

或使用箭头函数:

 getHeroes(): Observable<Hero[]> {
    return this.http.get<Hero[]>(this.heroesUrl).pipe(
      map((heroes: Hero[]) => this.mutateHeroes(heroes, h => this.addSkillsToHero(h))),
      map((heroes: Hero[]) => this.mutateHeroes(heroes, h => this.splitName(h)))
    );
  }

救援箭头函数 (Lamdas)

如前所述,您的问题归结为函数上下文 (this) 的决定方式。这是在 运行 时间完成的事情。

要扩展此处给出的另一个答案,您可以定义一个已绑定其上下文 (this) 的函数。诀窍是尽早调用该函数并使用该函数的部分应用版本。一种方法是使用 bind.

const functionName = (function (args){
  /* Some code */
  return /*something*/
}).bind(this);

这有点丑陋,所以 JavaScript 有做同样事情的箭头函数。

const functionName = (args) => {
  /* Some code */
  return /*something*/
}

那更漂亮了。更好的是 一行 单表达式 Lamdas 有一个隐含的 return:

const functionName = (args) => {
  return /*something*/;
}
// Is the same as
const functionName = (args) => /*something*/;

普通函数在调用时会被赋予一个上下文。箭头函数总是将它们定义的范围绑定为它们的上下文。如果您不使用 Javascript 的原型继承,通常没有太多理由需要从定义它的范围更改函数的上下文。

我发现如果您愿意稍微学习一下这些知识,您真的可以整理您的代码。

以下是我将完全使用箭头函数编写您的 HeroService class 的方式。如果这是最好的方法,还有待商榷。当然,这种方法允许更多 declarative/functional 风格。在实践中,两者的结合可以让你两全其美。

export class HeroService {
  // [...]

  public getHeroes = (): Observable<Hero[]> =>
    this.http.get<Hero[]>(this.heroesUrl).pipe(
      map(this.mutateHeroes(this.addSkillsToHero)),
      map(this.mutateHeroes(this.splitName))
    );

  public getHero = (id): Observable<Hero> => 
    this.http.get<Hero>(this.heroesUrl + "/" + id).pipe(
      map(this.addSkillsToHero),
      map(this.splitName)
    );

  private mutateHeroes = transformer => 
    (heroes: Hero[]) => heroes.map(transformer);

  private splitName = (hero: Hero) => {
    let heroNames: string[] = hero.name.split(" "); 
    return ({
      ...hero,
      firstName: heroNames.splice(0, 1).join(" "),
      lastname: heroNames.splice(heroNames.length - 1, 1).join(" "),
      middlename: heroNames.join(" ")
    });
  }

  private addSkillsToHero = (hero: Hero) => ({
    ...hero, skills: this.getSkills(Math.ceil(Math.random() * 10))
  });

  private getSkills = (count: number): string[] => 
    Array.from({length: count}).map(this.getRandomSkill);

  private getRandomSkill = (): string => 
    SKILL_TAGS[Math.floor(Math.random() * SKILL_TAGS.length)];
  
  // [...]
}

额外:组合函数

一个很酷的 属性 正确实现的映射函数(不管你是在数组还是流上映射)是:

map(x => f(g(x))) 
  === 
map(g).map(f)

这告诉我们,如果我们组合 addSkillsToHerosplitName,那么您可以用一个 map 而不是两个来做到这一点。 RxJS 流实际上是换能器,所以它们(在某种程度上)为你做这件事,但数组映射不是这样你也可以通过组合获得一些边际性能(你只迭代你的数组一次!)。

通常你有一个像 Ramda/Redux/Lodash/etc 这样的库,你可以写像

这样的东西
private addSkillsAndSplitName = 
  compose(this.addSkillsToHero, this.splitName);

但是用 vanilla JavaScript 组合两个函数也很容易。

private addSkillsAndSplitName = 
  hero => this.addSkillsToHero(this.splitName(hero))

然后你可以像这样使用这个新功能:

export class HeroService {
  // [...]

  public getHeroes = (): Observable<Hero[]> =>
    this.http.get<Hero[]>(this.heroesUrl).pipe(
      map(this.mutateHeroes(this.addSkillsAndSplitName))
    );

  public getHero = (id): Observable<Hero> => 
    this.http.get<Hero>(this.heroesUrl + "/" + id).pipe(
      map(this.addSkillsAndSplitName)
    );

而且您没有在可测试性、灵活性或关注点分离方面受到影响,因为功能仍然存在于单独的函数中。