角度和去抖动


160

在AngularJS中,我可以通过使用ng-model选项来反跳模型。

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

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

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中有一个吗?


3
这可能是相关的github.com/angular/angular/issues/1773,显然不是必需的。
Eric Martinez

使用RxJS v6freakyjolly.com/…来查看Angular 7的帖子
Code Spy

Answers:


202

针对RC.5更新

使用Angular 2,我们可以debounceTime()在窗体控件的valueChanges可观察对象上使用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

上面的代码还包括一个如何限制窗口调整大小事件的示例,如@albanx在下面的注释中所要求的。


虽然上面的代码可能是这样做的Angular方式,但效率不高。每次击键和每次调整大小事件(即使它们被去抖和抑制)都将导致更改检测运行。换句话说,去抖动和节流不影响更改检测运行的频率。(我发现Tobias Bosch 在GitHub上发表的评论对此进行了确认。)您可以在运行插入器时看到此消息,并ngDoCheck()在键入输入框或调整窗口大小时看到被调用了多少次。(使用蓝色的“ x”按钮在单独的窗口中运行导航按钮,以查看调整大小事件。)

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

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()将对此组件及其子组件运行更改检测。如果您希望从根组件运行更改检测(即运行完整的更改检测检查),请ApplicationRef.tick()改用。(我ApplicationRef.tick()在插件的注释中打了个电话。)请注意,tick()将导致ngDoCheck()被调用。


2
@Mark Rajcok我认为您应该使用[ngModel]而不是[value],因为[value]不会更新输入值。
米拉德(Milad)2016年

1
是否有任何通用的反跳方法(例如,应用于窗口调整大小事件)?
albanx

1
@MarkRajcok我相信你在你的答案中描述的CD问题被解决github.com/angular/zone.js/pull/843
Jefftopia

2
我们什么时候需要退订以防止内存泄漏?
slanden

1
@slanden是的,根据netbasal.com/when-to-unsubscribe-in-angular-d61c6b21bad3的规定,我们应取消.fromEvent()订阅
Jon Onstott

153

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

view.component.html

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

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

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


更新资料

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

例:

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

5
我更喜欢这种解决方案!使用angular 2.0.0,rxjs 5.0.0-beta 12
alsco77 '16

2
完美,简单,清晰地工作,不涉及任何形式。我使用的是Angular 4.1.3,rxjs
5

我认为这是一种出色的解决方案,因为它可以根据需要选择使用表单,但是消除了这种依赖关系,使实现变得更加简单。谢谢。
最多

2
.pipe(debounceTime(300), distinctUntilChanged())rxjs 6需要它
Icycool

解决方案救了我。我在中使用keyUp事件on input.nativeElementmat-table当更改列数时该事件停止了工作
igorepst 18'27 Dec'9

35

可以作为指令实施

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

1
有更多的导入,对我有用:import“ rxjs / add / operator / debounceTime”; 导入“ rxjs / add / operator / distinctUntilChanged”;
Sbl

2
到目前为止,这使得它最容易实现整个应用程序
joshcomley

1
isFirstChange用于不初始化时发出
Oleg Polezky

2
在Angular 8和rxjs 6.5.2中可以进行以下更改。:如果你想使用管道语法,更改以下import 'rxjs/add/operator/debounceTime'; import 'rxjs/add/operator/distinctUntilChanged';import { debounceTime, distinctUntilChanged } from 'rxjs/operators';this.model.valueChanges .debounceTime(this.debounceTime) .distinctUntilChanged()this.model.valueChanges .pipe( debounceTime(this.debounceTime), distinctUntilChanged() )
kumaheiyama

1
在Angular 9和rxjs 6.5.4中工作,并在其注释中声明了@kumaheiyama的更改。只是不要忘记将指令导出到创建它的模块中。并且不要忘记将正在创建此指令的模块包含在使用它的模块中。
Filip Savic

29

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

首先导入必要的内容:

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

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

使用这种方式:

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

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


1
您为什么不取消订阅破坏?
Virendra Singh Rathore

更新。感谢您的关注!
Just Shadow

1
@JustShadow谢谢!真的很有帮助。
Niral Munjariya

这在第一次尝试时完美。但是,当我以某种方式删除搜索到的文本时,下一个请求需要很长时间才能响应。
Sadiksha Gautam,

那很奇怪。在我这边仍然可以正常使用。您能否分享更多信息,或者为此打开一个新问题?
Just Shadow

28

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

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

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

该博客文章对其进行了清晰的解释:http : //blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

这里是自动完成功能,但它适用于所有情况。


但是服务出现错误,该错误不会再运行
Arun Tyagi

我不明白这个例子。[...] 是单向目标绑定。为什么可以通知容器valueChanges?应该不是…… 喜欢(ngFormControl)="..."吗?
phil294

20

您可以创建一个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) {
      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);
  }  


}

谢谢,这有所帮助,但是我认为导入应该来自rsjs/Rx,以您编写它的方式使用导入时出现了错误...所以就我而言,现在是:import { Observable } from 'rxjs/Rx';
ghiscoding

2
@ghiscoding取决于rxjs版本。在版本6中为:import { Observable } from 'rxjs';
Matthias

谢谢!顺便说一句,你可以只使用一个pipe电话pipe(debounceTime(300), distinctUntilChanged())
人。

1
searchChangeObserver是一个订阅服务器,因此searchChangeSubscriber将是一个更好的名称。
Khonsort

12

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

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

然后将这样的东西扔到您的模板中:

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

3
或者只是(input)=“ changed($ event.target.value)”
Jamie Kudla

1
感谢您用lodash回答:)
Vamsi

我相信,无论是否发生反跳,这仍然会触发每个更改的Angular变化检测。
AsGoodAsItGets

5

直接在事件函数中使用初始化订户的解决方案:

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

完全适用于有角度的8个补全自动完成文本框。非常感谢。
Jasmin Akther Suma

4

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

我还为方法提供了一个额外的防抖动装饰器,该装饰器在其他场合也很有用。

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

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

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


3

我们可以创建一个[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>

2

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

2

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

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>

此类远非于编译Angular 7
Stephane '18

1

花了几个小时,希望我可以节省一些时间。对我来说,以下使用方法debounce在控件来说更直观,更容易理解。它建立在angular.io docs解决方案的基础上,可实现自动完成,但我可以不必依赖于将数据绑定到DOM就可以拦截呼叫。

柱塞

一个用例是在输入用户名后检查用户名是否已经被使用,然后警告用户。

注意:别忘了,(blur)="function(something.value)根据您的需求,对您来说可能更有意义。


1

使用RxJS v6在Angular 7中进行DebounceTime

链接

演示链接

在此处输入图片说明

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

    }

1

您也可以使用装饰器解决此问题,例如,使用utils-decorator lib(npm install utils-decorators)中的去抖动装饰器解决一个问题:

import {debounce} from 'utils-decorators';

class MyAppComponent {

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

0

这是迄今为止我发现的最佳解决方案。更新ngModelblurdebounce

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

https://stackoverflow.com/a/47823960/3955513借来的

然后在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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.