《高性能javascript编程》读书笔记

来源:互联网 发布:淘宝直通车没有展现 编辑:程序博客网 时间:2024/05/22 00:52

1. 加载与运行

    <script>标签的出现使整个页面因脚本解析、运行而出现等待。不论实际的
JavaScript 代码是内联的还是包含在一个不相干的外部文件中,页面下载和解析过程必须停下,等待脚本
完成这些处理,然后才能继续。这是页面生命周期必不可少的部分,因为脚本可能在运行过程中修改页面
内容。典型的例子是document.write()函数,例如:

<html><head>    <title>Script Example</title></head><body>    <p>        <script type="text/javascript">            document.write("The date is " + (new Date()).toDateString());        </script>    </p></body></html>

推荐的办法是:将所有<script>标签放在尽可能接近<body>标签底部的位置,尽量减少对整个页面下载的影响

ps. 目前我们的项目中就是把绝大部分<script>标签放在</body> 闭合之前。


每当页面解析碰到<script>标签,就会开始代码的下载和执行(对于外部脚本每个HTTP 请求都会产生额外的性能负)。限制页面的<script>总数也可以改善性能。

比如:将多个外部脚本打包成一个文件。


新的<script>元素加载file1.js 源文件。此文件当元素添加到页面之后立刻开始下载。此技术的重点在于:
无论在何处启动下载,文件的下载和运行都不会阻塞其他页面处理过程。你甚至可以将这些代码放在
<head>部分而不会对其余部分的页面代码造成影响(除了用于下载文件的HTTP 连接)

var script = document.createElement ("script");script.type = "text/javascript";script.src = "file1.js";document.getElementsByTagName_r("head")[0].appendChild(script);

当文件使用动态脚本节点下载时,返回的代码通常立即执行(除了Firefox 和Opera,他们将等待此前的
所有动态脚本节点执行完毕)。当脚本是“自运行”类型时这一机制运行正常,但是如果脚本只包含供页面
其他脚本调用调用的接口,则会带来问题。这种情况下,你需要跟踪脚本下载完成并准备妥善的情况。可
以使用动态<script>节点发出事件得到相关信息。

var script = document.createElement ("script")script.type = "text/javascript";//Firefox, Opera, Chrome, Safari 3+script.onload = function(){    alert("Script loaded!");};script.src = "file1.js";document.getElementsByTagName_r("head")[0].appendChild(script);

2.  DOM 编程

     对DOM 操作代价昂贵,在富网页应用中通常是一个性能瓶颈。

    文档对象模型(DOM)是一个独立于语言的,使用XML 和HTML 文档操作的应用程序接口(API)。

    浏览器通常要求DOM 实现和JavaScript 实现保持相互独立。

    简单说来,两个独立的部分以功能接口连接就会带来性能损耗。


以下函数在循环中更新页面内容。这段代码的问题是,在每次循环单元中都对DOM 元素访问两次:一次
读取innerHTML 属性能容,另一次写入它。

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

一个更有效率的版本将使用局部变量存储更新后的内容,在循环结束时一次性写入

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

使用虽不标准却被良好支持的innerHTML 属性和使用纯DOM 方法性能差别不大,但是,在所有浏览器中,innerHTML 速度更快一些。


昂贵的集合

var alldivs = document.getElementsByTagName_r('div');for (var i = 0; i < alldivs.length; i++) {document.body.appendChild(document.createElement('div'))}
这段代码看上去只是简单地倍增了页面中div 元素的数量。它遍历现有div,每次创建一个新的div 并附
加到body 上面。但实际上这是个死循环,因为循环终止条件alldivs.length 在每次迭代中都会增加,它反

映出底层文档的当前状态。


重绘和重排版

当DOM 改变影响到元素的几何属性(宽和高)——例如改变了边框宽度或在段落中添加文字,将发生
一系列后续动作——浏览器需要重新计算元素的几何属性,而且其他元素的几何属性和位置也会因此改变
受到影响。浏览器使渲染树上受到影响的部分失效,然后重构渲染树。这个过程被称作重排版。重排版完
成时,浏览器在一个重绘进程中重新绘制屏幕上受影响的部分。

不是所有的DOM 改变都会影响几何属性。例如,改变一个元素的背景颜色不会影响它的宽度或高度。
在这种情况下,只需要重绘(不需要重排版),因为元素的布局没有改变。

重排版和重绘代价昂贵,所以,提高程序响应速度一个好策略是减少此类操作发生的机会。为减少发生
次数,你应该将多个DOM 和风格改变合并到一个批次中一次性执行。

var el = document.getElementById('mydiv');el.style.borderLeft = '1px';el.style.borderRight = '2px';el.style.padding = '5px';
一个达到同样效果而效率更高的方法是:将所有改变合并在一起执行,只修改DOM 一次。可通过使用
cssText 属性实现:

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

文档片断

另一种减少重排版次数的方法是:在文档之外创建并更新一个文档片断,然后将它附加在原始列表上。
文档片断是一个轻量级的document 对象,它被设计专用于更新、移动节点之类的任务。文档片断一个便
利的语法特性是当你向节点附加一个片断时,实际添加的是文档片断的子节点群,而不是片断自己。下面
的例子减少一行代码,只引发一次重排版,只触发“存在DOM”一次。

var fragment = document.createDocumentFragment();appendDataToElement(fragment, data);document.getElementById('mylist').appendChild(fragment);

事件托管

当页面中存在大量元素,而且每个元素有一个或多个事件句柄与之挂接(例如onclick)时,可能会影
响性能。连接每个句柄都是有代价的,无论其形式是加重了页面负担(更多的页面标记和JavaScript 代码)
还是表现在运行期的运行时间上。你需要访问和修改更多的DOM 节点,程序就会更慢,特别是因为事件
挂接过程都发生在onload(或DOMContentReady)事件中,对任何一个富交互网页来说那都是一个繁忙的
时间段。挂接事件占用了处理时间,另外,浏览器需要保存每个句柄的记录,占用更多内存。当这些工作
结束时,这些事件句柄中的相当一部分根本不需要(因为并不是100%的按钮或者链接都会被用户点到),
所以很多工作都是不必要的。


一个简单而优雅的处理DOM 事件的技术是事件托管。它基于这样一个事实:事件逐层冒泡总能被父元
素捕获。采用事件托管技术之后,你只需要在一个包装元素上挂接一个句柄,用于处理子元素发生的所有
事件。


3. 算法和流程控制

在JavaScript 提供的四种循环类型中,只有一种循环比其他
循环明显要慢:for-in 循环。

不言而喻,如果一次循环迭代需要较长时间来执行,那么多次循环将需要更长时间。限制在循环体内进
行耗时操作的数量是一个加快循环的好方法。

for (var i=items.length; i--; ){    process(items[i]);}

达夫设备

即使循环体中最快的代码,累计迭代上千次(也将是不小的负担)。此外,每次运行循环体时都会产生
一个很小的性能开销,也会增加总的运行时间。减少循环的迭代次数可获得显著的性能提升。最广为人知
的限制循环迭代次数的模式称作“达夫设备”。


var iterations = Math.floor(items.length / 8),startAt = items.length % 8,i = 0;do {switch(startAt){case 0: process(items[i++]);case 7: process(items[i++]);case 6: process(items[i++]);case 5: process(items[i++]);case 4: process(items[i++]);case 3: process(items[i++]);case 2: process(items[i++]);case 1: process(items[i++]);}startAt = 0;} while (--iterations);

条件表达式

使用if-else 或者switch 的流行理论是基于测试条件的数量:条件数量较大,倾向于使用switch 而不是
if-else。这通常归结到代码的易读性。这种观点认为,如果条件较少时,if-else 容易阅读,而条件较多时switch
更容易阅读。

事实证明,大多数情况下switch 表达式比if-else 更快,但只有当条件体数量很大时才明显更快。两者间
的主要性能区别在于:当条件体增加时,if-else 性能负担增加的程度比switch 更多。

一般来说,if-else 适用于判断两个离散的值或者判断几个不同的值域。如果判断多于两个离散值,switch
表达式将是更理想的选择。


优化if-else

优化if-else 的目标总是最小化找到正确分支之前所判断条件体的数量。最简单的优化方法是将最常见的
条件体放在首位。

另外一种减少条件判断数量的方法是将if-else 组织成一系列嵌套的if-else 表达式。使用一个单独的一长
串的if-else 通常导致运行缓慢,因为每个条件体都要被计算。


4. 字符串连接

加和加等于操作

这些操作符提供了连接字符串的最简单方法,事实上,除IE7 和它之前的所有现代浏览器都对此优化得
很好,所以你不需要寻找其他方法。然而,有些技术可以最大限度地提高这些操作的效率。

str += "one" + "two";
此代码执行时,发生四个步骤:
1. A temporary string is created in memory.
内存中创建了一个临时字符串。
2. The concatenated value "onetwo" is assigned to the temporary string.
临时字符串的值被赋予“onetwo”。
3. The temporary string is concatenated with the current value of str.
临时字符串与str 的值进行连接。
4. The result is assigned to str.
结果赋予str。


下面的代码通过两个离散表达式直接将内容附加在str 上避免了临时字符串(上面列表中第1 步和第2
步)。在大多数浏览器上这样做可加快10%-40%:

str += "one";str += "two";

实际上,你可以用一行代码就实现这样的性能提升,如下:

str = str + "one" + "two";// equivalent to str = ((str + "one") + "two")

这就避免了使用临时字符串,因为赋值表达式开头以str 为基础,一次追加一个字符串,从左至右依次
连接。如果改变连接顺序(例如,str = "one" + str + "two"),你会失去这种优化。这与浏览器合并字符串
时分配内存的方法有关。除IE 以外,浏览器尝试扩展表达式左端字符串的内存,然后简单地将第二个字
符串拷贝到它的尾部。如果在一个循环中,基本字符串位于最左端,就可以避免多次复制一
个越来越大的基本字符串。

这些技术并不适用于IE。它们几乎没有任何作用,在IE8 上甚至比IE7 和早期版本更慢。这与IE 执行
连接操作的机制有关。

在IE8 中,连接字符串只是记录下构成新字符串的各部分字符串的引用。在最后时
刻(当你真正使用连接后的字符串时),各部分字符串才被逐个拷贝到一个新的“真正的”字符串中,然后
用它取代先前的字符串引用,所以并非每次使用字符串时都发生合并操作。

IE7 和更早的浏览器在连接字符串时使用更糟糕的实现方法,每连接一对字符串都要把它们复制到一块
新分配的内存中。

数组联结

Array.prototype.join 方法将数组的所有元素合并成一个字符串,并在每个元素之间插入一个分隔符字符
串。如果传递一个空字符串作为分隔符,你可以简单地将数组的所有元素连接起来。

在大多数浏览器上,数组联结比连接字符串的其他方法更慢,但是事实上,为一种补偿方法,在IE7 和
更早的浏览器上它是连接大量字符串唯一高效的途径。

var str = "I'm a thirty-five character string.",strs = [],newStr,appends = 5000;while (appends--) {strs[strs.length] = str;}newStr = strs.join("");

原生字符串连接函数接受任意数目的参数,并将每一个参数都追加在调用函数的字符串上。这是连接字
符串最灵活的方法,因为你可以用它追加一个字符串,或者一次追加几个字符串,或者一个完整的字符串
数组。

不幸的是,大多数情况下concat 比简单的+和+=慢一些,而且在IE,Opera 和Chrome 上大幅变慢。


5. 响应接口

浏览器UI 线程

JavaScript 和UI 更新共享的进程通常被称作浏览器UI 线程(虽然对所有浏览器来说“线程”一词不一定
准确)。此UI 线程围绕着一个简单的队列系统工作,任务被保存到队列中直至进程空闲。一旦空闲,队
列中的下一个任务将被检索和运行。这些任务不是运行JavaScript 代码,就是执行UI 更新,包括重绘和重
排版。此进程中最令人感兴趣的部分是每次输入均导致一个或多个任务被加入队列。


浏览器限制

浏览器在JavaScript 运行时间上采取了限制。这是一个有必要的限制,确保恶意代码编写者不能通过无
尽的密集操作锁定用户浏览器或计算机。此类限制有两个:调用栈尺寸限制和长时间脚
本限制。长运行脚本限制有时被称作长运行脚本定时器或者失控脚本定时器,但其基本思想是浏览器记录
一个脚本的运行时间,一旦到达一定限度时就终止它。当此限制到达时,浏览器会向用户显示一个对话框。

如果整整几秒钟对JavaScript 运行来说太长了,那么什么是适当的时间?事实证明,即使一秒钟对脚本
运行来说也太长了。一个单一的JavaScript 操作应当使用的总时间(最大)是100 毫秒。这个数字根据Robert
Miller 在1968 年的研究。有趣的是,可用性专家Jakob Nielsen 在他的著作《可用性工程》(Morgan Kaufmann,
1944)上注释说这一数字并没有因时间的推移而改变,而且事实上在1991 年被Xerox-PARC(施乐公司的
帕洛阿尔托研究中心)的研究中重申。

Nielsen 指出如果该接口在100 毫秒内响应用户输入,用户认为自己是“直接操作用户界面中的对象。”
超过100 毫秒意味着用户认为自己与接口断开了。由于UI 在JavaScript 运行时无法更新,如果运行时间长
于100 毫秒,用户就不能感受到对接口的控制。


用定时器让出时间片

定时器与UI 线程交互的方式有助于分解长运行脚本成为较短的片断。调用setTimeout()或setInterval()
告诉JavaScript 引擎等待一定时间然后将JavaScript 任务添加到UI 队列中。

请记住,定时器代码只有等创建它的函数运行完成之后,才有可能被执行。例如,如果前面的代码中定
时器延时变得更小,然后在创建定时器之后又调用了另一个函数,定时器代码有可能在onclick 事件处理
完成之前加入队列:

var button = document.getElementById("my-button");button.onclick = function(){oneMethod();setTimeout(function(){    document.getElementById("notice").style.color = "red";}, 50);anotherMethod();};
如果anotherMethod()执行时间超过50 毫秒,那么定时器代码将在onclick 处理完成之前加入到队列中。
其结果是等onclick 处理运行完毕,定时器代码立即执行,而察觉不出其间的延迟。

在任何一种情况下,创建一个定时器造成UI 线程暂停,如同它从一个任务切换到下一个任务。因此,
定时器代码复位所有相关的浏览器限制,包括长运行脚本时间。此外,调用栈也在定时器代码中复位为零。
这一特性使得定时器成为长运行JavaScript 代码理想的跨浏览器解决方案。


定时器精度

在Windows 系统上定时器分辨率为15 毫秒,也就是说一个值为15 的定时器延时将根据最后一次系统
时间刷新而转换为0 或者15。设置定时器延时小于15 将在Internet Explorer 中导致浏览器锁定,所以最小
值建议为25 毫秒(实际时间是15 或30)以确保至少15 毫秒延迟。

此最小定时器延时也有助于避免其他浏览器和其他操作系统上的定时器分辨率问题。大多数浏览器在定
时器延时小于10 毫秒时表现出差异性。

在数组处理中使用定时器

当处理过程不必是同步处理并且数据不必按顺序处理,那么代码将适于使用定时器分解工作

function processArray(items, process, callback){    var todo = items.concat(); //create a clone of the original    setTimeout(function(){        process(todo.shift());        if (todo.length > 0){            setTimeout(arguments.callee, 25);        } else {            callback(items);        }    }, 25);}

网页工人线程

自JavaScript 诞生以来,还没有办法在浏览器UI 线程之外运行代码。网页工人线程API 改变了这种状
况,它引入一个接口,使代码运行而不占用浏览器UI 线程的时间。

由于网页工人线程不绑定UI 线程,这也意味着它们将不能访问许多浏览器资源。JavaScript 和UI 更新
共享同一个进程的部分原因是它们之间互访频繁,如果这些任务失控将导致糟糕的用户体验。网页工人线
程修改DOM 将导致用户界面出错,但每个网页工人线程都有自己的全局运行环境,只有JavaScript 特性
的一个子集可用。工人线程的运行环境由下列部分组成:

一个浏览器对象,只包含四个属性:appName, appVersion, userAgent, 和platform

一个location 对象(和window 里的一样,只是所有属性都是只读的)

一个self 对象指向全局工人线程对象

一个importScripts()方法,使工人线程可以加载外部JavaScript 文件

所有ECMAScript 对象,诸如Object,Array,Data,等等。

XMLHttpRequest 构造器

setTimeout()和setInterval()方法

close()方法可立即停止工人线程


因为网页工人线程有不同的全局运行环境,你不能在JavaScript 代码中创建。事实上,你需要创建一个
完全独立的JavaScript 文件,包含那些在工人线程中运行的代码。要创建网页工人线程,你必须传入这个
JavaScript 文件的URL:

var worker = new Worker("code.js");worker.onmessage = function(event){    alert(event.data);};worker.postMessage("Nicholas");


0 0
原创粉丝点击