更快的 JavaScript StringBuilder






3.74/5 (9投票s)
更快的 JavaScript StringBuilder
引言
我目前正在处理一个复杂的 AJAX 控件(Grid),并且需要连接大量的 string
s(HTML)以便在客户端使用 JavaScript 进行渲染。
在 JavaScript 中,每个 string
都是不可变的(就像 .NET 一样),这意味着每次引擎使用 + 或 += 连接字符串时,它都会创建一个新的 string
实例并丢弃旧的。这在某些浏览器(主要是 Internet Explorer)中会导致连接速度非常慢。而在其他浏览器如 Mozilla Firefox 或 Safari 中,这些连接在执行前会进行优化(Firefox 3 的 JavaScript 引擎非常棒!)。
不幸的是,我必须支持 Internet Explorer 6 和 Internet Explorer 7,众所周知,这是一个很大的问题。
背景
通常,为了优化字符串连接,会使用 Array
和 Array.join
方法。在 ASP.NET Ajax Framework 1.0 中,StringBuilder
就采用了这种方法,并且在 Internet Explorer 中的性能非常好。以下代码解释了这种方法。
// The constructor initializes an Array
StringBuilderEx = function()
{
this._buffer = new Array();
}
StringBuilderEx.prototype =
{
// This method appends the string into an array
append : function(text)
{
this._buffer[this._buffer.length] = text;
},
// This method does concatenation using JavaScript built-in function
toString : function()
{
return this._buffer.join("");
}
};
我们可以使用一个 JavaScript 技巧来优化这个类:每次函数调用都有一个开销,我们可以使用函数指针将 append
重定向到 push
,将 toString
重定向到 join
。
// Assigns our class to Array class
var StringBuilderEx = Array;
// Using prototype I link function append to push and toString to join
Array.prototype.append=Array.prototype.push;
Array.prototype.toString=Array.prototype.join;
这段代码看起来很奇怪,但它非常快,因为我们每次 append
只少了一个函数调用,每次 toString
也少了一个函数调用。
单独使用 append
来生成 HTML 会使代码非常难以维护。ASP.NET AJAX 框架还有一个格式化文本的方法,类似于 C# 的格式化,它扩展了 String
(String.format
)。下面的示例展示了使用单独的 append
以及结合 append
和 String.format
进行相同的 string
连接。
// It's very difficult to maintain
var sb = Sys.StringBuilder();
sb.append("<div style='width:");
sb.append(width);
sb.append("px;height:");
sb.append(height);
sb.append("px'></div>");
// It's more readable and easy to maintain
var sb = Sys.StringBuilder();
sb.append(String.format("<div style='width:{0}px;height:{1}px'></div>", width, height));
不幸的是,在 Internet Explorer 中使用 String.format
的性能非常差。我在网上搜索了能够同时实现 append
和 format
且性能良好的方法,并找到了一些很好的例子。我将它们与我的一些原创想法结合了起来。
我实现了两个解决方案来同时拥有 append
和 format
(appendFormat
)。
- 使用正则表达式(语法 "Hello {0}! {1}"):我使用 /\{(\d+)\}/g 来查找 { 和 } 中的所有十进制数字,并使用一个函数来替换参数。
- 使用 split 和 join(语法 "Hello ?! ?"):使用 ?(问号)符号来替换参数,我只需要在
toString
函数中进行一次 split 和两次 join。
// Assign our class to Array class
var StringBuilderEx = Array;
// Using prototype I link function append to push
Array.prototype.append=Array.prototype.push;
// Used to convert arguments in array
Array.prototype._convertToArray=function(arguments)
{
if (!arguments)
return new Array();
if (arguments.toArray)
return arguments.toArray();
var len = arguments.length
var results = new Array(len);
while (len--)
{
results[len] = arguments[len];
}
return results;
};
// First solution using regular expression
Array.prototype.appendFormat=function(pattern)
{
var args = this._convertToArray(arguments).slice(1);
this[this.length]=pattern.replace(/\{(\d+)\}/g,
function(pattern, index)
{
return args[index].toString();
});
};
// Second solution using split and join
Array.prototype.appendFormatEx=function(pattern)
{
if (this._parameters==null)
this._parameters = new Array();
var args = this._convertToArray(arguments).slice(1);
for (var t=0,len=args.length;t<len;t++)
{
this._parameters[this._parameters.length]=args[t];
}
this[this.length]=pattern;
};
// Concatenate the strings using join
// (some lines of code are relay with second solution)
Array.prototype.toString=function()
{
var hasParameters = this._parameters!=null;
hasParameters = hasParameters && this._parameters.length>0;
if (hasParameters)
{
var values = this.join("").split('?');
var tempBuffer = new Array();
for (var t=0,len=values.length;t<len;t++)
{
tempBuffer[tempBuffer.length]=values[t];
tempBuffer[tempBuffer.length]=this._parameters[t];
}
return tempBuffer.join("");
}
else
{
return this.join("");
}
};
Using the Code
我用 JavaScript 编写了一个自定义类,名为 StringBuilderEx
,它包含以下方法:
append
:追加string
。appendFormat
:追加并使用与 C# 相同的语法("Hello {0}")格式化string
。appendFormatEx
:追加并使用自定义语法("Hello ?")格式化string
。toString
:检索string
s 的连接结果。
// Sample
var sb = new StringBuilderEx();
sb.append("Hello");
sb.appendFormat("Hello {0}!!! {1}", "World", "Bye");
sb.append("Hello ?!!! ?", "World", "Bye");
性能
我在 Vista 上的 Internet Explorer 7 中进行了测试。
运行测试 追加字符串(20000 个)...
- 测试使用 + 连接
string
s:17552 毫秒。 - 测试使用
Sys.StringBuilder (append)
连接string
s:667 毫秒。 - 测试使用
StringBuilderEx (append)
连接string
s:461 毫秒。
运行测试 格式化追加字符串(20000 个)...
- 测试使用
Sys.StringBuilder (appendFormat)
连接string
s:4204 毫秒。 - 测试使用
StringBuilderEx (appendFormat)
连接string
s:1658 毫秒。 - 测试使用
StringBuilderEx (appendFormat Split+Join)
连接string
s:1225 毫秒。
我也在 Vista 上的 Firefox 3.0b5 中进行了测试。
运行测试 追加字符串(20000 个)...
- 测试使用 + 连接
string
s:109 毫秒。 - 测试使用
Sys.StringBuilder (append)
连接string
s:211 毫秒。 - 测试使用
StringBuilderEx (append)
连接string
s:152 毫秒。
运行测试 格式化追加字符串(20000 个)...
- 测试使用
Sys.StringBuilder (appendFormat)
连接string
s:1962 毫秒。 - 测试使用
StringBuilderEx (appendFormat)
连接string
s:770 毫秒。 - 测试使用
StringBuilderEx (appendFormat Split+Join)
连接string
s:497 毫秒。
关注点
我不知道这是否是最快的 StringBuilder
(可能不是),但它在 Internet Explorer 7 和 FF3 上表现相当快(甚至比 Sys.StringBuilder
快),这在我的 grid 中带来了很大的性能提升。希望这对大家有所帮助。
如果有人知道有更好的性能方法,请随时联系我!
历史
- 2008 年 5 月 1 日 - 首次发布
- 2008 年 5 月 3 日 - 修复了 bug 并修改了文章