Angular 2+ 和去抖动

Angular 2+ and debounce

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

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

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

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

一个解决方案是编写我自己的去抖功能,例如:

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);

还有我的html

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

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

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

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

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

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

此处用于自动完成,但适用于所有情况。

已更新 RC.5

使用 Angular 2 我们可以在表单控件的 valueChanges 可观察对象上使用 RxJS 运算符 debounceTime() 去抖:

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

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


虽然上面的代码可能是 Angular 的做法,但效率不高。每次击键和每次调整大小事件,即使它们被去抖动和限制,都会导致变化检测 运行ning。换句话说,去抖动和节流不会影响变化检测的频率运行s。 (我发现 Tobias Bosch 的 GitHub comment 证实了这一点。)当你 运行 plunker 时你可以看到这一点,你会看到当你输入输入时 ngDoCheck() 被调用了多少次框或调整 window 的大小。 (使用蓝色 "x" 按钮 运行 plunker 在单独的 window 中查看调整大小事件。)

一种更有效的技术是从 Angular 的 "zone" 之外的事件中自己创建 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

我使用 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() 代替。 (我在 plunker 的注释中调用了 ApplicationRef.tick()。)请注意,调用 tick() 将导致调用 ngDoCheck()

在这上面花了几个小时,希望我能为别人节省一些时间。对我来说,以下在控件上使用 debounce 的方法更直观,更容易理解。它建立在 angular.io 文档自动完成解决方案的基础上,但我能够拦截调用而不必依赖于将数据绑定到 DOM.

Plunker

此用例场景可能是在输入用户名后检查用户名是否已被使用,然后警告用户。

注意:别忘了,(blur)="function(something.value) 可能更适合您,具体取决于您的需要。

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

view.component.html

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

view.component.ts

import { Subject } from 'rxjs';
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);
    }
}

这会触发更改检测。


更新

rxjs 6 需要

.pipe(debounceTime(300), distinctUntilChanged())

示例:

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

简单的解决方案是创建一个可以应用于任何控件的指令。

import { Directive, ElementRef, Input, Renderer, HostListener, Output, EventEmitter } from '@angular/core';
import { NgControl } from '@angular/forms';

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

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

    private modelValue = null;

    constructor(public model: NgControl, el: ElementRef, renderer: Renderer){
    }

    ngOnInit(){
        this.modelValue = this.model.value;

        if (!this.modelValue){
            var firstChangeSubs = this.model.valueChanges.subscribe(v =>{
                this.modelValue = v;
                firstChangeSubs.unsubscribe()
            });
        }

        this.model.valueChanges
            .debounceTime(this.debounceTime)
            .distinctUntilChanged()
            .subscribe(mv => {
                if (this.modelValue != mv){
                    this.modelValue = mv;
                    this.onDebounce.emit(mv);
                }
            });
    }
}

用法为

<textarea [ngModel]="somevalue"   
          [debounce]="2000"
          (onDebounce)="somevalue = $event"                               
          rows="3">
</textarea>

它可以作为指令实施

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();
  }

}

像这样使用

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

组件示例

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);
    });
  }
} 

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

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

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

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


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

这是装饰器的代码:

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);
    }
  }
}

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

对于任何使用 lodash 的人来说,debounce 任何功能都非常容易:

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

然后将类似这样的内容添加到您的模板中:

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

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

view.component.html

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

view.component.ts

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

export class ViewComponent {
    searchChangeObserver;

  onSearchChange(searchValue: string) {

    if (!this.searchChangeObserver) {
      new Observable(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);
  }  


}

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

指令代码

@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);
        });
    }
}

如何使用

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

这是迄今为止我找到的最佳解决方案。在 blurdebounce

上更新 ngModel
import { Directive, Input, Output, EventEmitter,ElementRef } from '@angular/core';
import { NgControl, NgModel } from '@angular/forms';
import 'rxjs/add/operator/debounceTime'; 
import 'rxjs/add/operator/distinctUntilChanged';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/map';

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

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

    private isFirstChange: boolean = true;

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

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

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

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

借自

然后在HTML:

<input [(ngModel)]="hero.name" 
        [debounce]="3000" 
        (blur)="hero.name = $event.target.value"
        (ngModelChange)="onChange()"
        placeholder="name">

blur 上,模型使用普通 javascript 显式更新。

此处示例:https://stackblitz.com/edit/ng2-debounce-working

由于话题比较老,Angular6-13and/or 使用其他库。
因此,这是 Angular 6+ 和 RxJS 的简短解决方案。

先导入必要的东西:

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

实施 ngOnInitngOnDestroy:

export class MyComponent implements OnInit, OnDestroy {
  public notesText: string;
  public 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();
  }
}

这样使用:

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

P.S。对于更复杂和高效的解决方案,您可能仍需要查看其他答案。

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

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);
    }
}

和html:

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

HTML 文件:

<input [ngModel]="filterValue"
       (ngModelChange)="filterValue = $event ; search($event)"
        placeholder="Search..."/>

TS 文件:

timer = null;
time = 250;
  search(searchStr : string) : void {
    clearTimeout(this.timer);
    this.timer = setTimeout(()=>{
      console.log(searchStr);
    }, time)
  }

Angular 7 中的 DebounceTime with RxJS v6

来源Link

演示 Link

在HTML模板中

<input type="text" #movieSearchInput class="form-control"
            placeholder="Type any movie name" [(ngModel)]="searchTermModel" />

在组件中

    ....
    ....
    export class AppComponent implements OnInit {

    @ViewChild('movieSearchInput') movieSearchInput: ElementRef;
    apiResponse:any;
    isSearching:boolean;

        constructor(
        private httpClient: HttpClient
        ) {
        this.isSearching = false;
        this.apiResponse = [];
        }

    ngOnInit() {
        fromEvent(this.movieSearchInput.nativeElement, 'keyup').pipe(
        // get value
        map((event: any) => {
            return event.target.value;
        })
        // if character length greater then 2
        ,filter(res => res.length > 2)
        // Time in milliseconds between key events
        ,debounceTime(1000)        
        // If previous query is diffent from current   
        ,distinctUntilChanged()
        // subscription for response
        ).subscribe((text: string) => {
            this.isSearching = true;
            this.searchGetCall(text).subscribe((res)=>{
            console.log('res',res);
            this.isSearching = false;
            this.apiResponse = res;
            },(err)=>{
            this.isSearching = false;
            console.log('error',err);
            });
        });
    }

    searchGetCall(term: string) {
        if (term === '') {
        return of([]);
        }
        return this.httpClient.get('http://www.omdbapi.com/?s=' + term + '&apikey=' + APIKEY,{params: PARAMS.set('search', term)});
    }

    }

您也可以通过使用装饰器来解决这个问题,例如使用 utils-decorator lib (npm install utils-decorators) 中的 debounce 装饰器:

import {debounce} from 'utils-decorators';

class MyAppComponent {

  @debounce(500)
  firstNameChanged($event, first) {
   ...
  }
}