提高JSCRIPT.NET代码执行效率的小技巧
 
 
 
准备工作:
(1) 一定的 ASP.NET 和 JScript.NET 编程经验;
(2) 下列平台之一:
Windows 2000(专业版、服务器版或高级服务器版) + Service Pack 2.0 ;
Windows XP 专业版
(3) 微软 IIS 5.0
(4) .NET 框架环境 (安装 Visual Studio .NET 或者 .NET 框架 SDK 时会自动安装 .NET 框架环境。具体方法参见 Five Steps to Getting Started with ASP.NET,[url]http://msdn.microsoft.com/library/en-us/dnaspnet/html/asp11122000.asp[/url])
简介
ASP.NET 页面比同等条件下的 ASP 页面的执行速度要快得多。因为 ASP.NET 代码是一次编译、反复执行;而 ASP 代码却是执行一次、解释一次。然而,简单地把 ASP 代码换成 ASP.NET 代码却未必能够改善性能。
本文将讨论一些简单的小技巧,它们充分利用了.NET 框架的灵活性,从而显著改善 ASP.NET 页面的性能。首先,我们讨论如何改进使用 COM 组件的页面。其次,我们谈谈 JScript.NET 语言提供了哪些新特性以提高效率。最后,我们介绍一些书写高效 JScript.NET 代码的小技巧。文中提到的方法易于实现,而且能使代码更流畅、更稳定、更可读。
怎样克服 COM 的交互操作(Interop)瓶颈
在 ASP.NET 页面中,代码并不直接调用 COM 组件。因为 .NET 框架提供一种运行库可调用的封装容器 (RCW),它介于 ASP.NET 页面的受管代码和 COM 组件的非受管代码之间充当代理 (proxy)。详情请见 Microsoft .NET/COM Migration and Interoperability ([url]http://msdn.microsoft.com/library/en-us/dnbda/html/cominterop.asp[/url])。RCW 需要在 ASP.NET 与 COM 进行数据转换,因此,ASP.NET 的总体执行效率可能反而不及 ASP 了。
怎么办?有两种方法:其一,使用 Interop Assembly,它们实际上是针对每个 COM 组件优化的 RCW;其二,用同样功能的 .NET 组件代替 COM 组件。相比之下,前者更方便,后者效率高。
使用 Interop Assembly
RCW 简化了 ASP.NET 与 COM 组件的通讯,然而它太慢了。一种解决方案就是用 Interop Assembly 取代默认的 RCW。 每个 Interop Assembly 都包含了表达 COM 组件类型库的元数据。Interop Assembly 实际上是一个独立的 DLL ,它能让客户端 (例如 ASP.NET) 把 COM 组件当作 .NET 类进行操作。它在交互之前就把 COM 组件的参数包装成了 .NET 框架的数据结构,由此避免了大多数转换。
原则上,尽量选用 primary Interop Assembly ,它们带有 COM 组件出版商数字签名。
[注] Visual Studio .NET 安装程序自动注册和安装了 ADO 和 MSHTML 等通用 COM 组件的 primary Interop Assembly 。在默认情况下,它们位于 C:\Program Files\Microsoft.NET\Primary Interop Assemblies 。
如果找不到适合你的 COM 组件的 primary Interop Assembly ,你也可以用类型库导入工具 Tlbimp.exe 生成一个 Interop Assembly 。Tlbimp.exe 可以在 .NET 框架 SDK 中找到,需要在 DOS 命令提示窗口中运行。它会根据 COM 组件的类型库提供的类信息自动生成一个 Interop Assembly 。默认情况下,由它产生的 Interop Assembly 具有和 COM 组件的名字空间一致的名字。
为了使用 Tlbimp.exe 产生 Interop Assembly,你必须首先找到 COM 组件的位置。
如何定位 COM 组件的 CLSID
1. 从 DOS 窗口运行 RegEdit.exe 。
注意: RegEdit.exe 允许查看和修改系统注册表。小心操作,不要修改注册表。
2. 展开 My Computer(我的电脑)项,然后展开其中的 HKEY_CLASSES_ROOT 项。
3. 展开与 COM 组件的名字相同的项。例如:为了寻找 ADODB COM 组件的 CLSID ,就应该展开 ADODB.Connection 项。
4. 单击 CLSID 项。
右边窗口中出现数值。
4. 双击右边的数值。
出现 Edit String(编辑字符串)窗口。
5. 在 Edit String 窗口中,复制 Value data (数值数据)栏目中的CLSID 数值,然后点 Cancel(取消)按钮。
典型的 CLSID 数值形如 {00000514-0000-0010-8000-00 AA006D2EA4} 。
注意:不要修改数据,也不要点 OK(确定) 按钮。
保持Register Editor (注册表编辑器)打开状态。
查到 CLSID 以后,就可以使用 RegEdit.exe 搜索与这个 CLSID 相关的 COM 组件库了。
寻找与某个 CLSID 相关的 COM 组件库
1. 回到Register Editor,展开 MyComputer 项。
2. 在菜单中,点 Edit (编辑),再点 Find (查找),在 Find what (查找目标) 对话框中粘贴 CLSID 数值。
请确认只有 Keys (项) 前面的方框被勾选。
3. 点 Find Next(查找下一个)按钮搜索键名为该 CLSID 值的项,再展开被找到的项。
此项可能包括下列子键之一: InProcServer32 、 LocalServer32 或 TypeLib。如果是 InProcServer32 或者 LocalServer32,往下看第 4 步。否则,往下看“根据 TypeLib 定位 COM 组件库”一节。
4. 单击 InProcServer32 或者 LocalServer32 。
右边窗口中出现数值。
5. 双击数值,打开 Edit String 窗口 。
在 Edit String 窗口中,复制 Value data 栏目中的全部内容,它是某个 DLL 文件的完整路径;然后点 Cancel 按钮。
Value data 栏目中的数据可能是这样:C:\Program Files\Common Files\system\ado\msado15.dll 。
注意:不要修改数据,也不要点 OK 按钮。
现在已经得到 DLL 的名字了。往下看“生成一个新的 Interop Assembly”一节。
根据 TypeLib 定位 COM 组件库
1. 单击 TypeLib。
右边的窗口中出现数值。
2. 双击数值,打开 Edit String 窗口。
3. 把来自 Value data 栏目的 TypeLib 数值粘贴到 Edit String 窗口中,然后点 Cancel 按钮。
注意:不要修改数据,也不要点 OK 按钮。
4. 在左边窗口里选中 My Computer 项,因为我们将在整个注册表中搜索与 TypeLib 数值对应的项 。
5. 从 Edit 菜单中选取 Find 项,把 TypeLib 数值粘贴到 Find what 对话框中。
请确认只有 Keys 前面的方框被勾选。
6. 点 Find Next 按钮搜索与 TypeLib 数值对应的项。展开被找到的项。它包含至少一个与版本号对应的子键,例如 1.0 。
7. 展开任何的版本号对应的子键。
它们又有子键,代表安装语言,0 代表默认语言。
8. 展开任何一种语言对应的子键。
9. 单击 win32 子键。
右边窗口里出现数值。
10. 双击数值,打开 Edit String 窗口。
11. 把来自 Value data 栏目的 DLL 路径粘贴到 Edit String 窗口中,然后点 Cancel 按钮。
DLL 路径形如 C:\Program Files\Microsoft SQL Server\80\COM\sqlmergx.dll 。
注意:不要修改数据,也不要点 OK 按钮。
得到包含完整路径的 COM 库名以后,生成 Interop Assembly 只是举手之劳了。
生成新的 Interop Assembly
1. 在 DOS 窗口中键入
cd c:\
当前路径切换为根目录。
2. 以完整的 COM 库名作为参数运行 Tlbimp.exe :
C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Bin\tlbimp.exe C:\Program Files\Common Files\System\ADO\msado15.dll
这里假设 COM 组件库为 ADODB.dll ,并假设它位于C:\Program Files\Common Files\System\ADO\msado15.dll。结果将在根目录中产生一个新的 Interop Assembly ,它的名字与对应的 COM 组件的名字空间相同,但未必与作为参数的 DLL 文件名一致。
这里的 C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Bin 是 Tlbimp.exe 被安装的默认位置,请换成实际安装路径。
注意:如果 primary Interop Assembly 已经被注册,则 Tlbimp.exe 将产生一个警告。此时请删除刚生成的 Interop Assembly ,并往下看“引入 Interop Assembly”一节。
3. 将生成的 Interop Assembly 移到适当的文件夹。
如果你的应用程序没有使用 global.asax ,那么就移到 Web 根目录,如 C:\Inetpub\wwwroot\bin ;否则,移到 global.asax 文件所在的文件夹。
注意:primary Interop Assembly 的位置不需移动,因为它们已经被注册到了全局组件缓存 (GAC) 中。
得到 primary Interop Assembly 或者新生成的 Interop Assembly 以后,我们就能在 ASP.NET 页面中使用它们了。
引用 Interop Assembly
1. 为了引用 Interop Assembly 而不是引用 COM 对象,只要在 ASP.NET 页面开头的 <%@ language=JScript %> 后面使用 @Import @Assembly 指示词即可。
例如:引用 ADODB Interop Assembly :
<%@ Import Namespace="ADODB" %>
<%@ Assembly Name="ADODB" %>
2. 将每个引用 COM 组件的 <object> 标记的属性名 progid 换成 class 。
例如:引用 COM 组件:
<object runat=server id="appConn" progid="ADODB.Connection" scope="Application" >
</object>
引用 Interop Assembly :
<object runat=server id="appConn" class="ADODB.Connection" scope="Application" >
</object>
3. 对于使用 ActiveXObjectobject 或者 Server.CreateObject 生成 COM 对象的代码,可用于构造 COM 组件,也可用作类型注解 (type annotation) 。
例如:
var rs = new ActiveXObject("ADODB.Recordset");
可以改成:
var rs : RecordSet = new Recordset;
ASPCOMPAT 属性
由 Visual Basic 6.0 等生成的 COM 组件调用了单线程运行单元(STA)。为了避免 ASP.NET 产生兼容性错误,我们可以在 <%@ page > 标记里设置 aspcompat 属性,以指示 ASP.NET 在 ASP 兼容模式下执行它:
<%@ Page aspcompat=true Language = JScript %>
注意:使用 Interop Assembly 时,虽然 ASP.NET 本身无法判断 COM 组件是否调用 STA,但是通过 aspcompat 属性可以避免由于 STA 引起的效率过低或者死锁。
使用对应的 .NET 组件
前面提到,只有把 COM 组件换成对应的 .NET 组件,才能从根本上解决 RCW 引起的效率低下。这是因为 .NET 组件使用受管代码,可以不经过 RCW 而被 ASP.NET 直接调用。
虽然在调用方法上有细微差别,但是常见的 COM 组件大都能找到功能相似的 .NET 组件。详情请见 .NET 框架文档。
注意:COM 组件的“名字空间”(namespace)和“类”(class)分别对应于 .NET 框架中的“组件”(component)和“组件对象”(component object)。
下面列出部分 COM 组件及其对应的 .NET 组件。
(1) ADO
System.Data Namespace(ADO.NET)
System.Data.SqlClient Namespace
(2) Scripting.FileSystemObject
System.IO Namespace
(3) Scripting.Dictionary
System.Collections Namespace
(4) Wscript.Shell
System Namespace(System.Environment Class)
Microsoft.Namespace(Registry Class)
System.Diagnostics Namespace(Process Class)
(5) ADSI
System.DirectoryServices Namespace
(6) MSXML 、XMLHTTP
System.Xml Namespace
(7) WMI
System.Management Namespace
(8) CDONTS.NewMail
System.Web.Mail Namespace
因此,我们建议用受管代码将 COM 组件改写为 .NET 组件。好处很多:强类型变量、增强的安全性、存取 .NET 框架,等等。详情请见 Microsoft .NET/COM Migration and Interoperability ([url]http://msdn.microsoft.com/library/en-us/dnbda/html/cominterop.asp[/url])。
如何使用类型注解
JScript.NET 包括了许多实用的新特征。其中主要是类型注解,它可以生成前期绑定代码。虽然类型注解使得 JScript.NET 成为强类型语言,但它仍然支持非类型(即松散类型或者后期绑定)变量。前期绑定能使编译器针对类型优化代码,从而提高效率。类型注解还能使编译器发现更多常见错误。
变量和参数的类型注解
原则上一切变量与函数的参数都应该使用类型注解。ASP.NET 使用快速模式编译 JScript.NET 代码,因此必须显式定义变量。事实上,无论是普通变量,还是函数参数、返回变量,加上类型注解都很容易。
类型注解不但有助于形成良好的编码习惯,而且能避免一些偶然失误,比如:赋值给错误的变量等。例如:通过类型注解,你可以显式规定循环变量只能赋整数值。记住:用冒号分隔变量和数据类型。代码如下:
for( var i:int = 0; i<10; i++)
x += i;
类型注解可用于 JScript.NET 支持的任何数据类型;也可以用于临时定义的新数据类型(类)。详情请见 JScript Data Types。
.NET 对象与 JScript 对象
JScript.NET 提供许多基于原型的对象,称为 JScript 对象。它们可以用作类型注解。然而, .NET 框架的对象却完全不同,它们是基于类的。JScript 对象可以与 .NET 框架的数据类型交互操作。换句话说,JScript 在必要时会在 JScript 对象与 .NET 框架的数据类型之间进行自动转换。例如:Date 对象与 System.DateTime 类交互操作,或者说,Date 对象可以代替 System.DateTime 类的实例,反之亦然。
凡是转换都会影响速度,通过类型注解就能避免转换。例如:为了处理日期和时间,如果使用 JScript.NET 函数时,就应该用 Date 对象作类型注解;而如果使用 .NET 框架函数时,就应该用 System.DateTime 作类型注解了。
数据转换常常是降低效率、产生运行时错误的罪魁祸首,因此你可能想让编译器把警告当作错误提示。怎么做呢?只要在 Page 标记中给 WarningLevel 属性赋值即可。WarningLevel 的有效范围是从 0 (忽略任何警告) 到 4 (把警告当成错误)。例如,为了捕获某页的所有警告,可以这么做:
<%@ Page Language=JScript WarningLevel=4 %>
下面列出部分 JScript 对象和对等的 .NET 框架数据类型
(1) ActiveXObject
无对等的类型
System.Object 相似
(2) Array
System.Array (交互操作)
Typed array (交互操作)
(3) Boolean
System.Boolean (交互操作)
(4) Date
System.DateTime (交互操作)
(5) Enumerator
无对等的类型
避免使用
(6) Error
无对等的类型
System.Exception 相似
(7) Function
无对等的类型
避免使用
(8) Number
System.Double (交互操作)
(9) Object
System.Object (交互操作)
(10) RegExp
System.Text.RegularExpression.Regex (交互操作)
(11) String (数据类型)(变量-长度)
String
System.String
(12) String (对象) (变量-长度)
System.String (交互操作)
(13) VBArray
System.Array
避免使用
注意:JScript 的 Array 对象是动态稀疏数组。而 System.Array 和 typed array 却是定长密集数组。因此在 JScript Array 对象与后两者之间的转换可能需要复制整个数组,这一过程相当缓慢。故通常的数组应该使用 System.Array ,除非你要用到JScript Array 对象的特殊性质。
以类代替构造函数
JScript.NET 既支持 JScript 对象,又支持基于类的对象。后者提供类型安全 (type safety),高效操作代码,并且能利用 .NET 框架类。因此,我们来看看如何用基于类的对象取代 JScript 对象。
JScript 对象转换为基于类的对象的基本规则
(1) 找出代码中的 JScript 对象。先搜索 this 关键字、expando 限定词、prototype 属性。再顺藤摸瓜,一一找出所有的 JScript 对象。
(2) 在 <script runat=server> 区块中定义所有类。类的名字与构造函数的名字相同。
(3) 把对象的构造函数和方法移到相应的类定义里,并且删除 expando 限定词。
(4) 定义类的所有变量、属性等。注意某些属性是在 expando 属性中以 this 关键字动态定义的,此时只需移去 expando 限定词即可;而有些则是以 prototype 关键字定义的,它们为所有对象所共享,因此需要加上 static 限定词。
(5) 以类的名字作为对象变量的类型注解。
以上的顺序不是绝对的。例如:你可以首先在构造函数外面加上类定义,也可以首先消去 expando 限定词。此后可以试着载入页面,根据出错信息进行相应的步骤。
举个例子吧。这里有一个使用了 JScript 对象的 ASP.NET 页面:
<%@ language=jscript %>
<html><body>
<script runat=server>
// Define the constructor.
expando function Circle (radius) {
this.r = radius;
}
// Declare an area method for all Circle objects.
expando function CircleArea () {
// The formula for the area of a circle is pi*r^2.
return this.pi * this.r * this.r;
}
</script>
<%
Circle.prototype.pi = Math.PI;
Circle.prototype.area = CircleArea;
// Create a new circle.
var ACircle = new Circle(2);
// Display the area of the circle.
Response.Write("Area of the circle is " + ACircle.area() +".<BR>\n");
%>
</body></html>
可以看出,构造函数是
expando function Circle (radius) {
this.r = radius;
}
在函数里定义了一个 r 属性,它的取值在 Circle 的各实例中可以不同。后面的代码:
Circle.prototype.pi = Math.PI;
Circle.prototype.area = CircleArea;
这两行通过 prototype 关键字显式定义了 pi 属性和 area 方法。于是 pi 属性成了 Circle 类原型的一部分,因而在所有实例中它都取同样的值。
现在,你能根据以上规则进行转换了:
<%@ language=jscript %>
<html><body>
<script runat=server>
class Circle {
// Define the constructor.
function Circle (radius : double) {
this.r = radius;
}
// Define the r field.
var r : double;
// Define the pi field.
static var pi : double = Math.PI;
// Define the area method.
function area () : double {
// The formula for the area of a circle is pi*r^2.
return Circle.pi * this.r * this.r;
}
}
</script>
<%
// Use type annotation, since Circle is a user-defined type.
var ACircle : Circle = new Circle(2);
// Display the area of the circle.
Response.Write("Area of the circle is " + ACircle.area() +".<BR>\n");
%>
</body></html>
基于类的对象有一个很大的优点:它的所有特征都被集中起来统一定义;而 JScript 对象是基于原型的,这些定义七零八散地分布在文件各处。基于类的对象还有一个优点就是它与 .NET 框架的交互能力更强。
在用法上,两种对象无明显差别。然而,在默认情况下,基于类的对象不能动态增加属性,除非定义为 expando 类。不幸的是,expando 类消耗更多资源,而且动态增加的属性不能被 .NET 框架存取。
如何提高 JScript.NET 的效率
JScript.NET 是一种功能强大而且成熟的编程语言,它能提供多种途径来实现同样的功能。举例来说,为了连接两个字符串,既能使用运算符 (+),又能使用方法 (Append)。至于哪种更好,还得视具体情况而定;下文将作详细解释。
除此之外, JScript.NET 还提供了一些特别的功能。例如:嵌套函数、with 语句、eval 方法,和 Function 构造器等。
使用 StringBuilder.Append 方法
对于大量字符串的连接,使用运算符 (+) 反而不如用 StringBuilder 对象的 Append 方法效率高。这是因为 JScript.NET 对它们作了不同的优化:运算符 (+) 针对少量字符串的情形优化,而 Append 方法则对大量字符串的情形优化。
两种方案的实际性能还取决于很多因素:系统当前负荷、页面请求数目、内存使用状况等。那么如何选择呢?其实只要选择恰当的工具,依次对两种方案的页面输入数据以测试其性能,最后比较测试结果即可。
这里有一个范例。我们在包含测试代码的两个页面中分别用运算符 (+) 和 Append 方法连接 100 个字符串。测试标准很简单:时间。结果 Append 方法大获全胜。
注意:本例采用大量重复数据作输入流,测试算法也很简单。我们建议选择专业工具,比如微软公司的 Web Application Stress (WAS),通过模拟页面的真实流量以得到更准确的结果。
第一个页面:
<%@ language=jscript %>
<html><body>
<script runat=server>
var starttime : Date;
var s : String;
var endtime : Date;
</script>
<%
starttime = new Date();
// Perform the concatenation 10000 times
// to get a measurable result.
for( var j=0; j<10000; j++) {
s = "";
// Concatenate 100 strings.
for( var i=0; i<100; i++)
s = s + "This is a test! ";
}
endtime = new Date();
Response.Write(s);
Response.Write("<BR> Milliseconds elapsed: ");
Response.Write(endtime-starttime);
%>
</body></html>
我们来改一改,用 StringBuilder 类取代 String 类型,用 Append 方法代替运算符 (+):
<%@ language=jscript %>
<html><body>
<script runat=server>
var starttime : Date;
var s : StringBuilder;
var endtime : Date;
</script>
<%
starttime = new Date();
// Perform the concatenation 10000 times
// to get a measurable result.
for( var j=0; j<10000; j++) {
s = new StringBuilder("");
// Concatenate 100 strings.
for( var i=0; i<100; i++)
s.Append("This is a test! ");
}
endtime = new Date();
Response.Write(s);
Response.Write("<BR> Milliseconds elapsed: ");
Response.Write(endtime-starttime);
%>
</body></html>
详情请见 StringBuilder class ([url]http://msdn.microsoft.com/library/en-us/cpref/html/frlrfSystemTextStringBuilderClassTopic.asp[/url])。
避免嵌套函数、with 语句、eval 方法和 Function 构造器
原则上,避免使用嵌套函数(即在函数中定义的函数)。嵌套函数能在一定程度上简化代码,却以消耗更多资源为代价。
为了提高效率,应该把嵌套函数移出来,要么在页面中单独定义,要么在类中定义。如果它使用了所在函数的变量,没关系,将其作为函数参数或者页面级的全局变量即可。
注意:Web 服务器在编译 ASP.NET 页面时,会把 <%...%> 区块中的内容置于某个特殊函数中,于是区块里的函数也成了嵌套函数。因此,不要在 <%...%> 区块里定义函数。这样一来可以提高效率,二来可以避免作用域冲突,何乐而不为?
类似地,with 语句也要避免。其实,用临时变量改写 with 结构,不但更有效率,而且可以减少失误。例如,这段代码使用了 with ,效率不高:
with (oTable.rows[9].style) { // Inefficient!
backgroundColor = "#ffff00";
border = "1px solid #ff0000";
cursor = "hand";
}
改成这样就好多了:
var o = oTable.rows[9].style; // More efficient.
o.backgroundColor = "#ffff00";
o.border = "1px solid #ff0000";
o.cursor = "hand";
最后,eval 方法和 Function 构造函数也应避免。它们可以在运行时产生新代码,大大提高了灵活性。然而,每次生成新代码,JScript.NET 脚本引擎都要运行一次。经验表明,脚本引擎浪费的时间远远超过了新代码运行的时间。相反,通过重写代码消除 eval 方法和 Function 构造器,执行效率就能上一个台阶。事实上,重写它们并不难吧!
结论
对于从 ASP 升级为 ASP.NET 的页面,可以有许多简单的途径提高其效率。例如:
(1) 对 COM 组件使用 Interop Assembly,或者改用对等的 .NET 组件;
(2) 使用类型注解和自定义类;
(3) 连接字符串时,用 StringBuilder.Append 方法可能比用运算符 (+) 更有效率;
(4) 避免那些低效且多余的 JScript 特性。
运用本文所介绍的小技巧,您的 ASP.NET 页面执行效率一定会有大幅提高。