SAP UI5 和 Angular 的函数防抖 (Debounce) 和函数节流 (Throttle) 实现原理介绍

笔者之前的文章 SAP UI5 OData谣言粉碎机:极短时间内发送两个Odata request, 前一个会自动被cancel掉吗,介绍过 SAP 成都研究院 CRM Fiori 开发团队开发过的一个 Live Search 的场景。
用户创建 Opportunity,维护 Account 字段,每输入一个字符,都会触发 SAP UI5 Input 控件的 liveChange 事件。在该事件的 onAccountInputFieldChanged 处理函数里,根据用户输入,发送 OData 请求到后台进行查询。

如果用户输入速度很快,则在短时间内,会有多个 OData 请求发送到后台,进而出现 Jerry 文章里描述的 OData 请求被 cancel 的情况。


最近 Jerry 做 SAP Spartacus 开发,遇到了同样的场景。因此通过本文把自己最近所学总结一下,记录下 SAP UI5 和 Angular 里如何使用函数防抖(Debounce)和函数节流(Throttle)来避免短时间内触发高频次函数调用的情况出现。
为了便于讲解,Jerry 做了一个只包含一个 Input 控件的 SAP UI5 页面。源代码地址.
在 Input 里输入字符,会触发 liveChange 事件,将当前 Input 的最新内容,发送到一个我自己开发的后台服务去。该后台服务什么也不做,只是简单将收到的内容返回给 UI.

这个 SAP UI5 页面里的 Input 控件的 liveChange 事件处理如下:

从 Chrome 控制台打印的输出来看,我在一秒钟之内,连续快速输入了 1234 共 4 个字符,一共产生了 4 个发送往后台的请求。

SAP UI5 如何使用函数防抖(Debounce)来降低函数调用的频次
函数防抖(Debounce),最早源于机械开关和继电器的术语“去弹跳”,即将多个信号合并为一个信号。
想象一个大家现实生活中都会遇到的场景:进电梯。电梯都有一个自动关闭门的超时时间,假设为 2 秒。当电梯检测到有人进入时,会重置这个 2 秒的计时器。如果下一个 2 秒之内,没有新的乘客进入电梯,电梯门才会自动关上。

电梯延迟关门这个场景,就是一个典型的函数防抖的现实例子。电梯关门的行为就是“函数”,通过电梯门的自动关闭超时时间,2 秒,来延迟电梯门的关闭动作的执行,从而降低电梯门的关闭频率,这就是“防抖”。
可以想象,如果电梯门的自动关闭没有设定超时时间,而是检测到没有人进出之后,立即关闭,这样会大大增加电梯门开合的频率,既浪费能源,也不安全。这就好比 Jerry 本文开头提到的例子:既然我短时间内输入了字符 1234,我期望在 UI 看到的,是后台服务接收到 1234 后返回的结果。至于后台如何对前三个请求,即字符 1,字符 12 和字符 123 进行处理,我不再关心。
我们可以仿照电梯门关闭超时时间的设定,来给 SAP UI5 的函数调用实现防抖控制。
下图 debounce 变量是一个函数构造器,本身是一个函数,接收另一个函数 fn 作为输入参数,职责是通过闭包,将 fn 改造成一个具有防抖控制功能的新函数,该新函数通过第 17 行的 return 语句返回。
防抖时间间隔通过函数构造器另一个输入参数 delay 指定。

假设我们指定的防抖时间间隔为 3000 毫秒即 3 秒,如果 3 秒之内,debounce 函数构造器返回的新函数被不断调用,此时执行上图代码第 19 行,调用 clearTimeout 重置计数器,此时原始函数 fn 不会得到执行。这个场景可以类比成:在电梯关门超时时间内,又有新的乘客进入,电梯超时计时器重置,电梯门不会关闭。
代码第 20 行,使用 setTimeout 重启超时时间间隔为 3 秒的计数器,3 秒过后,如果 JavaScript 任务队列里没有其他待执行任务,则执行原始函数 fn. 代码的第 20 行,好比电梯设备重新开启了 3 秒的超时定时器。
如果在等待的 3 秒之内,没有新的函数调用触发,则 3 秒过后,执行 21 行的原始函数 fn;这好比电梯在 3 秒之内,始终没有新的乘客进入,则 3 秒过后,电梯门自动关闭。
debounce 函数构造器的使用方式也很简单。
代码第 78 行,将原始的 sendRequest 函数,以及 3000 毫秒的防抖时间间隔,传入 debounce 构造器,返回一个兼有数据发送功能和防抖功能的 debounceVersion 函数。在第 85 行原来调用 sendRequest 函数的位置,改为调用 debounceVersion 函数即可。

函数防抖功能的测试:我在同一分钟的第 46 秒,48 秒,50 秒,51 秒四个时间点,分别输入了 1,2,3,4 总共 4 个字符,但是在最后一次即 51.996 秒又过了 3 秒之后,才仅仅有一个请求发送到后台:这说明 3 秒的函数防抖间隔生效了:


SAP UI5 如何使用函数节流(Throttle)来降低函数调用的频次
上述函数防抖的实现存在一个问题,还是以电梯的例子来说明。
设想有一个空间无限的电梯,关门的超时时间为 3 秒。如果不断的有新的乘客以小于 3 秒的时间间隔进入电梯,则电梯门永远没有机会关闭——即函数永远得不到执行。
函数节流(Throttle)是另一种降低函数调用频次的思路,同函数防抖的区别是,后者能保证在指定的节流间隔内,至少执行一次函数。
函数节流构造器的一个最简单的实现版本:
被节流器改造后的函数每次触发时,取一个当前系统时间戳,同前一次触发时取的时间戳比较。如果二者的时间差,大于等于构造器的输入参数 delay 即节流时间间隔,则进入第 39 行的 else 分支,触发原始函数 fn;否则说明节流时间间隔还未到达,使用第 34 行 setTimeout,将原始函数 fn,重新放入 JavaScript 事件队列内,延迟执行:

函数节流版本的构造器使用方式,同函数防抖版本的构造器没有差别:将原始函数 sendRequest 传入构造器 throttle,返回一个具有节流功能的新函数 throttleVersion,在 Input 控件 liveChange 事件处理函数里,调用 throttleVersion 这个新函数即可。

函数节流的测试结果:我设置的节流时间间隔为 3 秒,从 Chrome 控制台打印输出能观察到,SAP UI5 确实是大致以 3 秒的时间间隔,向后台发起的数据请求。

本文介绍的两种函数防抖和函数节流的实现代码,仅仅考虑了最基本的情况,还有很多不完善的地方,有兴趣的朋友可以在网络上搜索,这方面的资料非常多,这里不再赘述。
Jerry 之前的分享提到过,Angular 是响应式编程开发库 RxJS 的重度使用者,后者提供了众多功能强大的 Operators,使得 Angular 开发人员不用重复造轮子,就能轻易实现出具有函数防抖和函数节流的场景。

用 Angular 重新实现本文 SAP UI5 的 Demo,总共代码只有 44 行:

从 rxjs/operators 工具库中直接导出 debounceTime 和 throttleTime 这两个 operators:

类似 SAP UI5 Input 控件的 liveChange,Angular FormControl 的 valueChanges 也给应用开发人员提供了编写业务逻辑,响应用户输入的位置:后者的 valueChanges 数据类型是 Observable,应用开发人员可以通过 pipe 调用,传入 RxJS 各种功能强大的 Operators,让自己编写的包含业务逻辑的事件响应函数,按照实际需求来触发。

比如上图第 39 行代码,语义是:绑定到 jerryFormControl 的 input 控件有 valueChanges 发生时,首先经过防抖器的处理。至于是否能够满足触发 valueChanges 对应的事件处理函数的条件,由防抖器 debounceTime 的内部处理逻辑决定。
RxJS 防抖器 debounceTime 的内部实现使用了 setInterval,逻辑比 Jerry 本文介绍的 debounce 函数构造器复杂得多了,通过这些调用栈就能感受一二:

Jerry 这个 Angular Demo 的函数节流(时间间隔设定为 2 秒)功能测试如下:我在 7 秒之内,匀速输入 1234567890abc,可以看到总共触发了三个发送到后台的请求,请求间隔为 2 秒:


希望本文能帮助大家对函数防抖和函数节流的概念有一个最粗浅的理解,感谢阅读。
版权声明: 本文为 InfoQ 作者【Jerry Wang】的原创文章。
原文链接:【http://xie.infoq.cn/article/6d156cd48a7ef3413a61eda60】。文章转载请联系作者。
评论