《高性能JavaScript》--读书笔记

第一章 加载和运行

延迟脚本 defer

该属性表明脚本在执行期间不会影响到页面的构造,脚本会先下载但被延迟到整个页面都解析完毕后再运行.只适用于外部脚本

<script src="js/test.js" defer></script>
<div>123</div>
<script>
    alert('script');
    window.onload = function(){
        alert('loaded');
    }
</script>
<!-- 输出顺序 123 script defer loaded   -->

异步脚本 async

只适用于外部脚本,告诉浏览器立即下载脚本,同时不影响页面的解析,先下载完先执行,没有一定的顺序,但一定会在页面load事件前执行

总结

有几种方法可以减少JavaScript 对性能的影响:

  • 将所有<script>标签放置在页面的底部,紧靠</body>的上方。此法可以保证页面在脚本
    运行之前完成解析。
  • 将脚本成组打包。页面的<script>标签越少,页面的加载速度就越快,响应也更加迅速。不论外部脚本文件还是内联代码都是如此。
  • <script>标签添加defer属性
  • 动态创建<script>元素,用它下载并执行代码
  • 用XHR 对象下载代码,并注入到页面中
  • The YUI 3 approach
  • The LazyLoad library
  • The LABjs library

第二章 数据访问

在JavaScript 中有四种基本的数据访问位置:

  • 直接量 直接量仅仅代表自己,而不存储于特定位置。JavaScript 的直接量包括:字符串,数字,布尔值,对象,数组,函数,正则表达式,具有特殊意义的空值,以及未定义。
  • 变量 开发人员使用var 关键字创建用于存储数据值
  • 数组项 具有数字索引,存储一个JavaScript 数组对象。
  • 对象成员 具有字符串索引,存储一个JavaScript 对象。

总的来说,直接量和局部变量的访问速度要快于数组项和对象成员的访问速度。

标识符解析过程

此过程搜索运行期上下文的作用域链,查找同名的标识符。搜索工作从运行函数的激活目标之作用域链的前端开始。如果找到了,那么就使用这个具有指定标识符的变量;如果没找到,搜索工作将进入作用域链的下一个对象。此过程持续运行,直到标识符被找到,或者没有更多对象可用于搜索,这种情况下标识符将被认为是未定义的。

标识符识别性能

在运行期上下文的作用域链中,一个标识符所处的位置越深,它的读写速度就越慢。所以,函数中局部变量的访问速度总是最快的,而全局变量通常是最慢的(优化的JavaScript引擎在某些情况下可以改变这种状况)。请记住,全局变量总是处于运行期上下文作用域链的最后一个位置,所以总是最远才能触及的。
你可以通过这种方法减轻重复的全局变量访问对性能的影响:首先将全局变量的引用存储在一个局部变量中,然后使用这个局部变量代替全局变量。

改变作用域链

with 表达式
当代码流执行到一个with 表达式时,运行期上下文的作用域链被临时改变了。一个新的可变对象将被创建,它包含指定对象的所有属性。此对象被插入到作用域链的前端,意味着现在函数的所有局部变量都被推入第二个作用域链对象中,所以访问代价更高了。
try-catch 表达式的catch 子句
当try 块发生错误时,程序流程自动转入catch 块,并将异常对象推入作用域链前端的一个可变对象中。在catch块中,函数的所有局部变量现在被放在第二个作用域链对象中。

闭包,作用域,内存

通常,一个函数的激活对象与运行期上下文一同销毁。当涉及闭包时,激活对象就无法销毁了,因为引用仍然存在于闭包的[[Scope]]属性中。这意味着脚本中的闭包与非闭包函数相比,需要更多内存开销。

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

注意闭包中使用的两个标识符,id 和saveDocument,存在于作用域链第一个对象之后的位置上。这是闭包最主要的性能关注点:你经常访问一些范围之外的标识符,每次访问都导致一些性能损失。
关于域外变量的处理建议,减轻对运行速度的影响:将常用的域外变量存入局部变量中,然后直接访问局部变量。

对象成员

每深入原形链一层都会增加性能损失。记住,搜索实例成员的过程比访问直接量或者局部变量负担更重,所以增加遍历原形链的开销正好放大了这种效果。
由于对象成员可能包含其它成员,例如不太常见的写法window.location.href这种模式。每遇到一个点号,JavaScript 引擎就要在对象成员上执行一次解析过程。成员嵌套越深,访问速度越慢。
一般来说,如果在同一个函数中你要多次读取同一个对象属性,最好将它存入一个局部变量。以局部变量替代属性,避免多余的属性查找带来性能开销。在处理嵌套对象成员时这点特别重要

总结

在JavaScript中,数据存储位置可以对代码整体性能产生重要影响。有四种数据访问类型:直接量,变量,数组项,对象成员。它们有不同的性能考虑。

  • 直接量和局部变量访问速度非常快,数组项和对象成员需要更长时间。
  • 局部变量比域外变量快,因为它位于作用域链的第一个对象中。变量在作用域链中的位置越深,访问所需的时间就越长。全局变量总是最慢的,因为它们总是位于作用域链的最后一环。
  • 避免使用with 表达式,因为它改变了运行期上下文的作用域链。而且应当小心对待try-catch 表达式的catch子句,因为它具有同样效果。
  • 嵌套对象成员会造成重大性能影响,尽量少用。
  • 一个属性或方法在原形链中的位置越深,访问它的速度就越慢。

一般来说,你可以通过这种方法提高JavaScript 代码的性能:将经常使用的对象成员,数组项,和域外变量存入局部变量中。然后,访问局部变量的速度会快于那些原始变量。

第三章 DOM 编程

DOM 访问和修改

访问或修改元素最坏的情况是使用循环执行此操作,特别是在HTML 集合中使用循环。

function innerHTMLLoop() {
    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;
}

innerHTML 与DOM 方法比较

它们的性能如何?答案是:性能差别不大,但是,在所有浏览器中,innerHTML 速度更快一些,除
了最新的基于WebKit 的浏览器(Chrome 和Safari)。

HTML 集合

HTML 集合是用于存放DOM 节点引用的类数组对象。下列函数的返回值就是一个集合:

  • document.getElementsByName()
  • document.getElementsByTagName()
  • document.getElementsByClassName()
  • document.images 页面中所有的<img>元素
  • document.links 所有的<a>元素
  • document.forms 所有表单
  • document.forms[0].elements 页面中第一个表单的所有字段

这些方法和属性返回HTMLCollection 对象,是一种类似数组的列表。它们不是数组(因为它们没有诸
如push()或slice()之类的方法),但是提供了一个length属性,和数组一样你可以使用索引访问列表中的元素。
HTML 集合实际上在查询文档,当你更新信息时,每次都要重复执行这种查询操作。例如读取集合中元
素的数目(也就是集合的length),这正是低效率的来源。
优化的办法很简单,只要将集合的length 属性缓存到一个变量中,然后在循环判断条件中使用这个变

function loopCacheLengthCollection() {
    var coll = document.getElementsByTagName('div'),
    len = coll.length;
    for (var count = 0; count < len; count++) {
        //more code
    }
}

一般来说,对于任何类型的DOM 访问,如果同一个DOM 属性或方法被访问一次以上,最好使用一个
局部变量缓存此DOM成员。当遍历一个集合时,第一个优化是将集合引用存储于局部变量,并在循环之外缓存length 属性。然后,如果在循环体中多次访问同一个集合元素,那么使用局部变量缓存它。

DOM 漫谈

querySelectorAll()
IE8+此函数不返回HTML集合,所以返回的节点不呈现文档的“存在性结构”。这就避免了本章前面提到的HTML 集合所固有的性能问题(以及潜在的逻辑问题)。

查询并刷新渲染树改变

  • offsetTop, offsetLeft, offsetWidth, offsetHeight
  • scrollTop, scrollLeft, scrollWidth, scrollHeight
  • clientTop, clientLeft, clientWidth, clientHeight
  • getComputedStyle() (currentStyle in IE)

当你查询布局信息如偏移量、滚动条位置,或风格属性时,浏览器刷新队列并执行所有修改操作,以返回最新的数值。最好是尽量减少对布局信息的查询次数,查询时将它赋给局部变量,并用局部变量参与计算。

最小化重绘和重排版

重排版和重绘代价昂贵,所以,提高程序响应速度一个好策略是减少此类操作发生的机会。为减少发生次数,你应该将多个DOM 和风格改变合并到一个批次中一次性执行。
当你需要对DOM 元素进行多次修改时,你可以通过以下步骤减少重绘和重排版的次数:

  • 从文档流中摘除该元素
  • 对其应用多重改变
  • 将元素带回文档中

常用方法:

  1. 隐藏元素,进行修改,然后再显示它。
    var ul = document.getElementById('mylist');
    ul.style.display = 'none';
    appendDataToElement(ul, data);
    ul.style.display = 'block';
  2. 使用一个文档片断在已存DOM 之外创建一个子树,然后将它拷贝到文档中。
    var fragment = document.createDocumentFragment();
    appendDataToElement(fragment, data);
    document.getElementById('mylist').appendChild(fragment);
  3. 将原始元素拷贝到一个脱离文档的节点中,修改副本,然后覆盖原始元素。

推荐尽可能使用文档片断(第二种解决方案)因为它涉及最少数量的DOM 操作和重排版

将元素提出动画流

使用以下步骤可以避免对大部分页面进行重排版:

  1. 使用绝对坐标定位页面动画的元素,使它位于页面布局流之外。
  2. 启动元素动画。当它扩大时,它临时覆盖部分页面。这是一个重绘过程,但只影响页面的一小部分,避免重排版并重绘一大块页面。
  3. 当动画结束时,重新定位。

事件托管

当页面中存在大量元素,而且每个元素有一个或多个事件句柄与之挂接(例如onclick)时,可能会影
响性能
一个简单而优雅的处理DOM 事件的技术是事件托管。它基于这样一个事实:事件逐层冒泡总能被父元
素捕获。采用事件托管技术之后,你只需要在一个包装元素上挂接一个句柄,用于处理子元素发生的所有事件。

总结

为减少DOM 编程中的性能损失,请牢记以下几点:

  • 最小化DOM 访问,在JavaScript 端做尽可能多的事情。
  • 在反复访问的地方使用局部变量存放DOM 引用.
  • 小心地处理HTML 集合,因为他们表现出“存在性”,总是对底层文档重新查询。将集合的length 属性缓存到一个变量中,在迭代中使用这个变量。如果经常操作这个集合,可以将集合拷贝到数组中。
  • 如果可能的话,使用速度更快的API,诸如querySelectorAll()和firstElementChild。
  • 注意重绘和重排版;批量修改风格,离线操作DOM 树,缓存并减少对布局信息的访问。
  • 动画中使用绝对坐标,使用拖放代理。
  • 使用事件托管技术最小化事件句柄数量。

第四章 算法和流程控制

循环性能

在JavaScript 提供的四种循环类型中,只有一种循环比其他循环明显要慢:for-in 循环。
由于每次迭代操作要搜索实例或原形的属性,for-in 循环每次迭代都要付出更多开销。因此推荐的做法如下:
除非你需要对数目不详的对象属性进行操作,否则避免使用for-in 循环其他循环类型性能相当,难以确定哪种循环更快。选择循环类型应基于需求而不是性能。

基于函数的迭代

items.forEach(function(value, index, array){
    process(value);
});

尽管基于函数的迭代显得更加便利,它还是比基于循环的迭代要慢一些。

条件表达式

if-else 与switch 比较

大多数情况下switch 表达式比if-else 更快,但只有当条件体数量很大时才明显更快。两者间
的主要性能区别在于:当条件体增加时,if-else 性能负担增加的程度比switch更多。因此,我们的自然倾向认为条件体较少时应使用if-else 而条件体较多时应使用switch 表达式,如果从性能方面考虑也是正确的。

优化if-else

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

总结

  • for,while,do-while 循环的性能特性相似,谁也不比谁更快或更慢。
  • 除非你要迭代遍历一个属性未知的对象,否则不要使用for-in 循环。
  • 一般来说,switch 总是比if-else 更快,但并不总是最好的解决方法。
  • 当判断条件较多时,查表法比if-else 或者switch 更快。
  • 如果你遇到一个栈溢出错误,将方法修改为一个迭代算法或者使用制表法可以避免重复工作。

第五章 字符串和正则表达式

字符串连接

加和加等于操作
str += "one" + "two";
str = str + "one" + "two";

这就避免了使用临时字符串,因为赋值表达式开头以str为基础,一次追加一个字符串,从左至右依次
连接。

Array.prototype.join

str = [“a”,“b”,“c”].join(“”);

String.prototype.concat

str = str.concat(s1, s2, s3);

正则表达式优化

第六章 响应接口

浏览器UI 线程

浏览器限制

这是一个有必要的限制,确保恶意代码编写者不能通过无尽的密集操作锁定用户浏览器或计算机。此类限制有两个:调用栈尺寸限制和长时间脚本限制。

用定时器让出时间片

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

setTimeout第二个参数指出什么时候应当将任务添加到UI 队列之中,并不是说那时代码将被执行。

分解任务

如果函数运行时间太长,它可以拆分成一系列更小的步骤,把独立方法放在定时器中调用。你可以将每个函数都放入一个数组,然后使用前一节中提到的数组处理模式:

function saveDocument(id){
    var tasks = [openDocument, writeText, closeDocument, updateUI];
    setTimeout(function(){
        //execute the next task
        var task = tasks.shift();
        task(id);
        //determine if there's more
        if (tasks.length > 0){
            setTimeout(arguments.callee, 25);
        }
    }, 25);
}
限时运行代码

可通过原生的Date 对象跟踪代码的运行时间。这是大多数JavaScript 分析工具所采用的工作方式:

var start = +new Date(),
stop;
someLongProcess();
stop = +new Date();
if(stop-start < 50){
    alert("Just about right.");
} else {
    alert("Taking too long.");
}

Web Workers

工人线程的运行环境由下列部分组成:

  • 一个浏览器对象,只包含四个属性:appName, appVersion, userAgent, 和platform
  • 一个location 对象(和window 里的一样,只是所有属性都是只读的)
  • 一个self 对象指向全局工人线程对象
  • 一个importScripts()方法,使工人线程可以加载外部JavaScript 文件
  • 所有ECMAScript 对象,诸如Object,Array,Data,等等。
  • XMLHttpRequest 构造器
  • setTimeout()和setInterval()方法
  • close()方法可立即停止工人线程

要创建网页工人线程,你必须传入这个JavaScript 文件的URL:

var worker = new Worker("code.js");

工人线程和网页代码通过事件接口进行交互。网页代码可通过postMessage()方法向工人线程传递数据,它接收单个参数,即传递给工人线程的数据。此外,在工人线程中还有onmessage事件句柄用于接收信息。
当工人线程通过importScripts()方法加载外部JavaScript 文件,它接收一个或多个URL参数,指出要加载的JavaScript文件网址。工人线程以阻塞方式调用importScripts(),直到所有文件加载完成并执行之后,脚本才继续运行。由于工人线程在UI线程之外运行,这种阻塞不会影响UI响应。

实际用途

网页工人线程适合于那些纯数据的,或者与浏览器UI 没关系的长运行脚本。它看起来用处不大,而网
页应用程序中通常有一些数据处理功能将受益于工人线程,而不是定时器。
任何超过100毫秒的处理,都应当考虑工人线程方案是不是比基于定时器的方案更合适。当然,还要基
于浏览器是否支持工人线程。

总结

有效地管理UI 线程就是要确保JavaScript 不能运行太长时间,以免影响用户体验。最后,请牢记如下几点:

  • JavaScript运行时间不应该超过100毫秒。过长的运行时间导致UI更新出现可察觉的延迟,从而对整体用户体验产生负面影响。
  • 定时器可用于安排代码推迟执行,它使得你可以将长运行脚本分解成一系列较小的任务。
  • 网页工人线程是新式浏览器才支持的特性,它允许你在UI线程之外运行JavaScript代码而避免锁定UI。

第七章 Ajax

在现代高性能JavaScript 中使用的三种技术是XHR,动态脚本标签插入和多部分的XHR。

XMLHttpRequest

var url = '/data.php';
var params = ['id=934875','limit=20'];
var req = new XMLHttpRequest();
req.onreadystatechange = function() {
    if (req.readyState === 4) {
        var responseHeaders = req.getAllResponseHeaders(); // Get the response headers.
        var data = req.responseText; // Get the data.
        // Process the data here...
    }
}
req.open('GET', url + '?' + params.join('&'), true);
req.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); // Set a request header.
req.send(null); // Send the request.

动态脚本标签插入

var scriptElement = document.createElement('script');
scriptElement.src = 'http://any-domain.com/javascript/lib.js';
document.getElementsByTagName('head')[0].appendChild(scriptElement);

多部分XHR

这里介绍最新的技术,多部分XHR(MXHR)允许你只用一个HTTP 请求就可以从服务器端获取多个
资源。它通过将资源(可以是CSS 文件,HTML 片段,JavaScript代码,或base64编码的图片)打包成一个由特定分隔符界定的大字符串,从服务器端发送到客户端。
使用此技术有一些缺点,其中最大的缺点是以此方法获得的资源不能被浏览器缓存

第八章 编程实践

避免二次评估

大多数情况下,没必要使用eval_r()或Function(),如果可能的话,尽量避免使用它们。至于另外两个函数,setTimeout()和setInterval(),建议第一个参数传入一个函数而不是一个字符串。

使用对象/数组直接量

位操作运算符

位与& 两个操作数的位都是1,结果才是1
位或| 有一个操作数的位是1,结果就是1
位异^或 两个位中只有一个1,结果才是1
位非~ 遇0返回1,反之亦然

for (var i=0, len=rows.length; i < len; i++){
    if (i & 1) {  //判断奇偶数
        className = "odd";
    } else {
        className = "even";
    }
}

总结

  • 通过避免使用eval_r()和Function()构造器避免二次评估。此外,给setTimeout()和setInterval()传递函数参数而不是字符串参数。
  • 创建新对象和数组时使用对象直接量和数组直接量。它们比非直接量形式创建和初始化更快。
  • 避免重复进行相同工作。当需要检测浏览器时,使用延迟加载或条件预加载。
  • 当执行数学远算时,考虑使用位操作,它直接在数字底层进行操作。
  • 原生方法总是比JavaScript 写的东西要快。尽量使用原生方法。

第九章 构建和部署高性能JavaScript 应用

第十章 工具