Angular 2+和debounce

在AngularJS中,我可以使用ng-model选项来撤销一个模型。

ng-model-options="{ debounce: 1000 }"
如何在Angular中释放一个模型?< br / > 我试着在文档中搜索debounce,但我什么也找不到

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

我的html

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

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

256447 次浏览

不像在angar1中那样直接访问,但你可以轻松地使用NgFormControl和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 < / p >

这里是自动补全,但它适用于所有场景。

RC.5更新

在Angular 2中,我们可以在表单控件的valueChanges可观察对象上使用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();
}
}

< a href = " http://plnkr.co/edit/A8Ms99r7sazUeZS90z7K?p =预览noreferrer“rel = >恰好< / >

上面的代码还包括一个如何限制窗口大小调整事件的示例,就像@albanx在下面的评论中所问的那样。


虽然上面的代码可能是angular的方式,但它不是有效的。每一次击键和每一次调整大小事件,即使它们被取消和抑制,也会导致更改检测运行。换句话说,退出和节流不影响变更检测运行的频率。(我发现Tobias Bosch的GitHub的评论证实了这一点。)当你运行plunker时,你可以看到这一点,当你在输入框中输入或调整窗口大小时,你可以看到ngDoCheck()被调用了多少次。(使用蓝色的“x”按钮在一个单独的窗口中运行活塞,以查看调整大小事件。)

一个更有效的方法是你自己从事件中创建RxJS的可观察对象,在Angular的“区域”之外。这样,就不会在每次触发事件时调用更改检测。然后,在你的订阅回调方法中,手动触发更改检测–也就是说,你控制什么时候调用变更检测:

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

< a href = " http://plnkr.co/edit/E77SzTXvBnaBL4SzyOgh?p =预览noreferrer“rel = >恰好< / >

我使用ngAfterViewInit()代替ngOnInit()来确保定义了inputElRef

detectChanges()将在该组件及其子组件上运行变更检测。如果你更愿意从根组件运行变更检测(即,运行一个完整的变更检测检查),那么使用ApplicationRef.tick()代替。(我在plunker的注释中调用ApplicationRef.tick()。)注意,调用tick()将导致ngDoCheck()被调用。

花了好几个小时在这上面,希望我能帮别人省点时间。对我来说,下面在控件上使用debounce的方法对我来说更直观,更容易理解。它是建立在角上的。io文档解决方案的自动完成,但有能力为我拦截调用,而不必依赖于绑定数据到DOM。

< a href = " https://plnkr。有限公司/ eSHZE7 rel =“nofollow”>恰好< / >

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

注意:不要忘记,(blur)="function(something.value)可能对你更有意义,这取决于你的需要。

如果你不想处理@angular/forms,你可以只使用RxJS的Subject与更改绑定。

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

这确实触发了变更检测。关于不触发变更检测的方法,请参阅Mark的回答。


更新

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

例子:

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

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

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>

它可以作为指令来执行

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

我通过编写一个debounce decorator来解决这个问题。所描述的问题可以通过将@debounceAccessor应用于属性的集访问器来解决。

我还为方法提供了一个额外的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);
}
}
}

我为方法装饰器添加了一个额外的参数,让你在debounce延迟后触发方法。我这样做是为了在与鼠标悬停或调整事件相结合时使用它,我希望捕获发生在事件流的末尾。但是,在这种情况下,该方法不会返回值。

对于任何使用lodash的人来说,防反跳任何函数都是非常容易的:

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

然后在模板中添加如下内容:

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

你可以创建一个RxJS (v.6) 可观测的,它可以做任何你喜欢的事情。

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




}

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

这是我迄今为止找到的最好的解决办法。更新blurdebounce上的__abc0

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

由于主题是旧的,大部分答案不工作角6-13和/或使用其他库。
因此,这里有一个使用RxJS的Angular 6+的简短而简单的解决方案

首先导入必要的东西:

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

实现ngOnInitngOnDestroy:

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

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

Angular 7和RxJS v6中的DebounceTime

来源链接 . cn

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


}

你也可以通过使用装饰器来解决这个问题,对于一个实例,使用来自utils-decorator lib (npm install utils-decorators)的debounce装饰器:

import {debounce} from 'utils-decorators';


class MyAppComponent {


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