Angular 2+ 和去抖动

Posted

技术标签:

【中文标题】Angular 2+ 和去抖动【英文标题】:Angular 2+ and debounce 【发布时间】:2015-11-10 03:25:00 【问题描述】:

在 AngularJS 中,我可以使用 ng-model 选项来消除模型的抖动。

ng-model-options=" debounce: 1000 "

如何在 Angular 中消除模型的抖动? 我试图在文档中搜索 debounce,但找不到任何东西。

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

一个解决方案是编写我自己的 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);

还有我的html

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

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

【问题讨论】:

这可能是相关的github.com/angular/angular/issues/1773,显然还没有实现。 【参考方案1】:

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



【讨论】:

谢谢你的帮助,但是我认为导入应该来自rsjs/Rx,我在使用你编写的导入方式时出错了......所以在我的情况下它现在是:import Observable from 'rxjs/Rx'; @ghiscoding 取决于 rxjs 版本。在版本 6 中为:import Observable from 'rxjs';. 谢谢!顺便说一句,您可以只使用一个pipe 调用pipe(debounceTime(300), distinctUntilChanged()) searchChangeObserver 是一个订阅者,因此 searchChangeSubscriber 将是一个更好的名称。【参考方案2】:

由于该主题较旧,因此大多数答案Angular 6/7/8/9/10不起作用和/或使用其他库。 因此,这里有一个使用 RxJS 的 Angular 6+ 的简短解决方案。

先导入必要的东西:

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)' />

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

【讨论】:

@JustShadow 谢谢!这真的很有帮助。 这在第一次尝试时效果很好。但是当我以某种方式删除搜索到的文本时,下一个请求需要很长时间才能响应。 这很奇怪。它在我这边仍然可以正常工作。您能否分享更多信息或为此提出一个新问题?【参考方案3】:

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

import debounce from 'utils-decorators';

class MyAppComponent 

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

【讨论】:

【参考方案4】:

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

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

这会触发更改检测。 For a way that doesn't trigger change detection, check out Mark's answer.


更新

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

例子:

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

【讨论】:

我更喜欢这个解决方案!使用 angular 2.0.0,rxjs 5.0.0-beta 12 工作完美,简单明了,不涉及任何形式。我在 Angular 4.1.3,rxjs 5.1.1 我认为这是一个出色的解决方案,因为它可以在需要时使用表单,但消除了这种依赖性,从而使实现变得更加简单。谢谢。 .pipe(debounceTime(300), distinctUntilChanged()) 需要 rxjs 6 你认为我们需要在 OnDestroy 上取消订阅或做其他事情吗?【参考方案5】:

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

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

然后将这样的内容放入您的模板中:

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

【讨论】:

或只是 (input)="changed($event.target.value)" 感谢您用 lodash 回答 :) 我相信这仍然会在每次更改时触发 Angular 更改检测,无论去抖动如何。【参考方案6】:

Angular 7 中的 DebounceTime 与 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));
    

    

【讨论】:

感谢您提供的精彩博客链接!【参考方案7】:

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

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编译的。【参考方案8】:

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)
  

【讨论】:

【参考方案9】:

它可以作为指令来实现

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

【讨论】:

有更多的进口,这对我有用:import "rxjs/add/operator/debounceTime";导入“rxjs/add/operator/distinctUntilChanged”; 到目前为止,这使得它在应用程序范围内实现起来最简单 isFirstChange 用于在初始化时不发射 适用于 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() ) 在 Angular 9 和 rxjs 6.5.4 中工作,更改 @kumaheiyama 在他的评论中说明。只是不要忘记在您创建它的模块中导出指令。并且不要忘记将您正在创建此指令的模块包含到您正在使用它的模块中。【参考方案10】:

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

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

【讨论】:

对于 angular 8 prime ng 自动完成文本框完全适用。非常感谢。 很棒的答案..继续努力【参考方案11】:

为 RC.5 更新

使用 Angular 2,我们可以在表单控件的 valueChanges observable 上使用 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

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


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

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

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

【讨论】:

@Mark Rajcok 我认为您应该使用 [ngModel] 而不是 [value] ,因为 [value] 不会更新输入值。 是否有任何通用的去抖动方法(例如应用于窗口调整大小事件)? @MarkRajcok 我相信您在回答中描述的 CD 问题已由 github.com/angular/zone.js/pull/843 解决 我们什么时候需要取消订阅以防止内存泄漏? @slanden 是的,根据netbasal.com/when-to-unsubscribe-in-angular-d61c6b21bad3,我们应该取消订阅.fromEvent() 订阅【参考方案12】:

这是迄今为止我找到的最佳解决方案。在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);
        );
    

借自https://***.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

【讨论】:

【参考方案13】:

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

【讨论】:

Observable 的命名空间是什么?我的没有“fromEvent”方法【参考方案14】:

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

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

这使得去抖动属性或方法变得非常容易。该参数是去抖应该持续的毫秒数,在下面的示例中为 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);
    
  

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

【讨论】:

【参考方案15】:

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

Plunker

这方面的一个用例场景可能是在输入用户名后检查用户名是否已被使用,然后警告用户。

注意:不要忘记,(blur)="function(something.value) 可能对您更有意义,具体取决于您的需要。

【讨论】:

【参考方案16】:

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

<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

这里用于自动完成,但适用于所有场景。

【讨论】:

但是服务出现错误,这没有再次运行 我不明白这个例子。 [...] 是单向目标绑定。为什么可以通知容器valueChanges?它不应该是某事。喜欢(ngFormControl)="..."?

以上是关于Angular 2+ 和去抖动的主要内容,如果未能解决你的问题,请参考以下文章

Flutter TextField 值总是大写和去抖动

节流和去抖动功能

RxJS 中审计和去抖动的区别?

Angular2 @ TypeScript Observable 错误

Angular2 InputFormControl 和 ValueChange 连续触发

如何安装和导入 angular 2 '@angular/router'?