Qomolangma实现篇(四):基本特性增强与多投事件系统

一、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()的性能开销。“执行投送事件”时,是性能最优化的。