提高JSCRIPT.NET代码执行效率的小技巧
转载
提高JSCRIPT.NET代码执行效率的小技巧
准备工作: |
(1) 一定的 ASP.NET 和 JScript.NET 编程经验; |
Windows 2000(专业版、服务器版或高级服务器版) + Service Pack 2.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 组件。相比之下,前者更方便,后者效率高。 |
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 组件的位置。 |
1. 从 DOS 窗口运行 RegEdit.exe 。 |
注意: RegEdit.exe 允许查看和修改系统注册表。小心操作,不要修改注册表。 |
2. 展开 My Computer(我的电脑)项,然后展开其中的 HKEY_CLASSES_ROOT 项。 |
3. 展开与 COM 组件的名字相同的项。例如:为了寻找 ADODB COM 组件的 CLSID ,就应该展开 ADODB.Connection 项。 |
5. 在 Edit String 窗口中,复制 Value data (数值数据)栏目中的CLSID 数值,然后点 Cancel(取消)按钮。 |
典型的 CLSID 数值形如 {00000514-0000-0010-8000-00 AA006D2EA4} 。 |
注意:不要修改数据,也不要点 OK(确定) 按钮。 |
保持Register Editor (注册表编辑器)打开状态。 |
查到 CLSID 以后,就可以使用 RegEdit.exe 搜索与这个 CLSID 相关的 COM 组件库了。 |
1. 回到Register Editor,展开 MyComputer 项。 |
2. 在菜单中,点 Edit (编辑),再点 Find (查找),在 Find what (查找目标) 对话框中粘贴 CLSID 数值。 |
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 。 |
现在已经得到 DLL 的名字了。往下看“生成一个新的 Interop Assembly”一节。 |
2. 双击数值,打开 Edit String 窗口。 |
3. 把来自 Value data 栏目的 TypeLib 数值粘贴到 Edit String 窗口中,然后点 Cancel 按钮。 |
4. 在左边窗口里选中 My Computer 项,因为我们将在整个注册表中搜索与 TypeLib 数值对应的项 。 |
5. 从 Edit 菜单中选取 Find 项,把 TypeLib 数值粘贴到 Find what 对话框中。 |
6. 点 Find Next 按钮搜索与 TypeLib 数值对应的项。展开被找到的项。它包含至少一个与版本号对应的子键,例如 1.0 。 |
10. 双击数值,打开 Edit String 窗口。 |
11. 把来自 Value data 栏目的 DLL 路径粘贴到 Edit String 窗口中,然后点 Cancel 按钮。 |
DLL 路径形如 C:\Program Files\Microsoft SQL Server\80\COM\sqlmergx.dll 。 |
得到包含完整路径的 COM 库名以后,生成 Interop Assembly 只是举手之劳了。 |
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 页面中使用它们了。 |
1. 为了引用 Interop Assembly 而不是引用 COM 对象,只要在 ASP.NET 页面开头的 <%@ language=JScript %> 后面使用 @Import @Assembly 指示词即可。 |
例如:引用 ADODB Interop Assembly : |
<%@ Import Namespace="ADODB" %> |
<%@ Assembly Name="ADODB" %> |
2. 将每个引用 COM 组件的 <object> 标记的属性名 progid 换成 class 。 |
<object runat=server id="appConn" progid="ADODB.Connection" scope="Application" > |
<object runat=server id="appConn" class="ADODB.Connection" scope="Application" > |
3. 对于使用 ActiveXObjectobject 或者 Server.CreateObject 生成 COM 对象的代码,可用于构造 COM 组件,也可用作类型注解 (type annotation) 。 |
var rs = new ActiveXObject("ADODB.Recordset"); |
var rs : RecordSet = new Recordset; |
由 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 引起的效率过低或者死锁。 |
前面提到,只有把 COM 组件换成对应的 .NET 组件,才能从根本上解决 RCW 引起的效率低下。这是因为 .NET 组件使用受管代码,可以不经过 RCW 而被 ASP.NET 直接调用。 |
虽然在调用方法上有细微差别,但是常见的 COM 组件大都能找到功能相似的 .NET 组件。详情请见 .NET 框架文档。 |
注意:COM 组件的“名字空间”(namespace)和“类”(class)分别对应于 .NET 框架中的“组件”(component)和“组件对象”(component object)。 |
下面列出部分 COM 组件及其对应的 .NET 组件。 |
System.Data Namespace(ADO.NET) |
System.Data.SqlClient Namespace |
(2) Scripting.FileSystemObject |
System.Collections Namespace |
System Namespace(System.Environment Class) |
Microsoft.Namespace(Registry Class) |
System.Diagnostics Namespace(Process Class) |
System.DirectoryServices Namespace |
System.Management Namespace |
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++) |
类型注解可用于 JScript.NET 支持的任何数据类型;也可以用于临时定义的新数据类型(类)。详情请见 JScript Data Types。 |
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 框架数据类型 |
System.Text.RegularExpression.Regex (交互操作) |
(11) String (数据类型)(变量-长度) |
注意:JScript 的 Array 对象是动态稀疏数组。而 System.Array 和 typed array 却是定长密集数组。因此在 JScript Array 对象与后两者之间的转换可能需要复制整个数组,这一过程相当缓慢。故通常的数组应该使用 System.Array ,除非你要用到JScript Array 对象的特殊性质。 |
JScript.NET 既支持 JScript 对象,又支持基于类的对象。后者提供类型安全 (type safety),高效操作代码,并且能利用 .NET 框架类。因此,我们来看看如何用基于类的对象取代 JScript 对象。 |
(1) 找出代码中的 JScript 对象。先搜索 this 关键字、expando 限定词、prototype 属性。再顺藤摸瓜,一一找出所有的 JScript 对象。 |
(2) 在 <script runat=server> 区块中定义所有类。类的名字与构造函数的名字相同。 |
(3) 把对象的构造函数和方法移到相应的类定义里,并且删除 expando 限定词。 |
(4) 定义类的所有变量、属性等。注意某些属性是在 expando 属性中以 this 关键字动态定义的,此时只需移去 expando 限定词即可;而有些则是以 prototype 关键字定义的,它们为所有对象所共享,因此需要加上 static 限定词。 |
以上的顺序不是绝对的。例如:你可以首先在构造函数外面加上类定义,也可以首先消去 expando 限定词。此后可以试着载入页面,根据出错信息进行相应的步骤。 |
举个例子吧。这里有一个使用了 JScript 对象的 ASP.NET 页面: |
// Define the constructor. |
expando function Circle (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; |
Circle.prototype.pi = Math.PI; |
Circle.prototype.area = CircleArea; |
var ACircle = new Circle(2); |
// Display the area of the circle. |
Response.Write("Area of the circle is " + ACircle.area() +".<BR>\n"); |
expando function Circle (radius) { |
在函数里定义了一个 r 属性,它的取值在 Circle 的各实例中可以不同。后面的代码: |
Circle.prototype.pi = Math.PI; |
Circle.prototype.area = CircleArea; |
这两行通过 prototype 关键字显式定义了 pi 属性和 area 方法。于是 pi 属性成了 Circle 类原型的一部分,因而在所有实例中它都取同样的值。 |
// Define the constructor. |
function Circle (radius : double) { |
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; |
// 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"); |
基于类的对象有一个很大的优点:它的所有特征都被集中起来统一定义;而 JScript 对象是基于原型的,这些定义七零八散地分布在文件各处。基于类的对象还有一个优点就是它与 .NET 框架的交互能力更强。 |
在用法上,两种对象无明显差别。然而,在默认情况下,基于类的对象不能动态增加属性,除非定义为 expando 类。不幸的是,expando 类消耗更多资源,而且动态增加的属性不能被 .NET 框架存取。 |
JScript.NET 是一种功能强大而且成熟的编程语言,它能提供多种途径来实现同样的功能。举例来说,为了连接两个字符串,既能使用运算符 (+),又能使用方法 (Append)。至于哪种更好,还得视具体情况而定;下文将作详细解释。 |
除此之外, JScript.NET 还提供了一些特别的功能。例如:嵌套函数、with 语句、eval 方法,和 Function 构造器等。 |
使用 StringBuilder.Append 方法 |
对于大量字符串的连接,使用运算符 (+) 反而不如用 StringBuilder 对象的 Append 方法效率高。这是因为 JScript.NET 对它们作了不同的优化:运算符 (+) 针对少量字符串的情形优化,而 Append 方法则对大量字符串的情形优化。 |
两种方案的实际性能还取决于很多因素:系统当前负荷、页面请求数目、内存使用状况等。那么如何选择呢?其实只要选择恰当的工具,依次对两种方案的页面输入数据以测试其性能,最后比较测试结果即可。 |
这里有一个范例。我们在包含测试代码的两个页面中分别用运算符 (+) 和 Append 方法连接 100 个字符串。测试标准很简单:时间。结果 Append 方法大获全胜。 |
注意:本例采用大量重复数据作输入流,测试算法也很简单。我们建议选择专业工具,比如微软公司的 Web Application Stress (WAS),通过模拟页面的真实流量以得到更准确的结果。 |
// Perform the concatenation 10000 times |
// to get a measurable result. |
for( var j=0; j<10000; j++) { |
// Concatenate 100 strings. |
for( var i=0; i<100; i++) |
s = s + "This is a test! "; |
Response.Write("<BR> Milliseconds elapsed: "); |
Response.Write(endtime-starttime); |
我们来改一改,用 StringBuilder 类取代 String 类型,用 Append 方法代替运算符 (+): |
// 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! "); |
Response.Write("<BR> Milliseconds elapsed: "); |
Response.Write(endtime-starttime); |
详情请见 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"; |
var o = oTable.rows[9].style; // More efficient. |
o.backgroundColor = "#ffff00"; |
o.border = "1px solid #ff0000"; |
最后,eval 方法和 Function 构造函数也应避免。它们可以在运行时产生新代码,大大提高了灵活性。然而,每次生成新代码,JScript.NET 脚本引擎都要运行一次。经验表明,脚本引擎浪费的时间远远超过了新代码运行的时间。相反,通过重写代码消除 eval 方法和 Function 构造器,执行效率就能上一个台阶。事实上,重写它们并不难吧! |
对于从 ASP 升级为 ASP.NET 的页面,可以有许多简单的途径提高其效率。例如: |
(1) 对 COM 组件使用 Interop Assembly,或者改用对等的 .NET 组件; |
(3) 连接字符串时,用 StringBuilder.Append 方法可能比用运算符 (+) 更有效率; |
(4) 避免那些低效且多余的 JScript 特性。 |
运用本文所介绍的小技巧,您的 ASP.NET 页面执行效率一定会有大幅提高。 |