[英]How to wait for the last keystroke before executing a function in Angular?
我有以下输入:
<input type="text" placeholder="Search for new results" (input)="constructNewGrid($event)" (keydown.backslash)="constructNewGrid($event)">
和功能
constructNewGrid(e){
// I want to wait 300ms after the last keystroke before constructing the new grid
// If the passed time is <300ms just return without doing something
// else start constructing new grid
}
我不太确定如何建立这样的条件。 我应该如何解决这个问题? 我在 RxJS 中阅读了关于 debounceTime 的内容,这正是我想要的,但我没有在函数中使用 observable,所以:您将如何在函数中构建这样的条件?
Observables 似乎是要走的路,但好的旧setTimeout
也会让你走很长的路。 出于美观原因,让我们首先重命名您的输入处理程序:
反斜杠事件似乎有点双重,因为这也会触发(input)
<input type="text" placeholder="Search for new results"
(input)="onInput(input.value)" #input>
在您的组件中,您有两种选择来处理此输入,使用 observable 或不使用。 让我先展示给你看:
export class GridComponent {
private timeout?: number;
onInput(value: string): void {
window.clearTimeout(this.timeout);
this.timeout = window.setTimeout(() => this.constructNewGrid(value), 300);
}
constructNewGrid(value: string): void {
// expensive operations
}
}
这看起来很简单,对于您的用例来说可能就足够了。 但是人们一直在谈论的那些很酷的 rxjs 流呢? 看起来像这样:
export class GridComponent {
private search$ = new BehaviorSubject('');
private destroy$ = new Subject<void>();
ngOnInit(): void {
this.search$.pipe(
// debounce for 300ms
debounceTime(300),
// only emit if the value has actually changed
distinctUntilChanged(),
// unsubscribe when the provided observable emits (clean up)
takeUntil(this.destroy$)
).subscribe((search) => this.constructNewGrid(search));
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
onInput(value: string): void {
this.search$.next(value);
}
constructNewGrid(value: string): void {
// expensive operations
}
}
对于这样一个简单的事情,这看起来像是更多的代码,而且确实如此。 所以这取决于你。
然而,如果你觉得这种模式是你会更经常使用的东西,你也可以考虑编写一个指令,它看起来像这样:
@Directive({
selector: '[debounceInput]'
})
export class DebounceInputDirective {
@Input()
debounceTime: number = 0;
@HostListener('input', '[$event]')
onInput(event: UIEvent): void {
this.value$.next((event.target as HTMLInputElement).value);
}
private value$ = new Subject<string>();
@Output()
readonly debounceInput = this.value$.pipe(
debounce(() => timer(this.debounceTime || 0)),
distinctUntilChanged()
);
}
您可以像这样在组件中使用它:
<input type="text" placeholder="Search for new result"
(debounceInput)="onInput($event)" [debounceTime]="300">
以更加 rxjs 风格编写此指令的另一种方法是:
@Directive({
selector: 'input[debounceInput]'
})
export class DebounceInputDirective {
@Input()
debounceTime: number = 0;
constructor(private el: ElementRef<HTMLInputElement>) {}
@Output()
readonly debounceInput = fromEvent(this.el.nativeElement, 'input').pipe(
debounce(() => timer(this.debounceTime)),
map(() => this.el.nativeElement.value),
distinctUntilChanged()
);
}
使用指令(以及不相关的async
管道)的好处是您不必担心拖延的 rxjs 订阅。 这些可能是潜在的内存泄漏。
可是等等! 还有更多。 您可以忘记所有这些事情,并使用 angular 回到打字稿的根源。 装饰者! 在你的方法上使用一个花哨的去抖动装饰器怎么样。 然后,您可以保留之前的所有内容,只需在方法上方添加@debounce(300)
:
@debounce(300)
constructNewGrid(event): void {
// ...
}
什么? 真的吗? 这个 debounce 装饰器是什么样子的。 好吧,它可以像这样简单:
function debounce(debounceTime: number) {
let timeout: number;
return function (
_target: any,
_propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod: Function = descriptor.value;
descriptor.value = (...args: any[]) => {
window.clearTimeout(timeout);
timeout = window.setTimeout(() => originalMethod(...args), debounceTime);
};
return descriptor;
};
}
但这是未经测试的代码,但它是为了让您了解所有可能的内容:)
将 DOM 事件转换为流的最简单方法是 RxJS 的fromEvent
创建操作符。
您将获得对 DOM 元素的引用,而不是绑定(input)="function"
。 在这里,将您的 DOM 元素称为“searchInput”。 你可以称之为任何东西。
<input type="text" placeholder="Search for new results" #searchInput>
然后在你的 TS 中:
@ViewChild('searchInput') searchInput: ElementRef;
ngOnInit(){
merge(
fromEvent(searchInput, 'input'),
fromEvent(searchInput, 'keydown.backslash')
).pipe(
debounceTime(3000)
).subscribe(e => constructNewGrid(e));
}
这将创建两个流并将它们合并在一起。 第一个流是来自“输入”的事件,第二个流是来自“keydown.backslash”的事件。
Angular 为您提供了一个 FormControl,它公开了一个流,但也包含了很多额外的东西,你不会从简单地绑定一个 DOM 事件中获得这些东西。 它具有内置的输入验证和大量支持,可帮助您一次管理大量 FromControl(通过 FormGroups)。
在这种情况下,它看起来像:
<input type="text" placeholder="Search for new results" [formControl]="searchInputControl">
然后在你的 TS 中:
searchInputControl = new FormControl('');
ngOnInit(){
this.searchInputControl.valueChanges.pipe(
debounceTime(3000)
).subscribe(val => constructNewGrid(val));
}
RxJS 主题是将命令式代码桥接到函数流中的标准方法。 您现在拥有的是一个回调函数,该函数正在传递您希望将其转换为流以处理它们的值。 你可以用一个主题来做到这一点。
stream = new Subject();
function callback(value){
this.stream.next(value);
}
现在您可以将这些值作为流处理。
ngOnInit(){
this.stream.pipe(
denounceTime(3000)
)subscribe(val => process(val));
}
在这种情况下,当您的主题被 angular 生命周期事件破坏时,您对该主题的订阅不会被清除。 您需要取消订阅以避免内存泄漏。
在较新的浏览器中,您不必担心清理对 DOM 元素/ formControl
订阅,因为一旦删除了 DOM 元素或formControl
,对该元素/控件的订阅也会从内存中删除。 但是,在较旧的浏览器中,这仍可能导致 DOM 元素订阅的内存泄漏。
因此,可以肯定的是,您可能希望包含一些取消订阅逻辑。 我经常看到有两种模式被使用。
第一个,您创建一个订阅对象。 订阅可以放在一起,以便对一个unsubscribe()
的调用可以取消订阅多个订阅。 您可以通过将一个订阅“添加”到另一个订阅中来做到这一点。 在这个例子中我们没有这样做,因为只有一个流。
@ViewChild('searchInput') searchInput: ElementRef;
private subscriptions: Subscription;
ngOnInit(){
subscriptions = merge(
fromEvent(searchInput, 'input'),
fromEvent(searchInput, 'keydown.backslash')
).pipe(
debounceTime(3000)
).subscribe(e => constructNewGrid(e));
}
ngOnDestory(){
subscriptions.unsubscribe();
}
使用第二个,您创建一个主题,其唯一的工作是杀死您创建的所有其他流。 然后每个长期存在的流都必须包含takeUntil()
操作符。
@ViewChild('searchInput') searchInput: ElementRef;
private _destroy$ = new Subject();
ngOnInit(){
merge(
fromEvent(searchInput, 'input'),
fromEvent(searchInput, 'keydown.backslash')
).pipe(
debounceTime(3000),
takeUntil(this._destroy$)
).subscribe(e => constructNewGrid(e));
}
ngOnDestory(){
this._destroy$.next();
this._destroy$.complete();
}
哪个更好? 那是品味问题。 我喜欢第二个,因为我在某处读到过(虽然我现在找不到它),组件生命周期事件可能会在将来的某个时候作为流公开。 如果是这样, takeUntil() 方法将变得更加简洁,因为您不再需要创建自定义主题来处理它。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.