首页 文章

角度和去抖动

提问于
浏览
110

在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 回答

  • 12

    Updated for 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方式,但效率不高 . 每次击键和每次调整大小事件,即使它们被去抖动和限制,也会导致变化检测运行 . 换句话说, debouncing and throttling do not affect how often change detection runs . (我发现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() 被调用 .

  • 0

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

    不能像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

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

  • 27

    它可以作为指令实施

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

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

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

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

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

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

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

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

  • 0

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

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

    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>
    
  • 3

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

    Plunker

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

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

  • 1

    这是我迄今为止找到的最佳解决方案 . 更新 blur blurdebounce

    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

  • 1

    由于主题是旧的,大多数答案 don't workAngular 6 .
    因此,对于带有RxJS的Angular 6,这是一个简短而简单的解决方案 .

    首先导入必要的东西:

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

    ngOnInit 上初始化:

    export class MyComponent implements OnInit {
      notesText: string;
      notesModelChanged: Subject<string> = new Subject<string>();
    
      constructor() { }
    
      ngOnInit() {
        this.notesModelChanged
          .pipe(
            debounceTime(2000),
            distinctUntilChanged()
          )
          .subscribe(newText => {
            this.notesText = newText;
            console.log(newText);
          });
      }
    }
    

    使用这种方式:

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

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

  • 2

    对于Reactive Forms和Angular v2(最新)加v4下的处理,请查看:

    https://github.com/angular/angular/issues/6895#issuecomment-290892514

    希望很快就会对这些事情提供原生支持......

  • 26

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

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

    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)
      }
    
  • 8
    @Component({
        selector: 'input-debounce',
        template: '<input type="text" class="form-control" [placeholder]="placeholder" [(ngModel)]="inputValue">'
    })
    export class InputDebounceComponent {
        @Input() placeholder: string;
        @Input() delay: number = 300;
        @Output() value: EventEmitter = new EventEmitter();
    
        public inputValue: string;
    
        constructor(private elementRef: ElementRef) {
            const eventStream = Observable.fromEvent(elementRef.nativeElement, 'keyup')
                .map(() => this.inputValue)
                .debounceTime(this.delay)
                .distinctUntilChanged();
    
            eventStream.subscribe(input => this.value.emit(input));
        }
    }
    

相关问题