Qomolangma实现篇(四):基本特性增强与多投事件系统
================================================================================
Qomolangma OpenProject v1.0
类别 :Rich Web Client
关键词 :JS OOP,JS Framwork, Rich Web Client,RIA,Web Component,
DOM,DTHML,CSS,JavaScript,JScript
项目发起:aimingoo (aim@263.net)
项目团队:aimingoo, leon(pfzhou@gmail.com)
有贡献者:JingYu(zjy@cnpack.org)
================================================================================
一、Qomolangma对JS基本特性的增强
~~~~~~~~~~~~~~~~~~
为了实现更为丰富的OOP特性,Qomo增强了JavaScript的一些基础特性。这主要表现在:
- 对JS基本类型系统(的方法)的增强
- 支持多投事件
这其中,对基本类型系统的增强,将严格恪守一条原则:不修改Object()对象原型。
除了array.indexOf()、array.remove()、string.trim() 等常见的增强之外,Qomo有
几项特性是与其它可能(可能)不一致的。这几项内容随后列一专题来讲述:
- Array.prototype.insert
- String.prototype.format
- Function.prototype.toString
此外,因为Qomo以后将提供与Altas相同的、基于vs.net的可视编辑特性,因此一些基
本的特性扩展参考或者拷贝了Altas的代码。但这些代码目前只是留在了JSEnhance.js
中而未被启用。你可以不关注它们。
在Mozilla系列的浏览器环境中,提供了一个uneval()函数,这个函数用于序列化脚本
对象,在今后的开发中很有价值。但它被放在了Compat/common_ie6.js中。这里也只提
及它,而不分析它的实现。
二、JSEnhance.js中部分增强特性
~~~~~~~~~~~~~~~~~~
首先,请记住JSEnhance.js最主要的特性是“它可以脱离Qomo framework使用”。这个
单元不依赖于Qomo的任何特性。它使用自然的、原始的JavaScript方法来扩展JS特性。
因此它可以用于任何的Framework。
Array.prototype.insert
----------
Qomo中为array.insert()提供了更强大的能力,使得它可以向任意位置插入数组、单
个或多个元素。这与一些其它的框架不同:它们通常只提供插入单个元素的能力。
String.prototype.format
----------
Qomo中的string.format()是参考Delphi实现的。因此你会到匹配符是“%s”和“%n”。
这里的“s(大小写均可)”用于指代一个被替换元,而“n(0..n)”用于指代第n个替换元。
由于在JS中没有明确的类型,因此没有"%d"之类的匹配符。
作为习惯,我提供了一个全局的函数:format()。
关于string.format()的使用,参见DOCUMENTs/TestCase/T_StringFormat.html。
Function.prototype.toString
----------
在JavaScript中,匿名函数(立即值)声明、函数对象构造、函数的标准语法声明等都
可以声明一个有效的函数。但这些函数的toString()并不一致。为了解决对函数名的依
赖性问题,并使得下面的语法总有确定的含义:
function func() { /* ... */ };
foo = eval(func.toString());
Qomo复写了function.toString()。使得它总是返回一个匿名函数的字符串。如(上例):
function () { /* ... */ };
三、JSEnhance.js中的多投事件系统
~~~~~~~~~~~~~~~~~~
首先,最重要的一点是:Qomo的多投事件系统对任何框架来说,是“完全透明”的!因
此,它可以在其它任何框架中,象一个普通的事件函数(响应句柄)一样地加入被植入。
事实上,Qomo的多投事件与Qomo OOP框架完全地脱离开,不利用任何的OOP特性、框架特
性。——这种设计思路完整地体现了Qomo的目标与宗旨,以及,我们对OOP的认知。
下面的代码展示Qomo中的多投事件系统的特性:
----------
e = new MuEvent();
document.writeln(typeof e, '<BR>');
for (i in e)
document.writeln(' - ', i, '<BR>');
----------
输出结果:
----------
function
- add
- addMethod
- clear
- reset
- close
----------
这表明“多投事件对象,实际上是一个函数”,它提供“add()等五个方法”。
由于“多投事件对象是函数”,因此下面的代码是成立的:
----------
func1 = func2 = function() { /* ... */ };
function MyObject() {
this.OnExec = new MuEvent();
this.run = function() {
// do somethings
this.OnExec();
}
}
var obj = new MyObject();
obj.OnExec.add(func1);
obj.OnExec.addMethod(window, func2);
obj.run();
----------
这个例子用最简的代码演示了多投事件对象的使用。你看到我们最终仍然要通
过某种方式来使OnExec()被执行。只不过它被执行的时候,将同时触发func1和
func2两种行为。
1. add(), addMethod()
----------
在多投事件对象的方法中,addMethod()第一个不容易理解的东西。但我们需
要了解到:使用add()加入的func1,执行期拿到的this对象会是obj本身;而使
用addMethod()加入的func2,执行期拿到的this对象将是window对象。
这有什么意义呢?
例如setTimeout()这样的函数在执行期只允许传入函数,而不能传入对象方法。
这就使得定时执行一个对象方法的代码只能这样写:
----------
function doTimer() {
obj1.call();
obj2.call();
}
setTimeout(doTimer, 1000);
----------
在使用MuEvent()的情况下,上面的代码就可以很简单了:
----------
var e = new MuEvent();
e.addMethod(obj1, obj1.call);
e.addMethod(obj2, obj2.call);
setTimeout(e, 1000);
----------
addMethod()在Atlas里被称为addAction()。这两者的含义是一致的。成熟的多
投事件系统通常都会提供这种特性。
2. clear()与reset()
----------
Qomo提供clear()方法来清除与该多投事件对象绑定的“事件句柄列表”。而
reset()则在清除之后再添加一个“事件句柄”。由于MuEvent对象也是函数,因
此下面的代码也可以添加一个事件投送列表:
----------
var e1 = new MuEvent();
var e2 = new MuEvent();
// ...
// add somethings to e1
e2.add(func1);
e2.add(func2);
e2.add(func3);
// clear e1, and add a list(e2)
e1.reset(e2);
----------
3. close()
----------
Qomo提供一种非常特殊的“关闭多投特性”的机制。——注意这在其它的框架
上都没有实现。
Qomo的多投事件对象是一个普通的函数,只不过它多了add()、addMethod()等等
方法。如果我们清除掉这些方法,那么该对象的外在表现就与一个普通函数完全
无异。这种情况下,一个第三方的框架根本无法识别这个“关闭多投特性的‘多
投事件对象’”,而当成一个普通函数处理。
因此Qomo的多投特性可以完全透明地嵌入一个第三方框架。甚至象DOM这样的浏览
器基础系统。例如下例:
----------
var loading = new MuEvent();
loading.add(loadPicture1);
loading.add(loadPicture2);
loading.add(loadPicture3);
// ...
loading.add(loadPicture1000);
loading.close();
window.onload = loading;
----------
这种情况下,浏览器的DOM框架完全感觉不到loading(作为一个函数)有什么不同。
Qomo提供的close()特性的作用远不至此。事实上,close()特性真正的价值在于对
系统设计层面的考量。例如我们做一个TLabledEdit对象,也就是将一个Lable与一
个Edit绑在一起。那么我们发现,我们事实上对Lable.onclick的行为的理解,肯定
是“选中Edit并置输入焦点”。这种行为特征在设计之初就被确定了,根本不应该
被更改。——当然,如果你的设计就是要更改,那另论。
而原始的TLable的设计中,TLable.onclick是一个公开的方法,并且是多投事件。
那么即使我们写下下面的代码:
----------
FLabled.onclick.addMethod(FEdit, FEdit.onclick);
----------
在其后的、用户的代码中仍然可以改变FLabled.onclick的行为。例如add/clear()。
这显然是这个TLabledEdit组件的原始设计者所不希望的。因此,在提供了close()
特性的情况下,它就可以在上面的代码中这样写:
----------
// 当创建结束调用
this.DoCreate = function() {
FLabled.onclick.addMethod(FEdit, FEdit.onclick);
FLabled.onclick.close();
}
----------
这样就可以保证onclick()的特性不被变更。而且,如果FEdit.onclick被变量(例
如add/reset),FLabled.onclick可以正常地感知到。
4. 为什么不提供del()
----------
Qomo的多投事件不提供del()特性。基于两个原因:
- del()可能导致事件的激活顺序被破坏
- del()需要执有内部“事件句柄列表”中的事件方法的引用,这破坏了封装性
因此,(在目前的版本中,)作为一项框架设计层面上的考量,Qomo不提供del()。但
是,由于atlas的多投事件有del()方法,因此在将来实现嵌入vs.net的代码时,Qomo
是可能会提供del()方法的。
四、多投事件系统的实现分析
~~~~~~~~~~~~~~~~~~
1. 基本的多投事件系统
----------
最基本的多投事件系统实现方法是这样:
----------
function MuEvent() {
// this is a new obj instance
var all = this;
all.length = 0;
function add(foo) { /* ... */ }
function addMethod(obj, foo) { /* ... */ }
function clear() { /* ... */ }
function reset(foo) { /* ... */ }
function run() { /* ... */ }
var e = function() { return run.call(this, arguments) }
e.add = add;
e.addMethod = addMethod;
e.clear = clear;
e.reset = reset;
return e;
}
----------
这样实现的看起来很简单、自然。而且由new()关键字构造的对象实例this已经被
内部变量all执有了一个引用,用以建立事件列表。避免了不必要的开销。看起来
是不错的。——自然close()方法的实现也很容易,不成问题。
但是这种情况下,我们对比多个事件对象,会发现一个不可接受的事实:
----------
var e1 = new MuEvent();
var e2 = new MuEvent();
alert(e1.add === e2.add)
----------
你会发现结果是false,也就是说:有多少个事件对象,就会有多少个add、clear
方法。其开销极其巨大:n * 5。
2. Qomo中多投事件系统的实现基础
----------
在Qomo里,这一切被巧妙地避免了。我为每一个事件对象建立了一个handle。它
是一个索引。
----------
function _MuEvent() {
// get a handle and init MuEvent Object
var handle = all.length++;
//...
----------
为了让add()等方法成为“唯一实例”,我将它放在了_MuEvent()之外来实现。但
这种情况下,对象执有的handle对add()方法就是不可见的了。因此我们还需要一
种机制,来使对象可以向add()等方法暴露handle。这里,我们选用了valueOf()。
对于函数(多投事件对象)ME来说,它的valueOf()的结果指向自身:ME。在大多数
的情况下,这是没有意义的。因此我们这样来实现valueOf():
----------
ME.valueOf = function() {
return handle
};
----------
而在add()中,我们这样来使用valueOf():
----------
var all2 = []; // all ME() object for recheck.
function add(foo) {
var i=this.valueOf(), e=all[i];
if (e && e==all2[i]) {
// add...
}
}
----------
由于我们使用了第二个数组all2来复核,因此可以避免用户使用这样的代码来套
取、破坏多投事件列表:
----------
// if e1's handle is 10, and hide into a Object/System
var e1 = new MuEvent();
// 套取用的函数
f = function(){};
// 指定欲套取的句柄
f.valueOf = function() { return 10 }
// 重置(注意所有的多投事件对象的方法是相同的)
f.clear = (new MuEvent()).clear;
// 破解e1的事件列表(利用valueOf()返回10的特性)
f.clear();
----------
所以这样来看,加入数组all2[]来复核是必须的。
3. “强壮”与“快”是两难的
----------
但接下来,我们也发现这个“多投事件系统”是不“强壮”的。为什么呢?因为
valueOf()仍然可以被外部代码改写。——这将导致依赖它来获取handle的add()
等方法失效。事实上,由于我们重定义了valueOf()的含义,也使得Qomo与一些第
三方的框架、系统中可能出现不兼容。
Qomo应当是一个强壮的系统。由于valueOf()的存在,影响了强壮性,也使“透明”
成为空话。
我们回到前面这个all2[]。事实上,由于复核的必要,我们已经存放了一份所有对
象的列表。因此,不通过handle来查找ME和事件列表对象,是可能的:
----------
function add(foo) {
var e = all.search(this);
if (e) {
// ...
}
}
----------
在这个代码中,我们需要在all.search()其实被设计成一个算法,用于在all2[]中
查找this对象(也就是ME()函数)。而search()返回的,则是“使用all2[]中this对象
的索引”,在all[]中查找到的“投送事件列表”。——这个索引其实就是handle。
这样,就不需要重写ME().valueOf()来公布handle了。但是,由于每次add()等操作
都将查找all2[],使得系统会相对慢一些。——简单的说:强壮了,但慢了。
所以整个MuEvent的实现代码是这样:
----------
var MuEvent = function (fast) {
var all = {
length : 0,
strong : !fast, // ^.^
// ...
}
return _MuEvent;
}(true);
----------
简单地说,上面的一行代码真实的反映了:强壮就不快,快就不强壮。
4. 真的不快吗?
----------
简单地分析一下我们在使用事件系统时候的一些特点,我们会发现:
- 事实上通常我们会成批地添加一个事件,或者一个对象的一组事件
- 事实上相关的对象、事件总是被“在临近时间上”被处理的
例如我们通常会在对象初始化的时候写这样的代码:
----------
obj.onclick.add(foo1);
obj.onclick.add(foo2);
obj.onmouseout.add(foo3);
----------
而在多投事件体系中,同一对象的OnXXXXX事件通常是连续被创建的,而且刚刚完成
创建的事件对象可能会被赋以一些初值。简单的讲,这些使用习惯表现为:
- 最近创建的事件总是可能会被很快操作到
- 最近操作的事件附近的(同一对象的)事件总是可能会被很快操作到
基于这两个原理。Qomo设计了一个在all2[]中查找事件对象的方法:总是从上一次
添加或查找到的事件对象的附近,开始前向、后向检索。具体的算法参见all.search().
加入检索算法使得add等行为的速度大大的加快。当然,这是基于开发人员的代码行
为分析的,而真正的“在数组中检索对象”的方法的效率并没有办法提高。这是JS自
身无可回避的问题。
但是,如果引用hash或者使用对象属性名来检测,则必然要给ME()对象一个“可在外
部访问的key/name值”。这又回到了前面提供handle的“fast方法”同样的问题上。
因此这样的问题,是不必要再讨论的。
Qomo的JSEnhance.js中,默认采用"fast = false"的配置,以提供一套强壮的系统。但
如果你确信你的系统是封闭的、不会导致第三方的框架的影响的,那么你可以在JSEnhance
中开启下面这个开关:
----------
var MuEvent = function (fast) {
// ...
return _MuEvent;
}(false); // <-- here, set to true
----------
最后,最重要的一点提示,是Qomo在这个多投事件系统上的效率牺牲,只会表现在add
等方法的调用上。并不会对ME()事件的执行构成任何的影响。因为在代码上:
----------
function _MuEvent() {
// get a handle and init MuEvent Object
var handle = all.last = all.length++;
var ME = function() {
if (all[handle].length > 0) // <--- 直接使用handle
return run.call(this, handle, arguments)
}
//...
}
----------
由于ME()的执行可以直接使用内部的handle变量,根本就不会调用all.search()。因此
Qomo只是在“维护事件投送列表(add/reset等)”时有一些search()的性能开销。“执行
投送事件”时,是性能最优化的。