这是Jerry 2020年的第80篇文章,也是汪子熙公众号总共第262篇原创文章。
系列目录
(3) HTML原生事件 VS SAP UI5 Semantic事件(本文)
(4) UI5控件元数据实现细节
(5) UI5控件的实例数据实现细节
(6) UI5控件数据绑定的实现原理
(7) UI5控件数据绑定的三种模式:One Way,Two Way和OneTime实现原理比较
(8) UI5控件ID的生成逻辑
(9) UI5控件的多语言(国际化,Internationalization,i18n)支持的实现原理
(10) XML视图里的button控件
(11) button控件和它背后的DOM元素
本文将讨论SAP UI5控件的事件处理,全文会围绕下图表现出的差异来阐述。
首先用一个简单的例子来回顾HTML原生事件处理原理。
有这样一个简单的HTML页面,里面使用了一个HTML原生button标签,通过onclick="copyText"注册了一个名为copyText的事件处理函数。
点击按钮之后,响应函数copyText将Field1的值拷贝到Field2去。这个通过onclick注册的事件处理函数,在Chrome开发者工具里可以直接查看。
除了onclick之外,调用浏览器原生的addEventListener方法也能给DOM元素注册事件。
在企业级web应用里,DOM树的结构通常都不简单。例如仅仅包含一个简单button控件的SAP UI5应用,页面渲染出来后也会自动生成5个div标签:
如果每个需要响应事件的控件,都使用onclick或者addEventListener给DOM元素注册一个事件处理函数,随着DOM事件处理函数数量的增加,web应用的性能会降低。因此SAP UI5引入了另一种所谓Semantic(语义)事件的概念,来完成UI5控件的事件注册和响应工作。
使用Jerry文章 一个用于SAP UI5学习的脚手架应用,没有任何后台API的依赖 提到的脚手架,开发一个只包含sap.ui.commons.button的UI5应用:
上图的Elements标签页里,显示的是SAP UI5应用渲染完毕后,生成的HTML原生代码。里包含的button标签的生成逻辑,我们已经在前一篇文章 深入学习SAP UI5框架代码系列之二:UI5 控件的渲染器 里介绍过了。
我们采用与前一个原生HTML button例子同样的操作方式,在Chrome开发者工具里检查UI5应用里该button的Event Listeners,却什么也没发现。
选中"Ancestors"前面的勾之后,一下子显示了很多条目出来:
展开条目中的click,发现SAP UI5把click事件注册在button标签的父节点,即id为content的div标签上了,如下图所示。
再看看UI5应用里sap.ui.commons.Button的事件注册代码:
这里并没有出现HTML原生事件click的身影,而将一个包含了属性名称press,值为JavaScript函数的JavaScript对象,作为输入参数,传入了UI5 Button的构造函数里:
用户点击这个按钮时,触发的应该是名称为click的事件,和我们在这里为press事件注册的处理函数有什么关系?
在UI5 button的实现源代码里能找到答案。切换到Chrome开发者工具的Sources标签页,快捷键Ctrl + O,输入button,选择第一个结果Button-dbg.js:
这里能看到,press作为button支持的事件,定义在Button-dbg.js里:
下面这段代码的含义是,当UI5 button有click事件发生时,如果其本身处于enabled并且是visible状态,则fire一个Press事件(this.firePress()):
因此,正是Button实现里的这个onclick函数,实现了从事件click映射到事件press的任务。
上图调试器里168行的this.firePress调用,最终如何成功地调用到UI5程序里针对press事件注册的处理函数的呢?
还记得这个系列的前一篇文章 深入学习SAP UI5框架代码系列之一:UI5 Module的懒加载机制 里介绍的一个知识点吗?
SAP UI5运行时为所有的Module维护了一个注册表,以键值对的数据结构存储了这些Module的信息,键的数据类型为string,值类型即window.eval()将加载好的JavaScript文件内容作为输入参数,执行后返回的JavaScript对象。
类似的原理,SAP UI5里每个控件都维护了一个键值对结构的事件注册表mEventRegistry, 键的数据类型string,存储事件名称,值类型为数组,里面存放了针对该事件,应用程序实现的响应函数。
下图展示的是我脚手架应用里的button控件的事件注册表,只包含一条记录,键为press,值为一个数组,里面唯一的元素即我在脚手架应用里实现的包含了alert调用的事件响应函数。
下图展示的逻辑是:
(1) SAP UI5框架从第237行的控件事件注册表里,根据事件名称press,取出存放其事件处理函数的数组;
(2) 遍历该数组,在for循环里用JavaScript function原型提供的call方法,对这些响应函数进行调用,完成事件响应:
至此又引出了一个新的问题:button控件的事件注册表mEventRegistry里的那唯一的条目,是何时填充进去的?
再回忆本系列第一篇文章里介绍的SAP UI5控件的原型链:
Button->Control->Element->ManagedObject->EventProvider->BaseObject.
UI5应用里这一行语句:
new sap.ui.commons.Button()
会依次执行控件原型链上每一个节点对应的构造函数。控件事件注册表mEventRegistry的填充操作,就发生在EventProvider这个节点的构造函数里:
上图的变量oValue,就是我new一个button实例时传入的press事件的处理函数。在第1192行代码里,调用attachPress将oValue指向的函数进行注册。函数attachPress最终调用EventProvider的attachEvent方法,将键值对写入mEventRegistry:
至此有最后一个问题还未解答:本文开头部分展示的Chrome开发者工具里,SAP UI5页面渲染后生成的button标签,在Event Listeners一栏里观察不到任何响应函数。而在其父节点,id为content的div标签里,在click事件下却能观察到响应函数。
Button父节点的div标签上的click方法,和本文讨论了这么长时间的button事件注册表里的press事件,到底有何关系?
按钮被点击时,查看调试器里显示的调用栈最外一层,发现SAP UI5的jquery-dbg.js, 响应的是HTML原生的click事件,且触发该事件的对象的的确确是id为content的div标签,而不是button标签,这一点可以从event.currentTarget的值来确认。
以上图调用栈中绿色的线为分隔,绿线下方的代码,处理的是HTML原生的点击事件click,同时完成了将click事件,经div投递给其子节点,button标签的任务。
绿线上方的Button.onclick, 前文我们已经阐述过,通过this.firePress将click事件映射成press事件,后续SAP UI5的所有事件处理,均围绕这个press事件进行。
按照SAP UI5开发团队大佬Andreas Kunz的介绍,button这种press事件称为Semantic事件。同HTML原生的click事件直接通过onclick或addEventListener注册在HTML DOM元素上不同,Semantic event的注册和调用都是通过SAP UI5框架的JavaScript代码施加在SAP UI5自行实现的控件上,比HTML原生的DOM事件处理和响应轻量得多,能避免随着DOM树复杂度的增加而造成的应用性能下降。
引入Semantic事件后,UI5控件不直接响应HTML原生事件,而是通过一个叫做UIArea的实体,来接收用户触发的HTML原生事件,并将其dispatch给UI5控件,后者再将其映射成一一对应的Semantic事件,并调用应用程序里实现的响应函数。这里的UIArea可以类比成设计模式里的Facade(外观)模式,对SAP UI5的应用开发人员屏蔽了底层事件映射的复杂度。
上图的UIArea的详细描述,在SAP UI5官方文档里有记载。
下图高亮的一段对UIArea的阐述,展开来讲就是Jerry本文的内容,大家感兴趣的可以移步这个链接继续阅读。
如果把本文提到的Semantic事件换个叫法,比如称其为虚拟事件,那么很容易联想到Angular,Vue和React里引入的Virtual DOM(虚拟DOM)概念。从本质上说,这些前端框架都采取增加框架实现复杂度的代价,引入一个中间抽象层,来减少直接在JavaScript层操作DOM层造成的性能开销。
顺便说一句,AngularJS里的控件注册实现,同SAP UI5思路一致:同样未采取将事件处理函数直接注册到HTML DOM元素上的机制。
下图是一个Angularjs应用,第22行的ng-click指令,告诉Angularjs框架,超链接被点击后,根据模型字段name,进行排序。
Angularjs框架如何解析这个ng-click指令,并完成事件注册的?
在Angularjs应用bootstrap阶段,框架会遍历HTML DOM tree,递归调用compileNodes方法,逐一解析每一个包含了ng指令的元素:
当解析到包含了ng-click = "sortField = 'name'"的a标签时,调用Angular元素element的on方法,进行事件注册:
查看on方法的实现代码可知:Angularjs也并未将事件响应函数注册到DOM元素上,而是同SAP UI5一样,在框架内维护了一个控件事件注册表,this.$$listeners(SAP UI5的名称叫做mEventRegistry),采用键值对的数据结构,来存储事件名称和其对应的事件响应函数。
Angularjs应用里,事件响应函数被调用时的调用栈截图:
关于SAP UI5和Angularjs的事件处理机制比较的更多细节,可以参考我的SAP社区博客:
Compare Event handling mechanism: SAPUI5 and Angular
本系列下一篇文章介绍的内容:UI5控件元数据实现细节。
感谢阅读。
系列目录
(3) HTML原生事件 VS SAP UI5 Semantic事件(本文)
(4) UI5控件元数据实现细节
(5) UI5控件的实例数据实现细节
(6) UI5控件数据绑定的实现原理
(7) UI5控件数据绑定的三种模式:One Way,Two Way和OneTime实现原理比较
(8) UI5控件ID的生成逻辑
(9) UI5控件的多语言(国际化,Internationalization,i18n)支持的实现原理
(10) XML视图里的button控件
(11) button控件和它背后的DOM元素
更多Jerry的原创文章,尽在:"汪子熙":