جاوااسکریپت - انگولار ۲+ و دیبونس


در AngularJS توانستم با استفاده از گزینههای ng-model، یک مدل را تأخیر دهم.

ng-model-options="{ debounce: 1000 }"

چطور میتوانم یک مدل را در انگولار دیبانس کنم؟
من سعی کردم در مستندات به دنبال دیبانس بگردم اما چیزی پیدا نکردم.

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

و اچتیامال من

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

اما من به دنبال یک تابع از پیش ساخته هستم، آیا در انگولار چنین تابعی وجود دارد؟

پاسخ شماره ۱

بهروز شده برای RC.5

با Angular 2 میتوانیم با استفاده از عملگر RxJS debounceTime() روی مشاهدهپذیر valueChanges یک کنترل فرم، debounce انجام دهیم:

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

پلانکر

کد بالا همچنین شامل مثالی از چگونگی محدود کردن رویدادهای تغییر اندازه پنجره است، همانطور که @albanx در دیدگاهی در زیر خواسته بود.

اگرچه کد بالا احتمالاً روش انگولار برای انجام این کار است، اما کارآمد نیست. هر فشردن کلید و هر رویداد تغییر اندازه، حتی اگر با دیباونس و ثروتل کنترل شده باشند، منجر به اجرای تشخیص تغییر میشود. به عبارت دیگر، دیباونس و ثروتل تأثیری بر تعداد دفعات اجرای تشخیص تغییر ندارند. (من یک نظر در گیتهاب از طرف Tobias Bosch پیدا کردم که این موضوع را تأیید میکند.) شما میتوانید این را زمانی که پلاکر را اجرا میکنید ببینید و متوجه شوید چند بار ngDoCheck() فراخوانی میشود وقتی که در جعبه ورودی تایپ میکنید یا اندازه پنجره را تغییر میدهید. (برای دیدن رویدادهای تغییر اندازه، از دکمه آبی "x" استفاده کنید تا پلاکر را در پنجرهای جداگانه اجرا کنید.)

یک تکنیک کارآمدتر این است که خودتان از رویدادها Observables در RxJS ایجاد کنید، خارج از «زون» انگولار. به این ترتیب، تشخیص تغییر هر بار که یک رویداد رخ میدهد فراخوانی نمیشود. سپس، در متدهای callback مربوط به subscribe خود، بهصورت دستی تشخیص تغییر را فعال کنید - یعنی شما کنترل میکنید که چه زمانی تشخیص تغییر فراخوانی شود:

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

پلانکر

من از ngAfterViewInit() به جای ngOnInit() استفاده میکنم تا مطمئن شوم که inputElRef تعریف شده است.

detectChanges() تشخیص تغییرات را روی این کامپوننت و فرزندان آن اجرا میکند. اگر ترجیح میدهید تشخیص تغییرات را از کامپوننت ریشه اجرا کنید (یعنی یک بررسی کامل از تشخیص تغییرات انجام دهید)، از ApplicationRef.tick() استفاده کنید. (من یک فراخوانی به ApplicationRef.tick() را در کامنتها در plunker قرار دادهام.) توجه داشته باشید که فراخوانی tick() باعث میشود ngDoCheck() فراخوانی شود.

پاسخ شماره ۲

اگر نمیخواهید با @angular/forms سروکار داشته باشید، میتوانید فقط از یک Subject در RxJS با اتصالهای تغییر استفاده کنید.

view.component.html

<input [ngModel]='model' (ngModelChange)='changed($event)' />

view.component.ts

import { Subject } from 'rxjs';
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);
    }
}

این واقعاً شناسایی تغییر را فعال میکند. برای روشی که شناسایی تغییر را فعال نکند، پاسخ مارک را بررسی کنید.


بهروزرسانی

.pipe(debounceTime(300), distinctUntilChanged()) برای rxjs 6 لازم است.

مثال :

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

پاسخ شماره ۳

از آنجایی که این موضوع قدیمی است، بیشتر پاسخها در Angular 6-13 کار نمیکنند و/یا از کتابخانههای دیگر استفاده میکنند.
بنابراین این یک راه حل کوتاه و ساده برای Angular 6+ با RxJS است.

ابتدا موارد ضروری را وارد کنید:

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

متدهای ngOnInit و ngOnDestroy را پیادهسازی کنید:

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

ضمناً، برای راهحلهای پیچیدهتر و کارآمدتر ممکن است بخواهید پاسخهای دیگر را نیز بررسی کنید.

پاسخ شماره ۴

این میتواند بهعنوان یک دستورالعمل پیادهسازی شود

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

پاسخ شماره ۵

مانند Angular1 به طور مستقیم قابل دسترسی نیست، اما میتوانید به راحتی با NgFormControl و Observable های 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

این برای تکمیل خودکار است اما در همه سناریوها کار میکند.

پاسخ شماره ۶

شما میتوانید یک Observable در RxJS (نسخه ۶) بسازید که هر کاری که دوست دارید انجام دهد.

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


}

پاسخ شماره ۷

برای هر کسی که از lodash استفاده میکند، بسیار آسان است که هر تابعی را debounce کند:

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

سپس فقط چیزی شبیه به این را در قالب خود قرار دهید:

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