角度和去抖动

问题描述 投票:126回答:15

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

javascript angular
15个回答
180
投票

针对RC.5进行了更新

使用Angular 2,我们可以在表单控件的debounceTime() observable上使用RxJS运算符valueChanges进行去抖:

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以查看调整大小事件。)

一种更有效的技术是在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()。 (我在plunker的评论中调用了ApplicationRef.tick()。)注意调用tick()将导致调用ngDoCheck()


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>

1
投票

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

Plunker

一个用例场景可能是在输入用户名之后检查用户名,看看有人是否已经使用了用户名,然后警告用户。

注意:不要忘记,根据您的需要,(blur)="function(something.value)可能对您更有意义。


1
投票

Angular 7中的DebounceTime与RxJS v6

来源Link

但是z zxswい

Link

在HTML模板中

enter image description here

在组件中

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

0
投票

这是我迄今为止找到的最佳解决方案。更新 .... .... 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)}); } } on ngModelblur

debounce

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中:

https://stackoverflow.com/a/47823960/3955513

<input [(ngModel)]="hero.name" [debounce]="3000" (blur)="hero.name = $event.target.value" (ngModelChange)="onChange()" placeholder="name"> 上,模型使用普通的javascript进行显式更新。

示例:blur


0
投票

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

https://stackblitz.com/edit/ng2-debounce-working

和HTML:

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

114
投票

如果你不想处理@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需要.pipe(debounceTime(300), distinctUntilChanged()) 6。

例:

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

30
投票

它可以作为指令实施

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

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

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


17
投票

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


}

12
投票

由于主题是旧的,大多数答案不适用于Angular 6/7。 因此,对于带有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 {
  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)' />

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


11
投票

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

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

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

<input [ngModel]="firstName" (ngModelChange)="changed()" />

3
投票

我通过编写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);
    }
  }
}

我为方法装饰器添加了一个额外的参数,让你在去抖延迟之后触发方法。我这样做,所以我可以在使用鼠标悬停或调整大小事件时使用它,我想在事件流结束时进行捕获。但是,在这种情况下,该方法不会返回值。


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>
© www.soinside.com 2019 - 2024. All rights reserved.