Javascript 角度和去抖动

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/32051273/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-23 07:33:41  来源:igfitidea点击:

Angular and debounce

javascriptangular

提问by koningdavid

In AngularJS I can debounce a model by using ng-model options.

在 AngularJS 中,我可以使用 ng-model 选项去抖动模型。

ng-model-options="{ debounce: 1000 }"

How can I debounce a model in Angular? I tried to search for debounce in the docs but I couldn't find anything.

如何在 Angular 中对模型进行去抖动?我试图在文档中搜索去抖动,但找不到任何东西。

https://angular.io/search/#stq=debounce&stp=1

https://angular.io/search/#stq=debounce&stp=1

A solution would be to write my own debounce function, for example:

一个解决方案是编写我自己的 debounce 函数,例如:

import {Component, Template, bootstrap} from 'angular2/angular2';

// Annotation section
@Component({
  selector: 'my-app'
})
@Template({
  url: 'app.html'
})
// Component controller
class MyAppComponent {
  constructor() {
    this.firstName = 'Name';
  }

  changed($event, el){
    console.log("changes", this.name, el.value);
    this.name = el.value;
  }

  firstNameChanged($event, first){
    if (this.timeoutId) window.clearTimeout(this.timeoutID);
    this.timeoutID = window.setTimeout(() => {
        this.firstName = first.value;
    }, 250)
  }

}
bootstrap(MyAppComponent);

And my html

还有我的 html

<input type=text [value]="firstName" #first (keyup)="firstNameChanged($event, first)">

But I'm looking for a build in function, is there one in Angular?

但我正在寻找一个内置函数,Angular 有吗?

回答by Mark Rajcok

Updated for RC.5

为 RC.5 更新

With Angular 2 we can debounce using RxJS operator debounceTime()on a form control's valueChangesobservable:

使用 Angular 2,我们可以debounceTime()在表单控件的valueChangesobservable上使用 RxJS 操作符去抖动:

import {Component}   from '@angular/core';
import {FormControl} from '@angular/forms';
import {Observable}  from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/throttleTime';
import 'rxjs/add/observable/fromEvent';

@Component({
  selector: 'my-app',
  template: `<input type=text [value]="firstName" [formControl]="firstNameControl">
    <br>{{firstName}}`
})
export class AppComponent {
  firstName        = 'Name';
  firstNameControl = new FormControl();
  formCtrlSub: Subscription;
  resizeSub:   Subscription;
  ngOnInit() {
    // debounce keystroke events
    this.formCtrlSub = this.firstNameControl.valueChanges
      .debounceTime(1000)
      .subscribe(newValue => this.firstName = newValue);
    // throttle resize events
    this.resizeSub = Observable.fromEvent(window, 'resize')
      .throttleTime(200)
      .subscribe(e => {
        console.log('resize event', e);
        this.firstName += '*';  // change something to show it worked
      });
  }
  ngDoCheck() { console.log('change detection'); }
  ngOnDestroy() {
    this.formCtrlSub.unsubscribe();
    this.resizeSub  .unsubscribe();
  }
} 

Plunker

Plunker

The code above also includes an example of how to throttle window resize events, as asked by @albanx in a comment below.

如@albanx 在下面的评论中所问的,上面的代码还包括如何限制窗口调整大小事件的示例。



Although the above code is probably the Angular-way of doing it, it is not efficient. Every keystroke and every resize event, even though they are debounced and throttled, results in change detection running. In other words, debouncing and throttling do not affect how often change detection runs. (I found a GitHub commentby Tobias Bosch that confirms this.) You can see this when you run the plunker and you see how many times ngDoCheck()is being called when you type into the input box or resize the window. (Use the blue "x" button to run the plunker in a separate window to see the resize events.)

虽然上面的代码可能是 Angular 的方式,但效率不高。每个击键和每个调整大小事件,即使它们被去抖动和限制,也会导致更改检测运行。换句话说,去抖动和节流不会影响更改检测运行的频率。(我发现Tobias Bosch的GitHub 评论证实了这一点。)您可以在运行 plunker 时看到这一点,并ngDoCheck()在您输入输入框或调整窗口大小时看到调用了多少次。(使用蓝色的“x”按钮在单独的窗口中运行 plunker 以查看调整大小事件。)

A more efficient technique is to create RxJS Observables yourself from the events, outside of Angular's "zone". This way, change detection is not called each time an event fires. Then, in your subscribe callback methods, manually trigger change detection – i.e., you control when change detection is called:

一种更有效的技术是从 Angular 的“区域”之外的事件中自己创建 RxJS Observables。这样,每次触发事件时都不会调用更改检测。然后,在您的订阅回调方法中,手动触发更改检测——即,您控制何时调用更改检测:

import {Component, NgZone, ChangeDetectorRef, ApplicationRef, 
        ViewChild, ElementRef} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/throttleTime';
import 'rxjs/add/observable/fromEvent';

@Component({
  selector: 'my-app',
  template: `<input #input type=text [value]="firstName">
    <br>{{firstName}}`
})
export class AppComponent {
  firstName = 'Name';
  keyupSub:  Subscription;
  resizeSub: Subscription;
  @ViewChild('input') inputElRef: ElementRef;
  constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef,
    private appref: ApplicationRef) {}
  ngAfterViewInit() {
    this.ngzone.runOutsideAngular( () => {
      this.keyupSub = Observable.fromEvent(this.inputElRef.nativeElement, 'keyup')
        .debounceTime(1000)
        .subscribe(keyboardEvent => {
          this.firstName = keyboardEvent.target.value;
          this.cdref.detectChanges();
        });
      this.resizeSub = Observable.fromEvent(window, 'resize')
        .throttleTime(200)
        .subscribe(e => {
          console.log('resize event', e);
          this.firstName += '*';  // change something to show it worked
          this.cdref.detectChanges();
        });
    });
  }
  ngDoCheck() { console.log('cd'); }
  ngOnDestroy() {
    this.keyupSub .unsubscribe();
    this.resizeSub.unsubscribe();
  }
} 

Plunker

Plunker

I use ngAfterViewInit()instead of ngOnInit()to ensure that inputElRefis defined.

我使用ngAfterViewInit()代替ngOnInit()来确保inputElRef已定义。

detectChanges()will run change detection on this component and its children. If you would rather run change detection from the root component (i.e., run a full change detection check) then use ApplicationRef.tick()instead. (I put a call to ApplicationRef.tick()in comments in the plunker.) Note that calling tick()will cause ngDoCheck()to be called.

detectChanges()将在此组件及其子组件上运行更改检测。如果您更愿意从根组件运行更改检测(即,运行完整的更改检测检查),请ApplicationRef.tick()改用。(我ApplicationRef.tick()在 plunker 的评论中调用了 。)请注意,调用tick()将导致ngDoCheck()被调用。

回答by 0xcaff

If you don't want to deal with @angular/forms, you can just use an RxJS Subjectwith change bindings.

如果您不想处理@angular/forms,您可以使用Subject带有更改绑定的 RxJS 。

view.component.html

视图.component.html

<input [ngModel]='model' (ngModelChange)='changed($event)' />

view.component.ts

view.component.ts

import { Subject } from 'rxjs/Subject';
import { Component }   from '@angular/core';
import 'rxjs/add/operator/debounceTime';

export class ViewComponent {
    model: string;
    modelChanged: Subject<string> = new Subject<string>();

    constructor() {
        this.modelChanged
            .debounceTime(300) // wait 300ms after the last event before emitting last event
            .distinctUntilChanged() // only emit if value is different from previous value
            .subscribe(model => this.model = model);
    }

    changed(text: string) {
        this.modelChanged.next(text);
    }
}

This does trigger change detection. For a way that doesn't trigger change detection, check out Mark's answer.

这确实会触发更改检测。对于不触发更改检测的方式,请查看 Mark 的答案。



Update

更新

.pipe(debounceTime(300), distinctUntilChanged())is needed for rxjs 6.

.pipe(debounceTime(300), distinctUntilChanged())需要 rxjs 6。

Example:

例子:

   constructor() {
        this.modelChanged.pipe(
            debounceTime(300), 
            distinctUntilChanged())
            .subscribe(model => this.model = model);
    }

回答by Oleg Polezky

It could be implemented as Directive

它可以作为指令实施

import { Directive, Input, Output, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { NgControl } from '@angular/forms';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
import { Subscription } from 'rxjs';

@Directive({
  selector: '[ngModel][onDebounce]',
})
export class DebounceDirective implements OnInit, OnDestroy {
  @Output()
  public onDebounce = new EventEmitter<any>();

  @Input('debounce')
  public debounceTime: number = 300;

  private isFirstChange: boolean = true;
  private subscription: Subscription;

  constructor(public model: NgControl) {
  }

  ngOnInit() {
    this.subscription =
      this.model.valueChanges
        .debounceTime(this.debounceTime)
        .distinctUntilChanged()
        .subscribe(modelValue => {
          if (this.isFirstChange) {
            this.isFirstChange = false;
          } else {
            this.onDebounce.emit(modelValue);
          }
        });
  }

  ngOnDestroy() {
    this.subscription.unsubscribe();
  }

}

use it like

像使用它一样

<input [(ngModel)]="value" (onDebounce)="doSomethingWhenModelIsChanged($event)">

component sample

组件样本

import { Component } from "@angular/core";

@Component({
  selector: 'app-sample',
  template: `
<input[(ngModel)]="value" (onDebounce)="doSomethingWhenModelIsChanged($event)">
<input[(ngModel)]="value" (onDebounce)="asyncDoSomethingWhenModelIsChanged($event)">
`
})
export class SampleComponent {
  value: string;

  doSomethingWhenModelIsChanged(value: string): void {
    console.log({ value });
  }

  async asyncDoSomethingWhenModelIsChanged(value: string): Promise<void> {
    return new Promise<void>(resolve => {
      setTimeout(() => {
        console.log('async', { value });
        resolve();
      }, 1000);
    });
  }
} 

回答by bertrandg

Not directly accessible like in angular1 but you can easily play with NgFormControl and RxJS observables:

不能像 angular1 那样直接访问,但您可以轻松地使用 NgFormControl 和 RxJS observables:

<input type="text" [ngFormControl]="term"/>

this.items = this.term.valueChanges
  .debounceTime(400)
  .distinctUntilChanged()
  .switchMap(term => this.wikipediaService.search(term));

This blog post explains it clearly: http://blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

这篇博文解释清楚:http: //blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

Here it is for an autocomplete but it works all scenarios.

这是一个自动完成,但它适用于所有场景。

回答by Just Shadow

Since the topic is old, most of the answers don't workon Angular 6/7/8/9and/or use other libs.
So here is a short and simple solution for Angular 6+ with RxJS.

由于该主题很旧,因此大多数答案不适用于Angular 6/7/8/9和/或使用其他库。
所以这是一个简短而简单的 Angular 6+ 和 RxJS 解决方案。

Import the necessary stuff first:

首先导入必要的东西:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

Initialize on ngOnInit:

初始化ngOnInit

export class MyComponent implements OnInit, OnDestroy {
  public notesText: string;
  private notesModelChanged: Subject<string> = new Subject<string>();
  private notesModelChangeSubscription: Subscription

  constructor() { }

  ngOnInit() {
    this.notesModelChangeSubscription = this.notesModelChanged
      .pipe(
        debounceTime(2000),
        distinctUntilChanged()
      )
      .subscribe(newText => {
        this.notesText = newText;
        console.log(newText);
      });
  }

  ngOnDestroy() {
    this.notesModelChangeSubscription.unsubscribe();
  }
}

Use this way:

使用这种方式:

<input [ngModel]='notesText' (ngModelChange)='notesModelChanged.next($event)' />

P.S.: For more complex and efficient solutions you might still want to check other answers.

PS:对于更复杂和有效的解决方案,您可能仍想查看其他答案。

回答by Matthias

You can createan RxJS (v.6) Observablethat does whatever you like.

你可以创建一个 RxJS (v.6) Observable来做任何你喜欢的事情。

view.component.html

视图.component.html

<input type="text" (input)="onSearchChange($event.target.value)" />

view.component.ts

view.component.ts

import { Observable } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';

export class ViewComponent {
    searchChangeObserver;

  onSearchChange(searchValue: string) {

    if (!this.searchChangeObserver) {
      Observable.create(observer => {
        this.searchChangeObserver = observer;
      }).pipe(debounceTime(300)) // wait 300ms after the last event before emitting last event
        .pipe(distinctUntilChanged()) // only emit if value is different from previous value
        .subscribe(console.log);
    }

    this.searchChangeObserver.next(searchValue);
  }  


}

回答by Brad C

For anyone using lodash, it is extremely easy to debounceany function:

对于任何使用 lodash 的人来说,去抖动任何函数都非常容易:

changed = _.debounce(function() {
    console.log("name changed!");
}, 400);

then just throw something like this into your template:

然后把这样的东西扔到你的模板中:

<(input)="changed($event.target.value)" />

回答by Serhii Vasko

Solution with initialization subscriber directly in event function:

直接在事件函数中初始化订阅者的解决方案:

import {Subject} from 'rxjs';
import {debounceTime, distinctUntilChanged} from 'rxjs/operators';

class MyAppComponent {
    searchTermChanged: Subject<string> = new Subject<string>();

    constructor() {
    }

    onFind(event: any) {
        if (this.searchTermChanged.observers.length === 0) {
            this.searchTermChanged.pipe(debounceTime(1000), distinctUntilChanged())
                .subscribe(term => {
                    // your code here
                    console.log(term);
                });
        }
        this.searchTermChanged.next(event);
    }
}

And html:

和 html:

<input type="text" (input)="onFind($event.target.value)">

回答by Fredrik_Macrobond

I solved this by writing a debounce decorator. The problem described could be solved by applying the @debounceAccessor to the property's set accessor.

我通过编写去抖动装饰器解决了这个问题。所描述的问题可以通过将 @debounceAccessor 应用于属性的 set 访问器来解决。

I've also supplied an additional debounce decorator for methods, which can be useful for other occasions.

我还为方法提供了一个额外的 debounce 装饰器,这对其他场合很有用。

This makes it very easy to debounce a property or a method. The parameter is the number of milliseconds the debounce should last, 100 ms in the example below.

这使得对属性或方法进行去抖动变得非常容易。该参数是去抖动应该持续的毫秒数,在下面的示例中为 100 毫秒。

@debounceAccessor(100)
set myProperty(value) {
  this._myProperty = value;
}


@debounceMethod(100)
myMethod (a, b, c) {
  let d = a + b + c;
  return d;
}

And here's the code for the decorators:

这是装饰器的代码:

function debounceMethod(ms: number, applyAfterDebounceDelay = false) {

  let timeoutId;

  return function (target: Object, propName: string, descriptor: TypedPropertyDescriptor<any>) {
    let originalMethod = descriptor.value;
    descriptor.value = function (...args: any[]) {
      if (timeoutId) return;
      timeoutId = window.setTimeout(() => {
        if (applyAfterDebounceDelay) {
          originalMethod.apply(this, args);
        }
        timeoutId = null;
      }, ms);

      if (!applyAfterDebounceDelay) {
        return originalMethod.apply(this, args);
      }
    }
  }
}

function debounceAccessor (ms: number) {

  let timeoutId;

  return function (target: Object, propName: string, descriptor: TypedPropertyDescriptor<any>) {
    let originalSetter = descriptor.set;
    descriptor.set = function (...args: any[]) {
      if (timeoutId) return;
      timeoutId = window.setTimeout(() => {
        timeoutId = null;
      }, ms);
      return originalSetter.apply(this, args);
    }
  }
}

I added an additional parameter for the method decorator which let's you trigger the method AFTER the debounce delay. I did that so I could for instance use it when coupled with mouseover or resize events, where I wanted the capturing to occur at the end of the event stream. In this case however, the method won't return a value.

我为方法装饰器添加了一个附加参数,让您在去抖动延迟后触发方法。我这样做是为了例如在与鼠标悬停或调整大小事件结合使用时使用它,我希望在事件流的末尾进行捕获。但是,在这种情况下,该方法不会返回值。

回答by BebbaPig

We can create a [debounce] directive which overwrites ngModel's default viewToModelUpdate function with an empty one.

我们可以创建一个 [debounce] 指令,它用一个空的函数覆盖 ngModel 的默认 viewToModelUpdate 函数。

Directive Code

指令代码

@Directive({ selector: '[debounce]' })
export class MyDebounce implements OnInit {
    @Input() delay: number = 300;

    constructor(private elementRef: ElementRef, private model: NgModel) {
    }

    ngOnInit(): void {
        const eventStream = Observable.fromEvent(this.elementRef.nativeElement, 'keyup')
            .map(() => {
                return this.model.value;
            })
            .debounceTime(this.delay);

        this.model.viewToModelUpdate = () => {};

        eventStream.subscribe(input => {
            this.model.viewModel = input;
            this.model.update.emit(input);
        });
    }
}

How to use it

如何使用它

<div class="ui input">
  <input debounce [delay]=500 [(ngModel)]="myData" type="text">
</div>