65.9K
CodeProject 正在变化。 阅读更多。
Home

更快的 JavaScript StringBuilder

2008年5月1日

CPOL

3分钟阅读

viewsIcon

64309

downloadIcon

278

更快的 JavaScript StringBuilder

引言

我目前正在处理一个复杂的 AJAX 控件(Grid),并且需要连接大量的 strings(HTML)以便在客户端使用 JavaScript 进行渲染。

在 JavaScript 中,每个 string 都是不可变的(就像 .NET 一样),这意味着每次引擎使用 + 或 += 连接字符串时,它都会创建一个新的 string 实例并丢弃旧的。这在某些浏览器(主要是 Internet Explorer)中会导致连接速度非常慢。而在其他浏览器如 Mozilla Firefox 或 Safari 中,这些连接在执行前会进行优化(Firefox 3 的 JavaScript 引擎非常棒!)。

不幸的是,我必须支持 Internet Explorer 6 和 Internet Explorer 7,众所周知,这是一个很大的问题。

背景

通常,为了优化字符串连接,会使用 ArrayArray.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# 的格式化,它扩展了 StringString.format)。下面的示例展示了使用单独的 append 以及结合 appendString.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 的性能非常差。我在网上搜索了能够同时实现 appendformat 且性能良好的方法,并找到了一些很好的例子。我将它们与我的一些原创想法结合了起来。

我实现了两个解决方案来同时拥有 appendformatappendFormat)。

  1. 使用正则表达式(语法 "Hello {0}! {1}"):我使用 /\{(\d+)\}/g 来查找 { 和 } 中的所有十进制数字,并使用一个函数来替换参数。
  2. 使用 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:检索 strings 的连接结果。
// 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 个)...

  • 测试使用 + 连接 strings:17552 毫秒。
  • 测试使用 Sys.StringBuilder (append) 连接 strings:667 毫秒。
  • 测试使用 StringBuilderEx (append) 连接 strings:461 毫秒。

运行测试 格式化追加字符串(20000 个)...

  • 测试使用 Sys.StringBuilder (appendFormat) 连接 strings:4204 毫秒。
  • 测试使用 StringBuilderEx (appendFormat) 连接 strings:1658 毫秒。
  • 测试使用 StringBuilderEx (appendFormat Split+Join) 连接 strings:1225 毫秒。

我也在 Vista 上的 Firefox 3.0b5 中进行了测试。

运行测试 追加字符串(20000 个)...

  • 测试使用 + 连接 strings:109 毫秒。
  • 测试使用 Sys.StringBuilder (append) 连接 strings:211 毫秒。
  • 测试使用 StringBuilderEx (append) 连接 strings:152 毫秒。

运行测试 格式化追加字符串(20000 个)...

  • 测试使用 Sys.StringBuilder (appendFormat) 连接 strings:1962 毫秒。
  • 测试使用 StringBuilderEx (appendFormat) 连接 strings:770 毫秒。
  • 测试使用 StringBuilderEx (appendFormat Split+Join) 连接 strings:497 毫秒。

关注点

我不知道这是否是最快的 StringBuilder(可能不是),但它在 Internet Explorer 7 和 FF3 上表现相当快(甚至比 Sys.StringBuilder 快),这在我的 grid 中带来了很大的性能提升。希望这对大家有所帮助。

如果有人知道有更好的性能方法,请随时联系我!

历史

  • 2008 年 5 月 1 日 - 首次发布
  • 2008 年 5 月 3 日 - 修复了 bug 并修改了文章
© . All rights reserved.