高性能JS(读书札记)
第一章:加载和执行
1.1脚本位置
将js脚本放在body底部
1.2组织脚本
文件合并,减少http请求(打包工具)
1.3无阻塞的脚本
js倾向于阻止浏览器的某些处理过程,如http请求和用户界面更新,这是所有开发者面临的最显著的性能问题。
尽管下载单个较大的js文件只产生一次http请求,却会死锁浏览器一大段时间。为避免这种情况,你需要向页面中逐步加载js文件,这样做在某种程度上来说不会阻塞浏览器。
无阻塞脚本的秘诀在于,在页面加载完成后才加载js代码。用专业术语来说,这意味着在window对象的load事件触发后再下载脚本。有很多方式可以做到这一点。
1.3.1延迟的脚本
HTML4为<script>标签添加了defer属性(部分浏览器支持),该属性指明本元素所含脚本不会修改DOM,因此代码能够安全地延迟执行。
1.3.2动态脚本元素
动态加载js文件,无论在何时启动下载,文件的下载和执行过程不会阻塞页面其他进程。
使用动态脚本节点下载文件时,返回的代码通常会立即执行。当脚本“自执行”时,这种机制运行正常。但是当代码只包含供页面其他脚本调用的接口时,你必须跟踪并确保脚本下载完成并准备就绪。可以通过侦听此事件来获得脚本加载完成时的状态:
function loadScript(url ,callback){ var script = documment.creatElement("script") script.type = "text/javascript"; if(script.readystatechange = function(){//IE script.onreadystatechange = function(){ if(script.readystate == "load' || script.readystate == "complete"){ script.onraedystatechange = null; callback(); } }; }else{//其它浏览器 script.onload = function(){ callback(); }; }
script.src = url; document.getElementsByTagName("head")[0].appendChild(script); }
如果需要动态加载多个js文件,则需确保文件加载的顺序。在所有的主流浏览器中,只有firefox和opera能够按照开发人员指定的顺序执行,其他浏览器会按照从服务端还回的顺序下载和执行代码。你可以将下载操作串联起来以确保下载顺序,如
loadScript("file1.js",function(){ loadScript("file2.js,function(){ loadScript("file3.js,function(){ loadScript("file4.js,function(){ alert("All files are loaded!") }); }); }); });
尽管方案可行,但如果需要下载的文件较多,这个方案会带来管理上的麻烦。
如果多个文件下载顺序很重要,更好的做法是把它们按照正确顺序写成一个文件(由于这个过程是异步的,因此大一点的文件不会有影响)。
1.3.3XMLHttpRequest脚本注入
此技术会先创建一个XHR对象,然后用它下载js文件,最后通过动态创建<script>元素将代码注入页面中。
1.3.4推荐的无阻塞模式
先添加动态加载所需的代码,然后加载初始化页面所需的剩下的代码。
第二章 数据访问
js的四种基本数据存取位置
直接量
直接量只代表自身,不存储在特定的位置。有字符串、数字、布尔值、对象、数组、函数、正则表达式以及特殊的null和undefined。
变量
开发人员用关键字var定义的数据储存单元。
数组元素
储存在js数组对象内部,以数字作为索引。
对象成员
储存在js对像内部,以字符串作为索引。
大多数情况下,从一个直接量和一个局部变量中存取数据比访问数组元素和对象成员的代价小些。
2.1管理作用域
2.1.1作用域链和标识符解析
function对象的内部属性[[Scope]]包含了一个函数被创建的作用域中对像的集合,这个集合被称为函数的作用域链。
2.1.2标识符解析的性能
标识符解析是有代价的,事实上没有哪种计算机操作可以不产生性能开销。在运行期上下文的作用域中,一个标识符所在的位置越深,它的读写速度也就越慢。
一个好的经验法则:如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量中。
2.1.3改变作用域链
一般来说,一个运行期上下文的作用域链是不会改变的。但是,有两个语句可以在执行时临时改变作用域链——with和catch子句。
with包含了参数指定对象的所有属性,这个对象被推入所用作用域链的头部,这意味这所有局部变量第二个作用域链对象中,因此访问的代价更高。
把一个异常对象推入一个可变对象并置于作用域的头部。一旦catch子句执行完毕,作用域链就会返回到之前的状态。
2.1.4动态作用域
2.1.5闭包、作用域和内存
因为闭包的[[Scope]]属性包含了与运行上下文作用域链相同的对象的引用,因此会有一项副作用。通常来说,函数的活动对象,会随同运行期上下文一同销毁。但引入闭包时,由于引用仍然存在于闭包的[[Scope]]属性中,因此激活对象无法被销毁。这意味着脚本中的闭包与非闭包相比,需要更多的内存开销。在大幸Web应用中,这可能是个问题,尤其在IE浏览器中需要关注。由于IE使用非原生js对象来实现DOM对象,因此闭包会导致内存泄漏。
2.2对象成员
2.2.1原型
js中的对象是基于原型的。原型是其他对象的基础,定义并实现了一个新对象必须包含的成员列表。这一概念完全不同于传统面向对象编程语言中“类”的概念,“类”定义了创建新对象的过程。而原型对象为所有对象实例共享,因此这些实例也共享了原型对象的成员。
对象可以有两种成员类型:实例成员和原型成员。
可以用hasOwnProperty()方法来判断对象是否包含特定的实例成员,用in操作符来确定对象是否包含特定的属性。
2.2.2原型链
对象的原型决定了实例的类型。默认情况下,所有对象都是对象(Object)的实例,并继承了所有基本方法。
2.2.3嵌套成员
2.2.4缓存对象成员值
由于所有类似的性能问题都与对象成员有关,因此应该尽可能避免使用它们。更确切地说,应当小心,只有在必要时使用对象成员。
如:
function hasEitherClass(element,className1,className2){ return element.className == className1 || element.className == className2; }
换成
function hasEitherClass(element,className1,className2){ var currentClassName = element.className; renturn currentClassName == className1 || currentClassName ==className2; }
js的命名空间是导致频繁访问嵌套属性的起因之一,不要再同一个函数里多次查找同一个对象成员,除非它的值改变了。
第3章 DOM编程
用脚本进行DOM操作的代价很昂贵,它是富Web应用中最常见的性能瓶颈。
三类
1.访问和修改DOM元素
2.修改DOM元素的样式会导致重绘(repaint)和重排(reflow)
3.通过DOM事件处理与用户的交互
3.1浏览器中的DOM
DOM与js是两个相互独立的功能,它们通过接口彼此连接,就会产生消耗。
3.2DOM访问与修改
修改元素会更为昂贵,因为它会导致浏览器重新计算页面的几何变化。
3.2.1innerHTML对比DOM方法
最终选择哪种方式取决于你的用户经常使用的浏览器,以及编码习惯。
3.2.2节点克隆
3.2.3HTML集合
HTML集合是包含了DOM节点引用的类数组对象。
读取一个集合的length比读取普通数组的length要慢很多,因为每次都要重新查询。