[js] 《高性能JavaScript》 读书笔记
Loading and Execution 加载和运行
一般来说浏览器中有多种线程:UI渲染线程、javascript引擎线程、浏览器事件触发线程、HTTP请求线程等。多线程之间会共享运行资源,浏览器的js会操作dom,影响渲染,所以js引擎线程和UI渲染线程是互斥的,导致执行js时会阻塞页面的渲染。
- 将
<script>
标签放在尽可能接近<body>
标签底部的位置,尽量减少对页面下载的影响。 - 将JavaScript脚本文件合并打包,减少http请求。
Nonblocking Scripts 无阻塞脚本
无阻塞脚本的意义在于在页面加载完成后才加载javascript代码。(window对象的load事件触发后)
Deferred Scripts 延期脚本
<script src="script.js"></script>
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。
<script async src="script.js"></script>
有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。
<script defer src="myscript.js"></script>
有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。
XMLHttpRequest Script Injection XHR 脚本注入
由于JavaScript的同源策略,脚本文件必须和页面放置在同一个域内,不能通过CDN下载,不常见。
var xhr = new XMLHttpRequest();
xhr.open("get","file.js",true);
xhr.onreadystatechange = function() {
if(xht.readyState === 4) {
if(xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
var script = document.createElement("script");
script.type = "text/javascript";
script.text = xhr.responseText;
document.body.appendChild(script);
}
}
};
xhr.send(null);
Dynamic Script Elements 动态脚本元素
创建一个script元素,指定src属性,然后在页面加载完成之后添加到页面的任何地方。
function loadScript(url, callback) {
var script = document.createElement("script") script.type = "text/javascript";
if (script.readyState) { //IE
script.onreadystatechange = function() {
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreadystatechange = null;
callback();
}
};
} else { //Others
script.onload = function() {
callback();
};
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
}
Recommended Nonblocking Pattern 推荐的非阻塞模式
loadScript("./file1.js", function () {
loadScript("./file2.js", function () {
loadScript("./file3.js", function () {
//do something
})
})
})
Data Acess 数据访问
数据存储在哪里,关系到代码运行期间数据被检索到的速度。
JavaScript中有四种基本的数据存储位置:Literal values(直接量)、Variables(变量)、Array items(数组项)、Object members(对象成员)。
直接量和局部变量的访问性能差异微不足道,性能消耗代价高一些的是全局变量、数组项、对象成员。
Managing Scope 管理作用
每一个javascript函数都表示为一个对象,是Function对象的一个实例。
Function对象同其他对象一样,拥有可以编程访问的属性,和一系列不能通过代码访问而仅供js引擎存取的内部属性。
其中一个内部属性是[[scope]]。
[[scope]]包含一个函数被创建的作用域中对象的集合。
这个集合被称作函数的作用域链,它决定哪些数据能被函数访问。
函数作用域中每个对象被称为一个可变对象,每个可变对象都以“键值对”的形式存在。
当一个函数被创建后,他的作用域链会被创建此函数的作用域中可访问的数据对象所填充。
执行函数时会创建一个称为执行环境(execution context)的内部对象。一个执行环境定义了一个函数执行时的环境。函数每次执行时对应的执行环境都独一无二,所以多次调用同一个函数就会创建多个执行环境。当函数执行完毕,执行环境销毁。
每个执行环境都有自己的作用域链,用于解析标识符。当执行环境被创建时,他的作用域链初始化为当前运行函数的[[scope]]属性中的对象。
在函数执行过程中,每遇到一个变量,就会经历一次标识符解析过程以决定从哪里获取或存储数据。该过程搜索执行环境的作用域链,查找同名的标识符。
标识符解析是有代价的。
在执行环境的作用域链中,一个标识符所在的位置越深,他的读写速度就越慢。而全局变量总在于执行环境作用域链的最末端(性能最差的)。
Object Members 对象成员
JavaScript中一切皆对象,对象的命名成员可以包含任意数据类型,包含函数。对象成员指的就是函数对象,函数对象的访问速度,比直接量和局部变量要慢。
Prototypes 原型
JavaScript中的对象是基于原型的,原型是对象的基础,定义并实现了一个新对象所必须具有的成员。原型对象为所有给定类型的对象实例共享,所有的实例共享原型对象的成员。
一个对象通过一个内部属性绑定到自己的原型,在FF/Safari/Chrome中,这一对象被称为_proto_
,任何时候创建一个内置类型的实例,这些实例将自动拥有一个Object作为他们的原型。
因此一个对象拥有成员可以分为两类:实例成员(own成员)和原型成员。
实例成员直接存在于实例自身,而原型成员则从对象成员继承。
var cat = {
name:"xiaohua",
age:1
}
cat的实例成员就是name和age,原型成员就是cat._proto_中的成员属性,而cat._proto_属性是Object.prototype,在这里就是Object。
Prototype Chains 原型链
对象的原型决定了一个实例的类型,默认情况下,所有对象都是Object的实例,并继承了所有基本方法,当我们使用构造器创建实例时,就创建了另外一种类型的原型。
function Animal(name,age){
this.name = name,
this.age = age
}
Animal.prototype.sayHello = function(){
console.log("Hello,I am a " + this.name);
}
var cat = new Animal("cat",1);
var dog = new Animal("dog",1);
cat是Animal的实例,cat._proto_是Animal.prototype,Animal.prototype._proto_是Object;dog和cat共享一个原型链,但各自拥有自己的实例成员name和age。
如果调用了cat.toString()方法时,搜索路径如下:
cat -> cat._proto_(Animal.prototype) -> cat._proto_.constructor(Animal) -> cat._proto_.Constructor._proto_(Object);
原型链每深入一个层级,就会带来更大的性能消耗,速度也就会更慢。
而对于实例成员的搜索开销本身就大于访问直接量或者是局部变量,因此这种性能消耗还是很值得去优化的。
针对数据访问导致的相关性能问题,主要的解决办法就是对数据进行暂存:
- 将全局变量暂存为局部变量,减少作用域链的深入搜索;
- 将实例的属性暂存,减少对原型链的多次深入搜索;
- 减少使用动态作用域(with或try catch)和闭包。
施工中...