这是Jerry 2021年的第 9 篇文章,也是汪子熙公众号总共第 280 篇原创文章。
本文Jerry原本写于2016年5月,当时发布于团队内部wiki.
五年过后,Jerry使用的开发框架,从SAP UI5变成了Angular.
最近Jerry在做SAP Spartacus开发时,遇到了和本文描述极为类似的场景。因为我学习新知识的时候,总喜欢把之前已经熟悉的知识拿来做横向类比,所以本文首先重温一个不少SAP UI5开发人员都理解得似是而非的知识点,为后续的分享做一个铺垫。
本公众号后续的文章,会介绍如何在Angular技术栈里,使用RxJS优雅地解决此类问题。
RxJS是Jerry之前的文章 Jerry在2020 SAP全球技术大会的分享:SAP Spartacus技术介绍的文字版 曾经提到的,一个Angular重度依赖的基于Observables的响应式编程库。
本文余下的部分,我们重新回到SAP UI5的世界。
My Opportunities是SAP成都研究院CRM Fiori开发团队于2014年到2016年之间,负责的CRM Fiori应用之一。用户在创建Opportunity时,需要指定Account字段,该字段支持Live Search功能,比如敲入一个字符"J", UI5会发送一个OData请求到后台,后者异步返回Account模型里fullname字段包含J的那些数据,作为搜索结果,通过下拉列表的方式显示在UI上:
OData请求url:
/sap/opu/odata/sap/CRM_OPPORTUNITY/AccountCollection?$top=10&$filter=substringof(%27J%27,fullName)&sap-client=001&$expand=MainAddress&$select=accountID,MainAddress/city,MainAddress/country,fullName
以此类推,如果在字符J后面再敲一个e,会触发一个新的OData请求,根据"Je"搜索:
/sap/opu/odata/sap/CRM_OPPORTUNITY/AccountCollection?$top=10&$filter=substringof(%27Je%27,fullName)&sap-client=001&$expand=MainAddress&$select=accountID,MainAddress/city,MainAddress/country,fullName
当时组内有同事观察到一个现象:如果用户快速输入一连串字符,则在Chrome开发者工具Network标签页里,从时间顺序上来说,先触发的OData请求,状态会被标注为canceled:
基于这个观察结果,有同事做出了这样的猜测:
极短时间内发送两个OData请求,则第一个会自动被cancel掉。
这个猜测即便纯粹从字面意义上讲,也有两点值得推敲之处:
(1) “极短时间”,多短算极短?1毫秒?1微秒?
(2) OData请求被谁cancel掉?UI5框架还是浏览器?
为了验证这个猜测是否正确,我写了一段简单的测试代码:在一个for循环里使用SAP UI5 OData Model read API,发出10个OData请求:
测试发现,无论是同步还是异步请求,都未出现被cancel的情况。
10个同步请求的执行情况如下图所示:同Timeline序列栏可以看到,10个同步请求按时间顺序,被后台处理,再依次收到响应。
10个异步请求的执行情况:10个请求几乎同时发出,同时收到响应。
测试结果表明,这个猜测“极短时间内发送两个OData请求,则第一个会自动被cancel掉”不成立。
但是我们在Chrome开发者工具里,确实观察到有OData请求被cancel,这又如何解释呢?
首先使用Chrome开发者工具network标签页里的Initiator功能,找到这些被cancel的OData请求,是下图第523行的refresh方法触发的:
在refresh方法内,如果第600行的标志位bChangeDetected为true,那么执行第601行的abortPendingRequest:
所以,这是一个“相煎何太急”的场景:在使用SAP UI5 OData Model API发送OData请求时,如果满足条件(bChangeDetected = true),则OData API会终止(abort)前一个pending的请求。
abortPendingRequest的注释写道:如果开发人员能确信,当前请求的响应数据不再需要,则可调用该方法来中止该请求。回到本文开头介绍的Opportunity Live Search的例子,当用户输入J,然后再输入e时,显然,前一个根据J进行搜索的请求,已经不再需要了,我们仅仅需要将Je对应的请求发送到后台即可。这就是abortPendingRequest方法调用的使用场景之一。
总共有多少种情况,会触发SAP UI5去调用abortPendingRequest方法?
根据关键字abortPendingRequest搜索,得到下列三处位置,即OData Model的三个API方法:
filter
sort
refresh
Jerry之前SAP CRM开发团队的同事Ben(文章 SAP成都研究院李三郎:SCP Application Router简介 的作者),对这三个API做了进一步的研究。
在SAP UI5的OData框架的ODataModel.js中,维护了一个HTTP请求的pending列表,其内维护了已经发送,但是还没有收到响应的request对象:
SAP UI5每次发起OData请求时,都会调用ODataModel的_request()方法。该方法会把当前的request对象加到pending列表中,并通过一个wrap method包装回调函数,确保在响应返回时,首先把缓存的request对象从pending列表中拿掉:
每次使用OData Model API发起filter, sort和refresh操作时,SAP UI5都会检查pending列表中是否存在pending的request对象。若存在,则先abort掉它,这就是我们在Chrome开发者工具里观察到的状态为canceled的HTTP请求。
总结
只有同时满足下列三个条件,我们才能观察到SAP UI5发出的OData请求被cancel的情况。
(1) 同一个OData Model实例发出的连续请求,因为pending列表是维护在this级别上的。
(2) 某个请求发送时,存在前一个状态还处于pending的HTTP请求。
(3) SAP UI5应用程序通过OData Model API发起filter, sort或者refresh操作。
这也印证了本文开始Jerry在for循环里,连续调用OData Model的read API发送请求时,没有观察到出现cancel的情况,因为不满足上述条件3.
当然,大前端发展到今天,已经有各种完善的理念和方案来避免此类问题。比如函数节流(throttle)和防抖(debounce)理念:
假设我们把用户在Account字段的每一次输入事件,触发的事件处理函数的执行,用一根竖线表示。则未经任何处理的原始场景,用函数节流和函数防抖重新实现的场景,三者比较的示意图如下:
从图中不难看出,应用了函数节流和防抖机制后,事件响应函数的触发频次大大降低。当事件响应函数本身包含了复杂耗时的业务逻辑时,触发频次的降低意味着避免了大量不必要的执行开销。
Jerry后续的文章,会通过实际例子,来介绍SAP UI5如何实现函数节流和防抖,二者的区别,以及如何在Angular里用RxJS更优雅地实现这两种机制。感谢阅读。
更多阅读
(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元素
更多Jerry的原创文章,尽在:"汪子熙":