高性能javascript之响应接口

响应接口

大多数浏览器有一个单独的处理进程,它由两个任务共享:Javascript任务和用户界面更新任务。每个时刻只有其中一个操作得以执行,也就是说当Javascript代码运行时用户界面不能对输入产生反应,反之亦然,当Javascript运行时,用户界面就被锁定了。

浏览器UI线程
JavaScript和UI更新共享的进程通常被称为浏览器UI线程。此UI线程维护着一个简单的队列系统,任务被保存到队列中直至进程空闲。一旦空闲,队列中的下一个任务将被检索和运行。这些任务不是运行Javascript代码就是执行UI更新,包括重绘和重排版。此进程中最令人感兴趣的部分是每次输入均导致一个或多个任务被加入。

<html> 
<head>
<title>Browser UI Thread Example</title>
 </head>
<body>
<button onclick="handleClick()">Click Me</button> <script type="text/javascript">
    function handleClick(){
    var div = document.createElement("div"); 
    div.innerHTML = "Clicked!";     
     document.body.appendChild(div);
} 
</script>
</body>
 </html>

当例子中按钮被点击时,它触发UI线程创建两个任务并添加到队列中。第一个任务是按钮UI更新,它需要做出相应的改变以指示它被按下了;第二个任务是Javascript运行任务,包含handleClick()的代码。假设UI线程空闲,第一个任务被检查并运行以更新按钮外观,然后Javascript任务被检查和运行。在运行过程中handleClick()创建了一个新的div元素,并追加到body元素上,其效果是引发另一次UI界面改变,也就是说在Javascript运行过程中,一个新的UI更新任务被添加队列中,当Javascript运行完后,UI还会再更新一次。
当所有UI线程任务执行之后,进程进入空闲状态,并等待更多任务添加到队列中。空闲状态是理想的。因为所有用户操作会立刻引发一次UI更新。
浏览器在Javascript运行时间上采取了限制,确保恶意代码编写者不能通过无尽的密集操作锁定用户和计算机。此类限制有两个:调用栈尺寸限制和长时间脚本限制。长运行脚本限制有时被称作长运行脚本定时器或者失控脚本控制器,但其基本思想是浏览器记录一个脚本的运行时间,一旦到达一定限度就终止它。

高性能javascript之DOM编程

DOM编程

对DOM操作代价昂贵,在富应用中通常是一个性能瓶颈。
文档对象模型(DOM)是一个独立于语言,使用XML和HTML文档操作的应用程序接口。在浏览器中,主要与HTML打交道,在网页应用中检索XML文档也很常见。DOM APIs主要用于访问这些文档中数据。
浏览器通常要求DOM实现和Javascript实现保持相互独立,例如在IE中,被称为JScript的Javascript实现位于库文件jscript.dll中,而DOM实现位于另一个库mshtml中。这样导致的问题是:简单来说,两个独立的部分以功能接口连接就会带来性能损耗。一个很形象的比喻是把DOM看成一个岛屿,把javascript看成另一个岛屿,两者之间以一座收费桥连接。每次ECMAScript需要访问DOM时,你需要过桥,交一次过桥费。你操作DOM次数越多,费用就越高。
简单来说,正如前面所讨论的那样,访问一个DOM元素的代价就是交一次过桥费。修改元素的费用可能更贵,因为它经常导致浏览器重新计算页面的几何变化。
为了给你一个关于DOM操作问题的量化印象,考虑下面的例子:

function innerHTMLoop(){
    for(var count = 0;count <= 15000; count++){
        document.getElementById('here').innerHTML += 'a';
    }
} 

此函数在循环中更新页面内容。这段代码的问题是,在每次循环单元中都对DOM元素访问两次:一次是读取innerHTML属性内容,另一次写入它。
一个更有效率版本是将使用局部变量存储更新后的内容,在循环结束后一次性写入:

function innerHTMLLoop2(){
   var content = '';
   for(var count = 0;count <= 15000; count++){
        content += 'a';
    }
    document.getElementById('here').innerHTML += content;

}

HTML集合

//无效的死循环
var alldivs = document.getElementsByTagName('div'); 
for (var i = 0; i < alldivs.length; i++) {
  document.body.appendChild(document.createElement('div'));
}  

//slow
function collectionGlobal() {
var coll = document.getElementsByTagName('div'), 
len = coll.length,
name = '';
for (var count = 0; count < len; count++) {
 name = document.getElementsByTagName('div')[count].nodeName; 
 name = document.getElementsByTagName_r('div')[count].nodeType; 
 name = document.getElementsByTagName_r('div')[count].tagName;
}
 return name; 
};

// faster
function collectionLocal() {
var coll = document.getElementsByTagName('div'), len = coll.length,
name = '';
for (var count = 0; count < len; count++) {
name = coll[count].nodeName; name = coll[count].nodeType; name = coll[count].tagName;
}
return name;
};

//fastest
function collectionNodesLocal() {
var coll = document.getElementsByTagName_r('div'), len = coll.length,
name = '',
el = null;
for (var count = 0; count < len; count++) {
el = coll[count]; name = el.nodeName; name = el.nodeType; name = el.tagName;
}
 return name;
};  

重绘和重排版
当浏览器下载完所有HTML标记,javascript、css,图片之后,它解析文件并创建两个内部数据结构:一个DOM树表示页面结构和一颗渲染树表示DOM节点如何显示。渲染树中为每个需要显示的DOM树节点存放至少一个节点。渲染树的节点称为”框”或者”盒”,符合CSS模型定义,将页面元素看作一个具有填充、边距、边框和位置的盒子。一旦DOM树和渲染数构造完毕,浏览器就可以显示(绘制)页面上的元素了。
当DOM改变影响到元素的几何属性(宽和高)-例如改变了边框高度或者在段落中添加文字,将发生一系列后续动作。浏览器需要重新计算元素的几何属性,而且其他元素的几何属性和位置也会因此改变受到影响。浏览器是渲染数上受到影响的部分失效,然后重构渲染数。这个过程称为重排版,重排版完成时,浏览器在一个重绘进程中重新绘制屏幕上受到影响的部分。
不是所有的DOM改变都会影响几何尺寸。例如,改变一个元素的背景颜色不会影响它的宽度和高度。在这种情况下,只需重绘不需要重排版,因为元素的布局没有改变。
重绘和重排版是负担很重的操作,可能导致网页应用的用户界面失去响应。

以下操作会发生重排版:
添加、删除可见的DOM元素、元素位置改变、元素尺寸改变(边距、填充、边框高度等属性改变)、内容改变、浏览器窗口尺寸改变
因为计算量与每次重排版有关,大多数浏览器通过队列化修改和批量显示优化重排版过程。然后获取布局信息的操作将导致刷新队列的动作。offsetTop、offsetLeft、offsetWidth、offsetHeight、scrollTop、clientTop。改变风格时最好不用使用这些属性。任何一个访问都将刷新渲染队列。

最小化重绘和重排版

//改变风格
var el = document.getElementById('mydiv'); 
el.style.borderLeft = '1px'; 
el.style.borderRight = '2px';
el.style.padding = '5px';

这里改变了三个风格属性,每次改变都影响到元素的几何属性。在这个例子中,它导致浏览器chong排版了三次。一个达到同样效果而效率更高的方法是:将所有改变合并在一起执行,只修改DOM一次。可以通过使用cssText属性实现:

 var el = document.getElementById('mydiv');
 el.style.cssText = 'border-left: 1px; border-right: 2px; padding: 5px;';

另一个一次性改变风格的方法是修改css的类名称,而不是改变内联风格代码。

批量修改DOM
有三种基本方法可以将DOM元素从文档中摘除:
1、隐藏元素,进行修改,然后再显示它
2、使用一个文档片段在已存在DOM元素之外创建一个子数,然后将它拷贝到文档中。
3、将原始元素拷贝到一个脱离文档的节点中,修改副本,然后覆盖原始元素。

高性能Javascirpt之数据访问

第二章:数据访问

在Javascript中有四种基本的数据位置:直接量、变量、数组项、对象成员,对一种数据存储位置都具有特定的读写操作负担。总的来说,对直接量和局部变量的访问速度要快于数组项和对象成员的访问速度。
作用域链和标识符解析
函数内部的[[Scope]]属性包含了一个函数被创建的作用域中对象的集合。此集合称为函数的作用域链,它决定哪些数据可由函数访问。此函数作用域链中的每个对象被称为一个可变对象。

function add(num1,num2){
    var sum = num1 + num2;
    return sum;
}   

当add()函数创建后,它的作用域链中填入了一个单独的可变对象,此全局对象代表了所有全局范围定义的变量。此全局变量包含了诸如窗口、浏览器和文档之类的访问接口。
函数作用域链将在运行时用到。如 var total = add(5,10);运行此add函数时,建立一个内部对象,称作”运行期上下文”。一个运行期上下文定义了一个函数运行时的环境。对函数的每次运行而言,每个运行时的上下文都是独一的,所以多次调用同一个函数就会导致多次创建运行期上下文。当函数执行完毕,运行期上下文就会被销毁。

一个运行期上下文有它自己的作用域链,用于标识符解析。当运行期上下文被创建时,它的作用域链被初始化,连同运行函数的[[Scope]]属性中所包含的对象。这些值按照它们出现在函数中的顺序,被复制到运行期上下文的作用域链中。这项工作一旦完成,一个被称作”激活对象”的新对象就为运行期上下文创建好了。
在函数运行过程中,每遇到一个变量,标识符识别过程要决定从哪里获得或者存储数据,此过程搜索运行期上下文的作用域链,查找同名的标识符。搜索工作从运行函数的激活目标之作用域链的前端开始。如果找到后,直接返回,若没有找到则进入作用域链的下一个对象。
在运行期上下文的作用域链中,一个标识符所处的位置越深,它的读写速度就越慢。所以,函数中局部变量的访问速度总是最快的,而全局变量的访问通常是最慢的。全局变量总是处于运行期上下文作用域链的最后一个位置。

function initUI(){
var bd = document.body,
links = document.getElementsByTagName_r("a"),
 i = 0,
len = links.length;
while(i < len){
update(links[i++]); }
    document.getElementById("go-btn").onclick = function(){ start();
};
bd.className = "active"; 
}  

此函数包含三个对document的引用,document是一个全局对象、搜素此变量,必须遍历整个作用域链,直到最后在全局变量对象中找到它。你可以通过这种方法减轻重复的全局变量访问对象性能的影响:首先将全局变量的引用存储在一个局部变量中,然后使用整个局部变量代替全局变量。

function initUI(){
var doc = document,
bd = doc.body,
links = doc.getElementsByTagName_r("a"),
i = 0,
len = links.length; while(i < len){
update(links[i++]); 
}
    doc.getElementById("go-btn").onclick = function(){ start();
};
    bd.className = "active"; 
}  

改变作用域链
一般来说,一个运行期上下文的作用域链不会被改变。但是,有两种表达式可以在运行期临时改变运行期的上下文作用域链。第一个是with表达式。
with表达式为所有对象属性创建一个默认操作变量。在其他语言中通常用来避免书写一些重复代码。当代码流执行到一个with表达式时,运行期上下文的作用域链被临时改变了。一个新的可变对象将创建,它包含指定对象的所有属性。此对象被插入到作用域链的前端,意味着现在函数的所有局部变量都被推入第二个作用域链对象中,所以访问代价更高了。
在Javascript中不只是with表达式人为地改变运行期上下文的作用域链,try-catch表达式的catch子句具有相同的效果。当try块发生错误时,程序流程自动转入catch块,并将异常对象推入作用域链前端的一个可变对象中。在catch块中,函数的所有局部变量现在被放在第二个作用域链对象中。例如:

try { 
    methodThatMightCauseAnError();
} catch (ex){
    alert(ex.message); //scope chain is augmented here
}

只要catch子句执行完毕,作用域链就会返回到原来的状态。在程序中可以通过精简代码的办法最小化catch子句对性能的影响。一个很好的模式是将错误交给一个专用函数来处理。例子如下:

try { 
    methodThatMightCauseAnError();
} catch (ex){
    handleError(ex);//delegate to handler method
}  

handleError()函数是catch子句中运行的唯一代码。此函数以适当的方法自由地处理错误,并接收由错误产生的异常对象。由于只有一条语句,没有局部变量访问,作用域链临时改变就不会影响代码的性能。

动态作用域
无论是with表达式还是try-catch表达式的catch子句,以及包含()的函数,都被认为是动态作用域。一个动态作用域只因代码运行而存在,因此无法通过静态分析(查看代码结构)来确定。

闭包、作用域和内存
闭包是Javascript最强大的一个方面,它允许函数访问局部变量之外的数据。

function assignEvents(){
    var id = "xdi9592";
    document.getElementById("save-btn").onclick = function(event){
       saveDocument(id); 
    };
}   

assignEvents函数为一个DOM元素指定了一个事件处理句柄。此事件句柄是一个闭包,当assignEvents()执行时创建,可以访问其范围内部的id变量。用这种方法封闭了对id变量的访问,必须创建一个特殊的作用域链。
在assignEvents()被执行时,一个激活对象被创建,并包含了一些应有的内容,其中包含id变量。它将成为运行期上下文作用域链上的第一个对象,全局对象是第二个。当闭包创建时,[[Scope]]属性与这些对象一起被初始化。
由于闭包的[[Scope]]属性包含与运行期上下文作用域链相同的对象引用,会产生副作用。通常,一个函数的激活对象与运行期上下文一起销毁。当涉及闭包时,激活对象就无法销毁,因为引用仍存在与闭包的[[Scope]]属性中。这意味着脚本中的闭包和非闭包函数相比,需要更多的内存开销。
当闭包被执行时,一个运行期上下文被创建,它的作用域链与[[Scope]]中引用的两个相同的作用域链同时被初始化,然后一个新的激活对象为闭包自身被创建。

对象成员
对象可以有两种类型的成员:实例成员和原形成员。实例成员直接存于实例自身,而原形成员则从对象原形继承。
一般来说,如果在同一个函数中你要多次读取同一个对象属性,最好将它存入一个局部变量。以局部变量替代属性,避免多余的属性查找带来性能开销。

Fork me on GitHub