角度和去抖动

时间:2015-08-17 13:09:01

标签: javascript angular

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

16 个答案:

答案 0 :(得分:175)

针对RC.5进行了更新

使用Angular 2,我们可以在表单控件的debounceTime()可观察对象上使用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()被调用。

答案 1 :(得分:111)

如果您不想处理@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);
    }

答案 2 :(得分:29)

它可以作为指令实施

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

答案 3 :(得分: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

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

答案 4 :(得分:16)

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


}

答案 5 :(得分:11)

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

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

然后将这样的内容扔进你的模板:

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

答案 6 :(得分:9)

由于该主题较旧,大多数答案在 Angular 6/7 不起作用
因此,这是使用RxJS的Angular 6+的简短解决方案。

首先导入必要的内容:

assert json_response(conn, 200)["data"] == [
  %{
    ...
    "published_at" => NaiveDateTime.to_iso8601(item.published_at),
    ...
  }
]

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

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

答案 7 :(得分: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);
    }
  }
}

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

答案 8 :(得分: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>

答案 9 :(得分: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>

答案 10 :(得分:2)

HTML文件:

#STM32F4XX

SET(CMSIS_DEVICE_HEADERS stm32f4xx.h system_stm32f4xx.h)
SET(CMSIS_DEVICE_SOURCES system_stm32f4xx.c)
SET(CMSIS_STARTUP_SOURCE startup_stm32f407xx.s)


FIND_PATH(CMSIS_DEVICE_INCLUDE_DIR ${CMSIS_DEVICE_HEADERS}
    PATH_SUFFIXES include stm32${STM32_FAMILY_LOWER} cmsis
    HINTS ${CMAKE_CURRENT_SOURCE_DIR}/Include/
    CMAKE_FIND_ROOT_PATH_BOTH
)

include_directories(CMSIS_INCLUDE_DIRS)

FIND_FILE(CMSIS_${SRC_CLEAN}_FILE ${SRC}
    PATH_SUFFIXES src stm32${STM32_FAMILY_LOWER} cmsis
    HINTS ${CMAKE_CURRENT_SOURCE_DIR}/Source/Templates
    CMAKE_FIND_ROOT_PATH_BOTH
)
LIST(APPEND CMSIS_SOURCES ${CMSIS_${SRC_CLEAN}_FILE})

FIND_FILE(CMSIS_STARTUP_SOURCE_FILE ${CMSIS_STARTUP_SOURCE}
    PATH_SUFFIXES src stm32${STM32_FAMILY_LOWER} cmsis
    HINTS ${CMAKE_CURRENT_SOURCE_DIR}/Source/Templates/gcc/
    CMAKE_FIND_ROOT_PATH_BOTH
) 

LIST(APPEND CMSIS_SOURCES ${CMSIS_STARTUP_SOURCE_FILE})

set(PUSH_SOURCE_HEADER_COMBINE
    ${CMSIS_SOURCES}
    ${CMSIS_INCLUDE_DIRS}
    )

add_library(STATIC ${PUSH_SOURCE_HEADER_COMBINE})

TS文件:

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

答案 11 :(得分:2)

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

import {debounce} from 'utils-decorators';

class MyAppComponent {

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

答案 12 :(得分:1)

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

Plunker

用例方案可能是在输入用户名后检查用户是否已经接受用户名,然后警告用户。

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

答案 13 :(得分:1)

使用RxJS v6在Angular 7中的DebounceTime

来源Link

演示Link

enter image description here

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

    }

答案 14 :(得分:0)

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

借鉴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

答案 15 :(得分:0)

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

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