SAP UI5 OData 谣言粉碎机:极短时间内发送两个 Odata request, 前一个会自动被 cancel 掉吗
最近笔者在使用 Angular 进行 SAP Spartacus 前端开发时,遇到了和本文标题描述极为类似的场景。因为我学习新知识的时候,总喜欢把之前已经熟悉的知识拿来做横向类比,所以本文首先重温一个不少 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 方法:
filtersortrefresh
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 字段的每一次输入事件,触发的事件处理函数的执行,用一根竖线表示。则未经任何处理的原始场景,用函数节流和函数防抖重新实现的场景,三者比较的示意图如下:
从图中不难看出,应用了函数节流和防抖机制后,事件响应函数的触发频次大大降低。当事件响应函数本身包含了复杂耗时的业务逻辑时,触发频次的降低意味着避免了大量不必要的执行开销。
笔者后续的文章,会通过实际例子,来介绍 SAP UI5 如何实现函数节流和防抖,二者的区别,以及如何在 Angular 里用 RxJS 更优雅地实现这两种机制。感谢阅读。
版权声明: 本文为 InfoQ 作者【Jerry Wang】的原创文章。
原文链接:【http://xie.infoq.cn/article/ef49643c77dbc15e19971ba7e】。文章转载请联系作者。
评论