Qomo 2.0 beta1 发布说明及新功能

注:有关本次发布的一些重要信息请参见:关于Qomo 2.0 beta1的发布

一、Qomolangma 2.0 Beta1

Qomo的上次发布是在5个月前,此次的beta1主要包含对框架、兼容层和Builder系统的更新。有关界面组件库、图形库的发布,大约要到beta2或beta3才会提供。

Beta1主要的更新包括:

  • 修改了大量代码和builder工具,实现对IE5.0的完整兼容,兼容safari v2,部分兼容opera
  • 对兼容层代码做了大量修改, 并调整了兼容层在system.js中的装载顺序
  • 新增载入第三方代码的$import2()函数
  • 实现接口委托
  • 实现类声明中的通用的特性get/setter
  • 更好的列举对象Enum()的实现,示例参考TestCase/T_Enum.html
  • 分析JavaScript源代码语法的工具ParseLite, 修改自Brendan Eich的Narcissus,性能极大优化
  • 一些小的实用工具函数:IsClass(), IsObject(), IsInterface(),HasInterface()等等
  • 高性能的模式替换对象Pattern(),示例参考TestCase/T_StringReplacePattern.html
  • 提供了一个正则表达式的性能分析工具:/Debug/TestCase/RegExpPerformanceTool.html
  • 其它的一些小的修正,以及代码注释上的增改

下面对其中的一些较特别的更新做补充介绍。

二、函数$import2()

$import2()是一个特殊的$imoprt()实现,它只适用于装载第三方代码的情况。beta1中它只在ParseLite的实现中被使用了,但以后的代码中将会有很多的地方用到它。它的函数描述为:

function $import2(src, prepare, patch, condition);

 - src: 装入的.js的url地址
 - prepare: 在装入代码前面添加的代码,例如变量初始化等
 - patch: 在装入代码尾部添加的代码, 用于向外部(例如Qomo)传出数据
 - condition: 装入代码的条件, 不填写时为true

其中src与condition参数与它们在$inline()和$import()中的用法是一致的。这里特别讲述一下prepare与patch。其中,prepare是简单的脚本文本,我们一般用它来初始化一些变量,以避免第三方代码包中的变量泄露出来,与Qomo的全局变量冲突。例如:

// 第三方代码文本(文件名: test.js)
value1 = 100;
name = 'mySystem';

如果我们直接执行它,那么Qomo的全局变量中就会多出value1和name两个变量名。如果我们用下面的方法来装载:

$import2('test.js', 'var value1,name;', '');

那么就不会有问题了。

但接下来,我们装载的test.js又能如何使用呢?比如这个test.js有一个类,或者对象,我们该如何来使用它呢?这就需要用到patch这个参数了。不过在讲述它之前,先说明$import2()中对this引用的特殊理解。

$import2()总是返回一个对象obj,如果this没有指向window,则使用this值作为obj;否则为obj新建一个对象实例。因此,如果你试图传入一个对象并用它传出值,那么可以这样写:

var obj = {};
$import2.call(obj, 'test.js', ...

这样的技巧可以用来修改对象的原型,例如:

$import2.call(MyObject.prototype, 'test.js', ...

当然你也可以直接使用传出对象,而无需事先声明它。例如:

var obj = $import2('test.js', ...

然而这个对象(或this引用)传入之后该如何使用呢?一方面你可以为patch参数传入一个字符串做代码块,这个代码块中可以直接使用this。另一方面,你也可以为patch参数传入一个函数,该函数在执行时的第一个参数就是这个this引用。例如:

var obj = $import2('test.js', 'var value1,name;', function(_this) {
  _this.value = value1;
  _this.name = name;
});

这样一来,我们就可以在外部系统中得到这个对象obj,它的value、name则来自于第三方代码。当然,其它的对象方法或类都可以用这种技术来从第三方代码中获取。

$import2()的一个实现实例,是Common/ParseLite.js。它从3rd/jsparse.js中获取了parse()和tokens()两个方法。而这两个方法被用作ParserLite.prototype原型方法。这样一来,ParserLite()就既具有Qomo的类的特性,又在对Qomo完全无扰的情况下,使用了第三方代码。

三、通用的特性get/setter

通用get/setter其实是Qomo V1中就已经具备的一个特性,只是未被公开。这个特性适用于这样的情况:某个类的许多特性的方法都基本一致,或者适合放在同一个函数中来实现。例如:

function MyObject() {

  this.setName = function(v) {
    if (!v) v = 'normal';
    this.set(v);
  }

  this.setValue = function(v) {
    if (!v) v = 'normal';
    this.set(v);
  }

  // ...
}

那么你可以用下面的代码来完成相同的功能:

function MyObject() {

  this.setValue =
  this.setName = function(v) {
    if (!v) v = 'normal';
    this.set(v);
  }

  // ...
}

也就是让setValue()与setName()使用同一个特性存取函数。

为了让你在代码中能够区分调用来自于哪一个方法,Qomo在调用这个特性存取函数时,会多传入一个参数,因此你可以写出如下的代码:

function MyObject() {

  this.setValue =
  this.setName = function(v, n) {
    if (!v) v = 'normal';
    this.set(v);

    ExtObject['OnChange' + n]();
  }

  // ...
}

这个例子中ExtObject是一个假设的外部对象。而在上面的特性存取函数被调用时,例如:

obj.set('Name', ' - MyName');
obj.set('Value', ' - MyValue');

则下面的外部对象方法也将被调用:

ExtObject.OnChangeName();
ExtObject.OnChangeValue();

当然,你也可以在这里传入参数v,或者用switch()语句来识别变量n并加以处理.

最后,请注意一个细节,对于通常的set/getter来说,其声明方法是:

this.getName = function() { ...
this.setName = function(v) { ...

而对通用的特性get/setter来说,传入的特性名是追加在其后的:

this.getName = function(n) { ...
this.setName = function(v,n) { ...

不过两种方法实现的读取器的使用方法完全一致:

obj.get(n);
obj.set(n,v);

四、接口委托(Delegate)

接口委托可能是本次(Qomo V2 Beta1)发布中最复杂的一项改动。在以前的文档中提及过它只是暂时未被实现,也指出了未被实现的原因:没有应用。

在Qomo V2开发中,因为$import2的出现,我们遇到了一个问题:如果一个对象是由$import2()装载的代码来实现的,而该对象又声明了接口,则该接口也必须要委托给第三方代码实现。例如下面的代码中:

// 接口声明
IMyIntf = function() {
  this.doAction1 =
  this.doAction2 = Abstract;
}

// 类声明
function MyObject() {
  Attribute(this, '3RdObject');

  this.doAction1 = function() {
    this.get('3RdObject').doAction1.apply(this, arguments);
  }

  this.doAction2 = function() {
    this.get('3RdObject').doAction2.apply(this, arguments);
  }

  this.Create = function() {
    // 1. 导入第三方代码
    // 2. 用第三方代码创建对象并置'3RdObject'特性
    this.set('3RdObject', aObject);
  }
}

// 类注册,并声明该类实现IMyIntf接口
TMyObject = Class(TObject, 'MyObject', IMyIntf);

在这个例子中,除了基本的框架之外,MyObject必须用重复的代码来实现doAction1, doAction2, ...等等类似的方法。事实上这些重复的代码并没多少特别的意义。所以我们将该接口的实现委托出去呢?

在新的Qomo版本中,使用下面的类声明过程可以做到了:

// 类声明
function MyObject() {
  Attribute(this, '3RdObject');

  this.Create = function() {
    // 1. 导入第三方代码
    // 2. 用第三方代码创建对象并置'3RdObject'特性
    this.set('3RdObject', aObject);
  }

  function proxy(_this) {
    return _this.get('3RdObject');
  }

  Delegate(_cls().Create, [
   [proxy, ['IMyIntf']]
  ]);
}

Delegate()工具函数实现在Interface.js中,用于委托一个对象/类的接口,交由第三方实现。在这个例子中,它被交由一个返回第三方对象的代理函数(proxy)——当然也可以在Delegate()中声明并使用
函数直接量。

除了使用代理函数之外,也可以简单的使用一个对象(或对象直接量)。例如:

  var obj = {
    doAction1 : function() { ... },
    doAction2 : function() { ... }
  }

  Delegate(_cls().Create, [
   [obj, ['IMyIntf']]
  ]);

Delegate函数声明为:

Delegate(consigner, confer);

其中consigner是委托者,在本例中是_cls().Create,由于_cls()是一个在类构造周期中使用的特殊函数,用于返回类的一个引用(本例中是TMyObject),所以事实上委托者就是TMyObject.Create。而该函数
事实上直接指向构造器MyObject,而当一个接口被注册到构造器时,其子类和实例都将继承该接口。这就与在Class()中声明该类实现IMyIntf接口的语义是一致的了。

上面函数声明中的confer是一个份委托协议。该协议总是一个数组,结构如下:

[
 [proxy, [confer_item, ...]],
 [proxy, [confer_item, ...]],
 ...
];

如前所述,其中proxy可以是代理对象或代理对象。而confer_item总是一个字符串,呈如下格式:

[<*|+|->]InterfaceName[.MethodName[:AliasName]]

字符串开始应当是一个修饰字符,如果缺省,则修饰字符为"+"。修饰字符的含义如下:

  * 实现指定接口,或所有接口的所有方法
  + 实现指定接口/接口的指定方法
  - 不实现指定接口/接口的指定方法

也就是一个包含和排除的规则集。这个修饰字符有两条优先规则:

  1. 排除匹配(-)优先于包含匹配(+/*)
  2. 指定匹配(接口.名称)优先于通用匹配( * )

这样,就包含了所有的委托关系。不在委托关系中的接口和方法,则默认由consigner自己来实现。

这个字符串其它的三个部分用于指定接口。包括:

  InterfaceName : 接口名. 例如IInterface
  MethodName : 方法名. 例如IInterface.QueryInterface
  AliasName : 别名, 这是指实现者(proxy)中用来实现MethodName的名字.

如果MethodName缺省,则表明实现整个接口(的全部方法);如果AliasName缺省,则表明代理与委托者使用相同的方法名。

对于类和构造器来说,委托关系与接口注册一样,也是可以被继承的。也就是说,一旦接口被声明委托,则子类和对象实例都使用该委托关系来实现接口。另外,委托关系也是可以被覆盖的,这种覆盖是confer_item的,也就是说可以可选地覆盖先前协议中的某些部分。下面的例子说明这种继承与覆盖的关系。

function MyObject() {
  Delegate(_cls().Create, [
   [{
     doAction1: function() { alert('doAction1') },
     doAction2: function() { alert('doAction2') }
    }, ['IMyIntf']]
  ]);
}

function MyObjectEx(){
  Delegate(_cls().Create, [
   [{
     doAction2: function() { alert('doAction2 - MyObjectEx') }
    }, ['IMyIntf.doAction2']]
  ]);
}

TMyObject = Class(TObject, 'MyObject', IMyIntf);
TMyObjectEx = Class(TMyObject, 'MyObjectEx');

// 测试代码
obj1 = new MyObject();
obj2 = new MyObjectEx();

intf1 = QueryInterface(obj1, IMyIntf);
intf2 = QueryInterface(obj2, IMyIntf);

intf1.doAction2();
intf2.doAction2();

需要注意的是,接口被委托实现并不表明对象也具有该方法。以上例来讲,intf1.doAction2()调用成功,并不表明obj1.doAction2()也能调用成功。Qomo没有自动实现该对象方法的能力。

五、接口内部聚合(Aggregate)

Aggregate在Qomo V2中有一种新的实现方案。Aggregate(聚合)的概念,是将多个接口实现在同一个目标中。它在Qomo的早期版本中被用于构造器或函数的内部,用于表明该构造器或函数内部实现了某个接口,而在它的外(方法)中不被表现出来。它的用法如下:

function MyObject() {
  // 声明聚合
  var intfs = Aggregate(cls().Create, IMyIntf, IObject, ...);

  // 实现被聚合的各个接口
  var intf1 = intfs.GetInterface(IMyIntf);
  intf1.doAction1 = ...
  intf1.doAction2 = ...

  var intf2 = intfs.GetInterface(IObject);
  intf1.hasEvent = ...
  intf1.hasProperty = ...
  ...
}

// 声明MyObject()注册了某些接口
Interface.RegisterInterface(MyObject, IMyIntf, IObject, ...);

在Qomo V2中,在我们实现了Delegate之后,我们发现聚合在实质上也可以理解为一种委托。对于上例来讲,我们可以用同样的委托代码来实现:

function MyObject() {
  Delegate(_cls().Create, [
   [{doAction1: ...,
     doAction2: ... }, ['IMyIntf']],
   [{hasEvent: ...,
     hasProperty: ...}, ['IObject']],
   ...
  ]);
}

因此,我们重新实现了Aggregate()这个工具函数,在它的内部存在一个名为Interfaces()的构造器。我们将Aggregate()的关系委托给该构造器的实例,并注册到接口系统中。这种新的实现方案比早期的版本更加简单,但使用的方法仍然是一致——不必再重写以前的代码。

新的Aggregate()的实现方案演示了Delegate()的灵活应用:我们对Aggregate参数构造了一个代理对象,并与该代理对象(自动地)创建了一份协议。不过,为此我们也稍稍地扩展了一下Delegate(),使得confer_item中直接InterfaceHandle,也就是接口的内部句柄。因此,在不知道InterfaceName的情况下,在Delegate()中也可以将InterfaceName填为一个整数值(InterfaceHandle)。不过目前这只被用在Inerface.js内部,用于Aggregate的实现。

六、其它

1. 关于IE5、safari和Opera的兼容

在Qomo V2中将全面兼容IE5.0,不过你必须使用Builer生成一个兼容它的版本,而不能直接在IE5.0中直接装载(未经编译的版本)和调试。

在Qomo V2中也完全兼容safari v2,需要留意的是,对于safari的早期版本并不兼容。这是因为safari从v2才开始支持Function.caller。

是同样的原因,却导致我们不能完全兼容Opera:它不支持Function.caller。而我们经过讨论,也无法在Opera上模拟出该效果,所以我们完成了除该项特性之外的全部兼容代码。然后,开始期待Opera的新
版本……

2. 关于parse分析的问题与应用

Qomo V2的Common目录中新增了一个ParseLite.js,我们通过装载第三方项目Narcissus的代码,并为它简单地注册了一个Qomo类,从而实现了这个工具。

不过,有两点要加以说明。其一,这是一个优化过的Narcissus项目,原来的Narcissus在处理稍大的代码块时效率就迅速地级数下降,通过灵活地使用RegExp,Qomo避免了这些问题。以对150K源代码进行分析为例,Narcissus需要450秒,而Qomo的ParseLite只需要~~EN~~不到8秒。

其二,ParseLite使用一个被裁剪过的Narcissus代码。没有execute部分,也去掉了一些扩展语法和异常处理——ParseLite希望被分析的代码是没有语法错误。

所以ParseLite适合于一些特殊的语法分析,例如分析一个代码块中有多少个函数,或者一个特定片断中的某个符号是何种语义。尽管它最终也返回一个语法树,但这个语法树不能直接用于Execute——它少了一些东西。

关于ParseLite的使用,请参考:

Framework/TestCase/T_ParserLite.html

3. 接口对特性提供直接支持

这是一个非常好的特性。在以前我们曾经声明过这样的接口:

INamedEnumer = function() {
  this.getLength =
  this.items =
  this.names = Abstract;
}

但它的实现却相当麻烦,因为Qomo对名为"getXXXXXXXXX"的方法有限制,因此你必须这样写代码:

function MyObject() {
  this.getLength = function() {
    // 这里的代码是为Attribute写的
  }

  this.Create = function() {
    // 这里的代码才提供给接口, 并通过访问Attribute才得到值
    this.getLength = function() {
      return this.get('Length');
    }
  }
}

在Qomo V2中,这个过程变得非常简单。对于接口来说,在QueryInterface()时,名字以"get/set"开始方法名,将会被映射到"对象.get()"或"对象.set()"。除非对象自己实现了getLength,或者根本没
有get/set方法(例如不是Qomo对象)。因此,用户不需要为此多写任意一行代码。如下例:

IUserInfo = function() {
  this.getName = Abstract;
  this.getAge = Abstract;
}

function MyObject() {
  Attribute(this, 'Name', 'MyName');
  Attribute(this, 'Age', 30);
}
TMyObject = Class(TObject, 'MyObject', IUserInfo);

var obj = new MyObjet();
var intf = QueryInterface(obj, IUserInfo);

alert(intf.getName());
alert(intf.getAge());

Qomo V2.0 Beta 1下载

https://github.com/aimingoo/qomo/releases

或从如下地址签出GIT/SVN: