SAP UI5和Angular的函数防抖(Debounce)和函数节流(Throttle)实现原理介绍
这是Jerry 2021年的第 11 篇文章,也是汪子熙公众号总共第 282 篇原创文章。
Jerry之前的文章 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页面。源代码地址:
https://github.com/wangzixi-diablo/ui5-toolset/blob/main/walkthrough/livesearch.html
在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秒:
希望本文能帮助大家对函数防抖和函数节流的概念有一个最粗浅的理解,感谢阅读。
更多阅读
(2) SAP UI5 控件渲染机制
(3) HTML原生事件 VS SAP UI5 Semantic事件
(7) SAP UI5控件数据绑定的三种模式:One Way, Two Way和OneTime实现原理比较
(8) SAP UI5控件ID的生成逻辑
(9) SAP UI5控件的多语言(国际化,Internationalization,i18n)支持的实现原理
(10) XML视图里的button控件
(11) button控件和它背后的DOM元素
(12) SAP UI5 OData谣言粉碎机:极短时间内发送两个Odata request,前一个会自动被cancel掉吗