繁体   English   中英

如何在 Angular 中执行函数之前等待最后一次击键?

[英]How to wait for the last keystroke before executing a function in Angular?

我有以下输入:

 <input type="text" placeholder="Search for new results" (input)="constructNewGrid($event)" (keydown.backslash)="constructNewGrid($event)">

和功能

constructNewGrid(e){
    // I want to wait 300ms after the last keystroke before constructing the new grid
    // If the passed time is <300ms just return without doing something
    // else start constructing new grid
}

我不太确定如何建立这样的条件。 我应该如何解决这个问题? 我在 RxJS 中阅读了关于 debounceTime 的内容,这正是我想要的,但我没有在函数中使用 observable,所以:您将如何在函数中构建这样的条件?

Observables 似乎是要走的路,但好的旧setTimeout也会让你走很长的路。 出于美观原因,让我们首先重命名您的输入处理程序:

反斜杠事件似乎有点双重,因为这也会触发(input)

<input type="text" placeholder="Search for new results"
  (input)="onInput(input.value)" #input>

在您的组件中,您有两种选择来处理此输入,使用 observable 或不使用。 让我先展示给你看:

export class GridComponent {
  private timeout?: number;

  onInput(value: string): void {
    window.clearTimeout(this.timeout);

    this.timeout = window.setTimeout(() => this.constructNewGrid(value), 300);
  }

  constructNewGrid(value: string): void {
    // expensive operations
  }
}

这看起来很简单,对于您的用例来说可能就足够了。 但是人们一直在谈论的那些很酷的 rxjs 流呢? 看起来像这样:

export class GridComponent {
  private search$ = new BehaviorSubject('');

  private destroy$ = new Subject<void>();

  ngOnInit(): void {
    this.search$.pipe(
      // debounce for 300ms
      debounceTime(300),
      // only emit if the value has actually changed
      distinctUntilChanged(),
      // unsubscribe when the provided observable emits (clean up)
      takeUntil(this.destroy$)
    ).subscribe((search) => this.constructNewGrid(search)); 
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  onInput(value: string): void {
    this.search$.next(value);
  }

  constructNewGrid(value: string): void {
    // expensive operations
  }
}

对于这样一个简单的事情,这看起来像是更多的代码,而且确实如此。 所以这取决于你。


然而,如果你觉得这种模式是你会更经常使用的东西,你也可以考虑编写一个指令,它看起来像这样:

@Directive({
  selector: '[debounceInput]'
})
export class DebounceInputDirective {
  @Input()
  debounceTime: number = 0;

  @HostListener('input', '[$event]')
  onInput(event: UIEvent): void {
    this.value$.next((event.target as HTMLInputElement).value);
  }

  private value$ = new Subject<string>();

  @Output()
  readonly debounceInput = this.value$.pipe(
    debounce(() => timer(this.debounceTime || 0)),
    distinctUntilChanged()
  );
}

您可以像这样在组件中使用它:

<input type="text" placeholder="Search for new result"
  (debounceInput)="onInput($event)" [debounceTime]="300">

以更加 rxjs 风格编写此指令的另一种方法是:

@Directive({
  selector: 'input[debounceInput]'
})
export class DebounceInputDirective {
  @Input()
  debounceTime: number = 0;

  constructor(private el: ElementRef<HTMLInputElement>) {}

  @Output()
  readonly debounceInput = fromEvent(this.el.nativeElement, 'input').pipe(
    debounce(() => timer(this.debounceTime)),
    map(() => this.el.nativeElement.value),
    distinctUntilChanged()
  );
}

使用指令(以及不相关的async管道)的好处是您不必担心拖延的 rxjs 订阅。 这些可能是潜在的内存泄漏。


可是等等! 还有更多。 您可以忘记所有这些事情,并使用 angular 回到打字稿的根源。 装饰者! 在你的方法上使用一个花哨的去抖动装饰器怎么样。 然后,您可以保留之前的所有内容,只需在方法上方添加@debounce(300)

@debounce(300)
constructNewGrid(event): void {
  // ...
}

什么? 真的吗? 这个 debounce 装饰器是什么样子的。 好吧,它可以像这样简单:

function debounce(debounceTime: number) {
  let timeout: number;

  return function (
    _target: any,
    _propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    const originalMethod: Function = descriptor.value;
    
    descriptor.value = (...args: any[]) => {
      window.clearTimeout(timeout);
      timeout = window.setTimeout(() => originalMethod(...args), debounceTime);
    };

    return descriptor;
  };
}

但这是未经测试的代码,但它是为了让您了解所有可能的内容:)

将 DOM 事件转换为 RxJS 流

将 DOM 事件转换为流的最简单方法是 RxJS 的fromEvent创建操作符。

您将获得对 DOM 元素的引用,而不是绑定(input)="function" 在这里,将您的 DOM 元素称为“searchInput”。 你可以称之为任何东西。

<input type="text" placeholder="Search for new results" #searchInput>

然后在你的 TS 中:

@ViewChild('searchInput') searchInput: ElementRef;
ngOnInit(){
    merge(
      fromEvent(searchInput, 'input'),
      fromEvent(searchInput, 'keydown.backslash')
    ).pipe(
      debounceTime(3000)
    ).subscribe(e => constructNewGrid(e));
}

这将创建两个流并将它们合并在一起。 第一个流是来自“输入”的事件,第二个流是来自“keydown.backslash”的事件。


角反应形式

这是一篇很好的介绍文章

Angular 为您提供了一个 FormControl,它公开了一个流,但也包含了很多额外的东西,你不会从简单地绑定一个 DOM 事件中获得这些东西。 它具有内置的输入验证和大量支持,可帮助您一次管理大量 FromControl(通过 FormGroups)。

在这种情况下,它看起来像:

<input type="text" placeholder="Search for new results" [formControl]="searchInputControl">

然后在你的 TS 中:

searchInputControl = new FormControl('');
ngOnInit(){
    this.searchInputControl.valueChanges.pipe(
      debounceTime(3000)
    ).subscribe(val => constructNewGrid(val));
}

只需制作更多主题!

RxJS 主题是将命令式代码桥接到函数流中的标准方法。 您现在拥有的是一个回调函数,该函数正在传递您希望将其转换为流以处理它们的值。 你可以用一个主题来做到这一点。

stream = new Subject();
function callback(value){
  this.stream.next(value);
}

现在您可以将这些值作为流处理。

ngOnInit(){
  this.stream.pipe(
    denounceTime(3000)
  )subscribe(val => process(val));
}

在这种情况下,当您的主题被 angular 生命周期事件破坏时,您对该主题的订阅不会被清除。 您需要取消订阅以避免内存泄漏。


内存问题?

在较新的浏览器中,您不必担心清理对 DOM 元素/ formControl订阅,因为一旦删除了 DOM 元素或formControl ,对该元素/控件的订阅也会从内存中删除。 但是,在较旧的浏览器中,这仍可能导致 DOM 元素订阅的内存泄漏。

因此,可以肯定的是,您可能希望包含一些取消订阅逻辑。 我经常看到有两种模式被使用。

第一个,您创建一个订阅对象。 订阅可以放在一起,以便对一个unsubscribe()的调用可以取消订阅多个订阅。 您可以通过将一个订阅“添加”到另一个订阅中来做到这一点。 在这个例子中我们没有这样做,因为只有一个流。

@ViewChild('searchInput') searchInput: ElementRef;
private subscriptions: Subscription;
ngOnInit(){
    subscriptions = merge(
      fromEvent(searchInput, 'input'),
      fromEvent(searchInput, 'keydown.backslash')
    ).pipe(
      debounceTime(3000)
    ).subscribe(e => constructNewGrid(e));
}
ngOnDestory(){
  subscriptions.unsubscribe();
}

使用第二个,您创建一个主题,其唯一的工作是杀死您创建的所有其他流。 然后每个长期存在的流都必须包含takeUntil()操作符。

@ViewChild('searchInput') searchInput: ElementRef;
private _destroy$ = new Subject();
ngOnInit(){
    merge(
      fromEvent(searchInput, 'input'),
      fromEvent(searchInput, 'keydown.backslash')
    ).pipe(
      debounceTime(3000),
      takeUntil(this._destroy$)
    ).subscribe(e => constructNewGrid(e));
}
ngOnDestory(){
  this._destroy$.next();
  this._destroy$.complete();
}

哪个更好? 那是品味问题。 我喜欢第二个,因为我在某处读到过(虽然我现在找不到它),组件生命周期事件可能会在将来的某个时候作为流公开。 如果是这样, takeUntil() 方法将变得更加简洁,因为您不再需要创建自定义主题来处理它。

暂无
暂无

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

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