Мутация наблюдаемого RxJS с переменными мутаторами - Angular

Я создал сервис, который получает некоторые пользовательские данные из удаленного источника. Сервис - это метод для получения нескольких пользователей и один для получения определенного пользователя. Наблюдаемые, возвращаемые двумя метонами, получают .pipe (ed) через map (), чтобы иметь возможность изменять пользовательские объекты до того, как они будут использованы.

Я хочу определить мутаторы только один раз, как для потока с несколькими пользователями, так и для потока с одним пользователем, но при моем текущем подходе я сталкиваюсь с проблемами области действия.

Имейте в виду, что я называю пользователей героями. Это аспект дизайна. Ниже приведены соответствующие методы из моего класса HeroService:

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)];
  }
  // [...]
}

CatchError () Observable (s) возвращает: Cannot read property 'getSkills' of undefined Я подозреваю, что мутатор не вызывается внутри области класса и поэтому не может быть найден.

Как бы я сделал это в JS?

Весь проект можно осмотреть по адресу:


person DumperJumper    schedule 19.02.2021    source источник


Ответы (2)


Проблема здесь:

 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 (это ключевое слово) теряется после назначения или 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)))
    );
  }
person Andrei Tătar    schedule 19.02.2021
comment
Спасибо, это был ответ, который я искал. Мне было бы интересно получить техническое объяснение, почему его перенос в другую функцию сохраняет область видимости. - person DumperJumper; 19.02.2021
comment
@DumperJumper добавил еще несколько деталей. - person Andrei Tătar; 19.02.2021

Стрелочные функции (Ламда) спешат на помощь

Как уже упоминалось, ваша проблема сводится к тому, как определяется контекст функции (this). Это то, что делается во время выполнения.

Чтобы расширить другой ответ, приведенный здесь, вы можете определить функцию с уже привязанным контекстом (this). Хитрость заключается в том, чтобы вызвать функцию на раннем этапе и работать с частично примененной версией этой функции. Один из способов сделать это - использовать bind.

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

Это довольно уродливо, поэтому в JavaScript есть стрелочные функции, которые делают то же самое.

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

Это намного красивее. Еще лучше то, что однострочные Ламды с одним выражением имеют неявный возврат:

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

При вызове нормальным функциям предоставляется контекст. Стрелочные функции всегда связывают область, в которой они определены, как свой контекст. Если вы не используете прототипное наследование Javascript, обычно нет особых причин, по которым контекст функции должен был бы измениться из области, в которой он определен.

Я считаю, что вы действительно можете улучшить свой код, если хотите немного углубиться в эти знания.

Вот как я бы написал ваш класс HeroService полностью со стрелочными функциями. Если это лучший способ сделать это, это предмет обсуждения. Конечно, такой подход позволяет использовать больше декларативного / функционального стиля. На практике сочетание этих двух вещей дает вам лучшее из обоих миров.

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)

Это говорит нам, что если мы составим addSkillsToHero и splitName, то вы можете сделать это с одним map вместо двух. Потоки RxJS на самом деле являются преобразователями, поэтому они делают это за вас (в некотором смысле), но карта массива - нет, поэтому вы также можете получить некоторую предельную производительность с помощью композиций (вы повторяете свой массив только один раз!).

Обычно у вас есть библиотека типа Ramda / Redux / Lodash / и т.д., и вы могли бы написать что-то вроде

private addSkillsAndSplitName = 
  compose(this.addSkillsToHero, this.splitName);

Но составить две функции довольно легко и с ванильным 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)
    );

И вы не пострадали от тестируемости, гибкости или разделения задач, поскольку функциональность по-прежнему существует в отдельных функциях.

person Mrk Sef    schedule 19.02.2021