RN-《高性能JavaScript》

第一章 加载和执行

脚本位置

当浏览器遇到 <script> 标签时,浏览器会停止处理页面,先执行 JavaScript 代码,然后继续解析和渲染页面,同样的情况也发生在使用 src 属性加载 JavaScript 的过程中,浏览器必须先花时间下载外链文件中代码,然后解析并执行,在这个过程中,页面渲染和用户交互是完全被阻塞的。

浏览器在解析到 <body> 标签之前,不会渲染页面的任何部分。

推荐将所有的 <script> 标签尽可能放到 <body> 标签的底部,以尽量减少对整个页面下载的影响。

组织脚本

浏览器在解析HTML页面的过程中每遇到一个 <script> 标签,都会因脚本而导致一定的延时,因此最小化延时时间将会明显改善页面的总体性能。

因此下载单个 100KB 的文件将比下载 4 个 25KB 的文件更快。也就是说,减少页面中外链脚本文件的数量将会改善性能。

异步加载js

在页面加载完成后才加载JavaScript代码。

三种方式

  1. defer 异步加载,但要等到 dom 文档全部解析完才会被执行。只有IE能用,也可以将代码写到内部。
  2. async 异步加载,dom 文档加载完就执行,async只能加载外部脚本,不能把 Javascript 代码写到 <script> 标签里。
  3. 创建 script ,插入到DOM中,加载完毕后callback

动态加载

第三种技术的重点在于:无论在何时启动下载,文件的下载和执行过程不会阻塞页面其他进程。

如果要求不是太高,那么就可以不用动态加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function loadScript(url, callback) {
let script = document.createElement('script');
script.type = "text/javascript";
if (script.readyState) {
script.onreadystatechange = function () { //IE
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreadystatechange = null;
// 只要其中一个状态触发,就删除事件处理器(以确保事件不会处理两次)
callback();
}
}
} else {
script.onload = function () {
// 确保加载完成后才能执行回调函数,如果没有这个保证的话,代码执行非常快,在没有下载完成的情况下执行callback会报错。
callback();
}
}
script.src = url;
document.head.appendChild(script);
// 通常来讲,把新创建的<script>标签添加到<head>标签里比添加到<body>里更保险
}
loadScript('b.js', function () {
test();
});

b.js代码,可以正常调用。

1
2
3
function test() {
console.log("b.js执行了");
}

如果像下面这样调用的话,会报错,Uncaught ReferenceError: test is not defined,原因就是执行的时候浏览器根据执行上下文不知道 test 是什么,因此要传入一个匿名函数,在匿名函数里面执行 test

1
loadScript('b.js', test);

最终写法:

脚本位置放在 </body> 之前,确保 JavaScript 执行过程不会阻碍页面其他内容的显示,然后进行动态加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<h1>hhhh</h1>
<script>
function loadScript(url, callback) {
let script = document.createElement('script');
script.type = "text/javascript";
if (script.readyState) {
script.onreadystatechange = function () { //IE
if (script.readyState == "loaded" || script.readyState == "complete") {
script.onreadystatechange = null;
// 只要其中一个状态触发,就删除事件处理器(以确保事件不会处理两次)
callback();
}
}
} else {
script.onload = function () {
// 确保加载完成后才能执行回调函数,如果没有这个保证的话,代码执行非常快,在没有下载完成的情况下执行callback会报错。
callback();
}
}
script.src = url;
document.head.appendChild(script);
// 通常来讲,把新创建的<script>标签添加到<head>标签里比添加到<body>里更保险
}
loadScript('b.js', function () {
test();
});
</script>
</body>

</html>

第二章 数据存取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function initUI() {
let bd = document.body, // 1
links = document.getElementsByTagName("a"), // 2
i = 0,
len = links.length;

while (i < len) {
update(links[i++]);
}
document.getElementById("go-btn").onclick = function () { // 3
start();
}
bd.className = "active";
}

上面的函数中引用了三次 document,而 document 是个全局对象。搜索该变量的过程必须遍历整个作用域链,直到最后在全局变量对象中找到。因此可以先将全局变量的引用存储在一个局部变量中,然后使用这个局部变量代替全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function initUI() {
let doc = document,
bd = doc.body, // 1
links = doc.getElementsByTagName("a"), // 2
i = 0,
len = links.length;

while (i < len) {
update(links[i++]);
}
doc.getElementById("go-btn").onclick = function () { // 3
start();
}
bd.className = "active";
}

第三章 DOM编程

减少访问DOM的次数

访问DOM元素是有代价的,修改元素则更为昂贵,因为它会导致浏览器重新计算页面的几何变化。

最坏的情况是在循环中访问或修改元素,尤其是对HTML元素集合循环操作。例如下面这段代码:

1
2
3
4
5
function innerHTMLLoop() {
for(let i = 0; i < 10000; i ++) {
document.getElementById('here').innerHTML += "a";
}
}

上面的循环函数修改页面元素的内容的问题在于每次循环,该元素都被访问两次:一次读取 innerHTML 属性值,另一次重写它。

可以这样改进:

1
2
3
4
5
6
7
function innerHTMLLoop() {
let content = "";
for(let i = 0; i < 10000; i ++) {
content += "a";
}
document.getElementById('here').innerHTML += content;
}

可以很直观地体验到速度的变快。

通用的经验法则是:减少访问DOM的次数,把运算尽量留在ECMAScript这一端处理

重绘与重排

浏览器下载完页面中的所有组件–HTML标记、JS、CSS、图片–之后会解析并生成两个内部数据结构:

  • DOM 树:表示页面结构
  • 渲染树:表示DOM节点如何显示

重排是什么:重新生成布局。当DOM 的变化影响了元素的几何属性(宽和高)–比如改变边框宽度或给段落增加文字导致行数增加–浏览器需要重新计算元素的几何属性,同样其他元素的几何属性和位置也会因此受到影响。浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。这个过程称为重排。

重绘是什么:重新绘制。完成重排后,浏览器会重新绘制受影响的部分到屏幕中。这个过程称为重绘。

重排一定会导致重绘,重绘不一定导致重排。如果DOM变化不影响几何属性(比如改变一个元素的背景色并不影响它的宽和高),则只发生一次重绘(不需要重排),因为元素的布局没有改变。

更多内容可以参考这篇文章:https://juejin.im/post/5c15f797f265da61141c7f86

第四章 算法和流程控制

使用递归的时候,尽量结合缓存。

如果遇到栈溢出错误,可以将方法改为迭代算法。

基于函数的迭代,比如 forEach,性能远不如 for 循环。

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

未看

第六章 快速响应的用户界面

这一章第一部分其实就是利用 setTimeout 可以处理复杂的任务,不再进行记录,可以查看 JavaScript 忍者秘籍13章的 setTimeout 部分。

第二部分 Web Workers 未看

第七章 Ajax

介绍了几种数据格式,现在主流的就是 JSON

第八章 编程实践

延时加载

1
2
3
4
5
6
7
8
// 未改进之前
function addHandler(target, eventType, handler) {
if (target.addEventListener) {
target.addEventListener(eventType, handler, false);
} else { // IE
target.attachEvent("on" + eventType, handler);
}
}

上面代码的问题就是:如果第一次调用 addHandler() 时就确定 addEventListener() 是存在的,那么随后每次调用时它应该也都存在,每次调用一个函数都重复相同的工作是一种浪费,有几种方法可以避免它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function addHandler(target, eventType, handler) {
// 复写现有函数
if (target.addEventListener) {
addHandler = function (target, eventType, handler) {
target.addEventListener(eventType, handler, false);
}
} else { // IE
addHandler = function (target, eventType, handler) {
target.attachEvent("on" + eventType, handler);
}
}
// 调用新函数
addHandler(target, eventType, handler);
}

上面这种方法就是延时加载,第一次调用时,会先检测并决定使用哪种方法去绑定事件处理器,然后原始函数被包含正确操作的新函数覆盖,最后一步调用新的函数,随后每次调用 addHandler() 都不会再做检测,因为检测代码已经被新的函数覆盖。

当一个函数在页面中不会立即调用时,延时加载是最好的选择。

条件预加载

这种方式会在脚本加载期间提前检测,而不会等到函数被调用。检测的操作依然只有一次,只是它在过程中来得更早,例如:

1
2
3
4
5
6
7
let addHandler = document.body.addEventListener ? 
function (target, eventType, handler) {
target.addEventListener(eventType, handler, false);
}:
function (target, eventType, handler) {
target.attachEvent("on" + eventType, handler);
};

条件预加载确保所有函数调用消耗的时间相同。其代价是需要在脚本加载时就检测,而不是加载后。预加载适用于一个函数马上就要被用到,并且在整个页面的生命周期中频繁出现的场合。

位操作

有好几种方法来利用位操作符提升JavaScript的速度。首先是使用位运算代替纯数学操作。比如通常采用对 2 取模运算实现表格行颜色交替,例如:

1
2
3
4
5
6
7
8
for(let i = 0, len = rows.length; i < len; i ++) {
if (i % 2) {
className = "even";
} else {
className = "odd";
}
// 增加class
}

如果你看到 32 位数字的二进制底层表示,会发现偶数的最低位是 0,奇数的最低位是 1。让给定数与数字 1 进行按位与运算,如果此数为偶数,那么它和 1 进行按位与运算的结果是 0,如果此数为奇数,那么它和 1 进行按位与运算的结果就是 1。

1
2
3
4
5
6
7
8
for(let i = 0, len = rows.length; i < len; i ++) {
if (i & 1) {
className = "even";
} else {
className = "odd";
}
// 增加class
}

原生方法

当原生方法可用时,尽量使用它们。特别是数学运算(Math)和DOM操作。这样用编译后的代码做越多的事情,你的代码就会越快。

第九章 第十章

这两章感觉没什么参考价值了

第五章:未看

第六章的第二部分 Web Workers 未看