简体   繁体   English

自定义 RxJS 操作符无法正常工作

[英]Custom RxJS operator not working properly

I've been trying to refactor a snippet of code into a custom rxjs operator but I cannot get it to work.我一直在尝试将一段代码重构为自定义 rxjs 运算符,但我无法让它工作。

This is my custom operator so far:到目前为止,这是我的自定义运算符:

export const isLastItemTheSame = (oldValue: any[], key: string, condition: boolean) => {

  return condition ? <T>(obsv: Observable<T[]>) => obsv.pipe(
    filter(newValue => {

      try {
        return (oldValue[oldValue.length - 1][key] === newValue[newValue.length - 1][key]);
      }
      catch(err) {
        return false;
      }
    }),
    mapTo(true)
  ) : <T>(obsv: Observable<T>) => obsv.pipe(ignoreElements());
};

The idea here is to compare a new list against an old list, if the last item in both lists match, the success callback of the subscribe should not fire.这里的想法是将新列表与旧列表进行比较,如果两个列表中的最后一项匹配,则不应触发subscribe的成功回调。 But if the items are not a match, it should.但是,如果项目不匹配,则应该匹配。

The issues I'm having right now is:我现在遇到的问题是:

  • <T>(obsv: Observable<T>) => obsv.pipe(ignoreElements()) bit doesn't work as it doesn't fire the success callback. <T>(obsv: Observable<T>) => obsv.pipe(ignoreElements())位不起作用,因为它不会触发成功回调。
  • When condition is true , the operator returns a boolean rather than the new list.conditiontrue ,运算符返回一个布尔值而不是新列表。 This makes it impossible to bind the new list to this.items in the subscribe success callback.这使得无法在订阅成功回调中将新列表绑定到this.items

I use it like:我像这样使用它:

const source$ = this.api.get<CustomResponse>('events');

source$.pipe(
  first(),
  tap((res) => this.total = res.totalsize || 0),
  map((res) => res.list),
  isLastItemTheSame(this.items, 'eventid', this.items.length && !isReset)
).subscribe((items: IEvent[]) => {

    // if (this.items.length && !isReset) {

    //   if (items[items.length - 1].eventid === this.items[this.items.length - 1].eventid) {
    //     return;
    //   }
    // }

    this.items = isReset ? items : [...this.items, ...items];
  }, (err) => {

    if (err.status !== 401) {

      this.router.navigate(['dashboard']).then(() => {
        this.notifications.newNotification({message: this.translate.instant('NOTIFICATIONS.EVENTS.GET_LIST_ERROR'), theme: 'danger'});
      });
    }
  }
);

The code that has been commented out is the code that I'm trying to refactor, which makes it easy to see what I'm trying to achieve.被注释掉的代码是我正在尝试重构的代码,这样可以很容易地看到我想要实现的目标。

How can I fix this?我怎样才能解决这个问题?

Edit: This answer assumes you want to actually compare against an external reference list rather than just the previously emitted value of the same observable.编辑:此答案假定您要实际与外部参考列表进行比较,而不仅仅是与同一 observable 先前发出的值进行比较。 That's what your question stated, but if you mean to reference the previous value, take a look at my second answer .这就是你的问题所说的,但如果你想引用以前的值,请看我的第二个答案


Let's make a few observations first:让我们先做一些观察:

When condition is true, the operator returns a boolean rather than the new list.当条件为真时,运算符返回一个布尔值而不是新列表。

That's what your mapTo(true) does, so just leave that out :-)这就是你的mapTo(true)所做的,所以把它排除在外:-)

ignoreElements()) bit doesn't work as it doesn't fire the success callback. ignoreElements()) 位不起作用,因为它不会触发成功回调。

That's because you don't want to ignore the elements, but just return the observable unchanged in this case.那是因为您不想忽略元素,而只是在这种情况下返回 observable 不变。

However, I would not move the condition into the operator – it has nothing to do with it, logically, and you pass the entire logic (the "body" of the if) into the operator anyway.但是,我不会将条件移到运算符中——从逻辑上讲,它与它无关,并且您无论如何都将整个逻辑(if 的“主体”)传递给运算符。

Furthermore, your operator is currently lacking a proper type signature and creating custom operators can be done in a less verbose anyway, eg using the static pipe function (which we don't even need here, though).此外,您的操作符目前缺乏正确的类型签名,并且创建自定义操作符可以以更简洁的方式完成,例如使用静态pipe函数(尽管我们在这里甚至不需要)。

Lastly, we cannot just pass the reference list statically if you want it to be reevaluated on every value coming through, so we need to use a supplier instead.最后,如果您希望在通过的每个值上重新评估引用列表,我们不能只是静态传递引用列表,因此我们需要使用供应商。


So, let's fix all of this.所以,让我们解决这一切。 My proposal for the operator would be this:我对运营商的建议是这样的:

import { OperatorFunction } from 'rxjs';
import { filter } from 'rxjs/operators';

export function filterIfLastElementMatches<T extends Record<any, any>>(
  previousSupplier: () => T[], 
  key: keyof T
): OperatorFunction<T[], T[]> {
  return filter(value => {
    const previous = previousSupplier();
    return !previous.length || !value.length || value[value.length - 1][key] !== previous[previous.length - 1][key]
  });
}

Then to use this and combine it with the conditional part:然后使用它并将其与条件部分结合起来:

source$.pipe(
  // …
  !isReset ? filterIfLastElementMatches(() => this.items, 'eventid') : tap(),
).subscribe(items => { /* … */ });

See it in action here .这里查看它的实际效果。

Note that the semantics for the condition are slightly different here since it will be evaluated when the wrapping method is executed, not when the emitted value comes along.请注意,这里条件的语义略有不同,因为它将在执行包装方法时进行评估,而不是在发出的值出现时进行评估。 I assume this isn't critical here.我认为这在这里并不重要。

Note : This is a second answer, but this time we assume you want to compare against the previously emitted value rather than an external reference list.注意:这是第二个答案,但这次我们假设您要与先前发出的值而不是外部引用列表进行比较。 The general points in my other answer still apply.我的另一个答案中的一般观点仍然适用。

If this is the mechanism you need, I strongly would advise using this answer over the one using an external supplier.如果这是您需要的机制,我强烈建议使用此答案而不是使用外部供应商的答案。


If the value to compare against should just be the previously emitted value, then you can simply use distinctUntilChanged for this with a custom comparator function.如果要比较的值应该只是之前发出的值,那么您可以简单地将distinctUntilChanged与自定义比较器函数一起使用。 If you insist on wrapping it into a custom operator, it could be this:如果您坚持将其包装到自定义运算符中,则可能是这样的:

import { OperatorFunction } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';

export function distinctUntilLastElementByKey<T extends Record<any, any>>(
    key: keyof T
): OperatorFunction<T[], T[]> {
    return distinctUntilChanged((previous, value) =>
        previous.length && value.length && value[value.length - 1][key] === previous[previous.length - 1][key]
    );
}

You can see it in action here .你可以在这里看到它的实际效果。

However, it might make more sense to only refactor the comparator function into its own function and in the code just use但是,仅将比较器函数重构为自己的函数并在代码中仅使用

source$.pipe(distinctUntilChanged(createLastItemKeyComparator('eventid')))

How about this:.这个怎么样:。 I mean - as a skeleton - you could extend the key testing to your needs.我的意思是 - 作为一个骨架 - 您可以将关键测试扩展到您的需求。

const filterIfLastEqual = () => <T>(source: Observable<T[]>) => {
  return new Observable(observer => {
    let lastValue: T;

    return source.subscribe({
      next(x) {
        const [nowLast] = x.slice(-1);
        if (lastValue !== nowLast) {
          console.log('diff', lastValue, nowLast)
          observer.next(x);
          lastValue = nowLast;
        } else {
          console.log('skipping', lastValue, nowLast)
        }
      },
      error(err) {
        observer.error(err);
      },
      complete() {
        observer.complete();
      }
    })

  })
}

const emitter = new Subject<any[]>();
emitter.pipe(
  filterIfLastEqual(),
).subscribe(
  v => console.log('received value:', v)
)

// test cases
emitter.next([1, 2, 3, 4]);
emitter.next([1, 2, 3, 4]);
emitter.next([1, 2, 3, 5]);
emitter.next([1, 2, 3, '5']);

You could also use scan() , which is stateful, in conjunction with distnctUntilChanged() .您还可以将有状态的scan()distnctUntilChanged()结合使用。 The logic would be pretty much the same.逻辑几乎相同。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM