角度和去抖动

kon*_*vid 145 javascript angular

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

ng-model-options="{ debounce: 1000 }"
Run Code Online (Sandbox Code Playgroud)

如何在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);
Run Code Online (Sandbox Code Playgroud)

而我的HTML

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

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

Mar*_*cok 186

针对RC.5进行了更新

使用Angular 2,我们可以debounceTime()在窗体控件的valueChangesobservable 上使用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();
  }
} 
Run Code Online (Sandbox Code Playgroud)

Plunker

上面的代码还包含一个如何限制窗口调整大小事件的示例,如@albanx在下面的评论中所要求的那样.


虽然上面的代码可能是Angular方式,但效率不高.每次击键和每次调整大小事件,即使它们被去抖动和限制,也会导致变化检测运行.换句话说,去抖动和限制不会影响更改检测的运行频率.(我发现Tobias Bosch 的GitHub评论确认了这一点.)当你运行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();
  }
} 
Run Code Online (Sandbox Code Playgroud)

Plunker

我使用ngAfterViewInit()而不是ngOnInit()确保inputElRef定义.

detectChanges()将对此组件及其子组件运行更改检测.如果您希望从根组件运行更改检测(即,运行完整的更改检测检查),请ApplicationRef.tick()改为使用.(我ApplicationRef.tick()在plunker的评论中打了个电话.)注意调用tick()会导致ngDoCheck()被调用.

  • @Mark Rajcok我认为不是[value],而是使用[ngModel],因为[value]不会更新输入值. (2认同)
  • 我们何时需要取消订阅以防止内存泄漏? (2认同)

0xc*_*aff 128

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

view.component.html

<input [ngModel]='model' (ngModelChange)='changed($event)' />
Run Code Online (Sandbox Code Playgroud)

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这确实触发了变化检测.对于不触发变化检测的方法,请查看Mark的答案.


更新

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

例:

   constructor() {
        this.modelChanged.pipe(
            debounceTime(300), 
            distinctUntilChanged())
            .subscribe(model => this.model = model);
    }
Run Code Online (Sandbox Code Playgroud)

  • 我更喜欢这个解决方案 使用角度2.0.0,rxjs 5.0.0-beta 12 (5认同)
  • 工作完美,简单明了,不涉及任何形式.我在Angular 4.1.3,rxjs 5.1.1上 (2认同)
  • rxjs需要`.pipe(debounceTime(300),distinctUntilChanged())` (2认同)

Ole*_*zky 33

它可以作为指令实施

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

}
Run Code Online (Sandbox Code Playgroud)

用它就像

<input [(ngModel)]="value" (onDebounce)="doSomethingWhenModelIsChanged($event)">
Run Code Online (Sandbox Code Playgroud)

组件样本

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);
    });
  }
} 
Run Code Online (Sandbox Code Playgroud)

  • 适用于 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() )` (3认同)
  • 到目前为止,这使其成为实现应用程序范围内最简单的方法 (2认同)

ber*_*ndg 28

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

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

this.items = this.term.valueChanges
  .debounceTime(400)
  .distinctUntilChanged()
  .switchMap(term => this.wikipediaService.search(term));
Run Code Online (Sandbox Code Playgroud)

这篇博文清楚地解释了它:http: //blog.thoughtram.io/angular/2016/01/06/taking-advantage-of-observables-in-angular2.html

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


Jus*_*dow 23

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

首先导入必要的内容:

import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
Run Code Online (Sandbox Code Playgroud)

初始化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();
  }
}
Run Code Online (Sandbox Code Playgroud)

使用这种方式:

<input [ngModel]='notesText' (ngModelChange)='notesModelChanged.next($event)' />
Run Code Online (Sandbox Code Playgroud)

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

  • 为什么不在销毁时取消订阅? (2认同)
  • @JustShadow 谢谢!这真的很有帮助。 (2认同)

Mat*_*ias 18

您可以创建一个RxJS(v.6)Observable,它可以执行您喜欢的任何操作.

view.component.html

<input type="text" (input)="onSearchChange($event.target.value)" />
Run Code Online (Sandbox Code Playgroud)

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


}
Run Code Online (Sandbox Code Playgroud)

  • @ghiscoding这取决于rxjs版本.在版本6中它是:````import {Observable}来自'rxjs';```. (2认同)

Bra*_*d C 11

对于使用lodash人来说,这是非常容易任何功能:

changed = _.debounce(function() {
    console.log("name changed!");
}, 400);
Run Code Online (Sandbox Code Playgroud)

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

<(input)="changed($event.target.value)" />
Run Code Online (Sandbox Code Playgroud)

  • 或者只是(输入)="已更改($ event.target.value)" (3认同)

Ser*_*sko 8

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

和 html:

<input type="text" (input)="onFind($event.target.value)">
Run Code Online (Sandbox Code Playgroud)