Dorado 5 : 3.5.其他客户端技巧 (RF4)

利用setTimeout()、clearTimeout()减少调用次数

我们通过一个场景分析来说明该优化技巧的使用方法。有一个选择员工的页面,页面上的主要的控件是一个支持多选的数据表格,每次可显示100位员工。现在需要在页面上实现这样一个功能,每当用户在表格中勾选或反选一个员工时,程序就立刻统计出所有被选择的员工的薪水的合计,并显示到页面上的一个标签中。

为了实现上述功能,我们首先编写的了一个叫getTotalSalary()的JavaScript函数,该函数利用遍历Dataset的方式统计所有被选择的员工的薪水的合计。并且在Dataset的afterSelectionChange事件中调用此函数。一切看起来都工作的非常流畅,直到用户点击了表格的列标题上的全选按钮。执行全选的过程慢的惊人,完全超出了可以忍受的范围。
发生这种现象的原因是在系统完成全选的过程中,总共有可能会触发近100次的afterSelectionChange事件,从而导致近100次的getTotalSalary()运算。
有一个解决方法是设法在执行全选的过程禁用afterSelectionChange事件,直到全选完成之后再进行一次getTotalSalary()运算。不过,这在Dorado中并不容易实现,Dorado没有提供专门的全选或全部反选事件。
所以我们可以考虑另一种替代方法 —— 使用setTimeout()和clearTimeout()方法。setTimeout()的作用是延时执行一段Script,例如下面的代码表示在1000毫秒之后弹出一个显示Hello World!的对话框。

setTimeout("alert(\"Hello World!\")", 1000);

setTimeout(function() {
alert("Hello World!");
}, 1000);

setTimeout()会返回一个数值代表系统内部使用的计时器的ID,我们可以在延时动作触发之前利用此ID取消掉它。利用执行下面的代码什么都不会发生,因为计时器刚一创建就被取消了。

var timeId = setTimeout("alert(\"Hello World!\")", 1000);
clearTimtout(timeId);

利用延时执行的特性来优化此页面的原理的是,在Dataset的afterSelectionChange事件中,激活getTotalSalary()方法之前先检查当前有没有正在等待执行的计时器,如果有则取消它。然后再设置一个新的计时器延时200-300毫秒,以激活getTotalSalary()方法。这对通常的操作几乎不会有任何影响。而对于全选或反选操作而言,两个选择动作之间的间隔时间肯定小于300毫秒,因此在执行全选或反选操作时将不断的重复"创建计时器、取消计时器"的动作,最终将只有最好一个计时器会得到执行。所以,在进行全选或反选操作时,同样便只会触发最后一个getTotalSalary()方法。
具体的做法是:

  • 在ViewModel的<function>中添加一个用户保存最后一个计时器ID的变量。例如:

    var timeId = 0;

  • 在Dataset的afterSelectionChange时间中,首先判断是否存在需要取消的计时器。然后创建新的计时器。例如:

    if (timeId != 0) {
    clearTimeout(timeId);
    timeId = 0;
    }
    timeId = setTimeout("getTotalSalary()", 300);

    经过这样的改造,任何情况下在300毫秒内有1个以上选中或反选动作,至多只会触发一次getTotalSalary()操作。

    检查IE内存泄露

    关于IE中DOM对象的内存泄露是一个常常被开发人员忽略的问题。然而它导致的问题却是非常严重的!它会导致IE的内存占用量只需上升,并且浏览器的整体运行速度明显下降。对于一些泄露比较严重的网页,设置只要刷新几次,运行速度就会降低一倍。
    比较常见的内存泄漏的模型有"循环引用模型"、"闭包函数模型"和"DOM插入顺序模型",对于前两种泄漏模型,我们都可以通过在网页析构是解除引用的方式来避免。而对于DOM插入顺序泄漏模型则需要通过改变一些惯有的编程习惯的方式来避免。
    有关内存泄漏的模型的更多介绍可以通过Google很快速的查到,本文不做过多的阐述。不过,这里向您推荐一个可用于查找和分析网页内存泄露的小工具—Drip,目前的较新版本是0.5,下载地址是http://outofhanwell.com/ieleak/index.php

    如何利用JavaScript代码生成界面元素

    客户端的编程中,对DOM对象的操作往往是最容易占用CPU时间的。而对于DOM对象的操作不同的编程方法之间的性能差异常常是非常大的。
    以下是三段效果运行结果完全相同的代码,它们的作用是在网页中创建一个10x1000的表格。然而它们的运行速度却有着天壤之别。

    测试代码1 - 耗时: 41秒

    var table = document.createElement("TABLE");
    document.body.appendChild(table);
    for(var i = 0; i < 1000; i++){
    var row = table.insertRow(-1);
    for(var j = 0; j < 10; j++){
    var cell = objRow.insertCell(-1);
    cell.innerText = "( " + i + " , " + j + " )";
    }
    }


    测试代码2 - 耗时: 7.6秒

    var table = document.getElementById("TABLE");
    document.body.appendChild(table);
    var tbody = document.createElement("TBODY");
    table.appendChild(tbody);
    for(var i = 0; i < 1000; i++){
    var row = document.createElement("TR");
    tbody.appendChild(row);
    for(var j = 0; j < 10; j++){
    var cell = document.createElement("TD");
    row.appendChild(cell);
    cell.innerText = "( " + i + " , " + j + " )";
    }
    }


    测试代码3 - 耗时: 1.26秒

    var tbody = document.createElement("TBODY");
    for(var i = 0; i < 1000; i++){
    var row = document.createElement("TR");
    for(var j = 0; j < 10; j++){
    var cell = document.createElement("TD");
    cell.innerText = "( " + i + " , " + j + " )";
    row.appendChild(cell);
    }
    tbody.appendChild(row);
    }
    var table = document.getElementById("TABLE");
    table.appendChild(tbody);
    document.body.appendChild(table);

    这里的"测试代码1"和"测试代码2"之间的差别在于在创建表格单元时使用了不同的API方法。而"测试代码2"和"测试代码3" 之间的差别在于处理顺序的略微不同。
    "测试代码1"和"测试代码2"之间如此大的性能差别我们无从分析,目前所知的是insertRow和insertCell是DHTML中表格特有的API,createElement和appendChild是W3C DOM的原生API。而前者应该是对后者的封装。不过,我们并不能因此而得出结论认为DOM的原生API总是由于对象特有的API。意见大家在需要频繁调用某一API时,对其性能表现做一些测试。
    "测试代码2"和"测试代码3"之间的性能差异主要来自于他们的构建顺序不同。"测试代码2"的做法是首先创建最外层的<TABLE>对象,然后再在循环中依次创建<TR>和<TD>。而"测试代码3"的做法是首先在内存中有内到外的构建好整个表格,最后再将他添加到网页中。这样做的目的是尽可能的减少浏览器重启计算页面布局的次数。每当我们将一个对象添加到网页中时,浏览器都会尝试页面中的控件的布局进行重新计算。所以,如果我们能够首先在内存将整个要构造的对象全部创建好,然后再一次性的添加到网页中。那么,浏览器将只会做一次布局的重计算。总结为一句话那就是越晚执行appendChild越好。有时为了提高运行效率,我们甚至可以考虑先使用removeChild将已存在的控件从页面中移除,然后构造完成后再重新将其放置回页面当中。

    innerText和innerHTML

    在IE中像一个标签注入内容时有两个可以选择的属性:innerText和innerHTML。二者的功能略有不同,innerText是不会识别内容中的HTML的,而innerHTML会将内容中的HTML转换成真正的DHTML对象。
    所以,如果你的内容里没有HTML,那就应该尽可能的使用innerText来替代innerHTML。innerText比innerHTML的效率高的多。

    function onRefresh(column, row, cell, value, record) {
    cell.innerHTML = "<a href='http://" + value + "'>" + value + "</a >";
    }

    在Dorado中innerText和innerHTML可能较常用于onRefresh这类事件,例如以下是某个表格列的onRefresh事件的代码:
    在默认情况下Mozilla中只有innerHTML属性, Dorado通过prototype为Mozilla也加上了innerText。所以在有Dorado的页面中我们可以放心的使用innerText属性。

    如何高效的拼装字符串

    在使用Ajax提交信息时,我可能常常需要拼装一些比较大的字符串通过XmlHttp来完成POST提交。尽管提交这样大的信息的做法看起来并不优雅,但有时我们可能不得不面对这样的需求。那么JavaScript中对字符串的累加速度如何呢?我们先来做下面的这个实验。累加一个长度为30000的字符串。

    测试代码1 - 耗时: 14.325秒

    var str = "";
    for (var i = 0; i < 50000; i++) {
    str += "xxxxxx";
    }

    这段代码耗时14.325秒,结果并不理想。现在我们将代码改为如下的形式:

    测试代码2 - 耗时: 0.359秒

    var str = "";
    for (var i = 0; i < 100; i++) {
    var sub = "";
    for (var j = 0; j < 500; j++) {
    sub += "xxxxxx";
    }
    str += sub;
    }

    这段代码耗时0.359秒!同样的结果,我们做的只是首先拼装一些较小的字符串然后再组装成更大的字符串。这种做法可以有效的在字符串拼装的后期减小内存复制的数据量。知道了这一原理之后我们还可以把上面的代码进一步拆散以后进行测试。下面的代码仅耗时0.140秒。

    测试代码3 - 耗时: 0.140秒

    ar strArray = new Array();
    for (var i = 0; i < 100; i++) {
    var sub = "";
    for (var j = 0; j < 500; j++) {
    sub += "xxxxxx";
    }
    strArray.push(sub);
    }
    str = String.prototype.concat.apply("", strArray);

    不过,上面这种做法也许并不是最好的!如果我们需要提交的信息是XML格式的(其实绝大多数情况下,我们都可以设法将要提交的信息组装成XML格式),我们还能找能更高效更优雅的方法—利用DOM对象为我们组装字符串。下面这段代买组装一个长度为950015的字符串仅须耗时0.890秒。

    利用DOM对象组装信息 - 耗时: 0.890秒

    var xmlDoc;
    if (browserType == BROWSER_IE) {
    xmlDoc = new ActiveXObject("Msxml.DOMDocument");
    }
    else {
    xmlDoc = document.createElement("DOM");
    }
    var root = xmlDoc.createElement("root");
    for (var i = 0; i < 50000; i++) {
    var node = xmlDoc.createElement("data");
    if (browserType == BROWSER_IE) {
    node.text = "xxxxxx";
    }
    else {
    node.innerText = "xxxxxx";
    }
    root.appendChild(node);
    }
    xmlDoc.appendChild(root);

    var str;
    if (browserType == BROWSER_IE) {
    str = xmlDoc.xml;
    }
    else {
    str = xmlDoc.innerHTML;
    }

    把自定义的JavaScript放入到包含文件中

    在经过一段时间的开发之后项目组往往会积累下一些通用的JavaScript函数,我们会将他们抽取到一个独立的文件当中。为了让其他页面可以使用这些通用函数,我们需要把它们引入到那些页面当中。
    有一种不好的做法是在服务端利用
    <%@ include file="common-func.jsp" %>

    <jsp:include page=" common-func.jsp""/>
    这样的方式来引入。这样做的弊端在于这些代码是直接输出到主页面当中的,这会使每一个JSP页面的尺寸变大,增加网络传输的负担。
    更好的做法是把这些函数抽取到一个独立的.js文件当中,并在页面上通过
    <script src="common-func.js"></script>
    来引入。在这种方式下,common-func.js在客户端也是以独立文件的形式存在的,浏览器可以对它进行缓存以避免在每一次使用该页面时都去下载它。
    如果你希望每一个Dorado页面都可以使用这个js库,还可以直接利用Dorado提供的js库的管理功能。方法是把你的js库文件复制到home\smartweb\v2\lib目录中,然后在home的根目录下添加一个javascript-lib.xml文件(如已存在则直接打开编辑),其内容大致如下:

    <?xml version="1.0" encoding="UTF-8"?>
    <libraries>
    <library name="Common Functions" path="common-func.js" />
    </libraries>