RN-《JavaScript忍者秘籍》(下)

第十章 正则表达式

用到再看

第十一章 代码模块化

AMD

AMD 的设计明确基于浏览器,最流行的实现是 RequireJS,有以下几项优点:

  • 自动处理依赖,无需考虑模块引入的顺序
  • 异步加载模块,避免阻塞
  • 在同一个文件中可以定义多个模块

CommonJS

CommonJS 的设计是面向通用 JavaScript 环境。在 Node.js 社区具有最多的用户,不显示地支持浏览器。

CommonJS 使用基于文件的模块,文件同步加载,每个文件中只能定义一个模块。

CommonJS 在服务端更流行,因为模块加载相对更快,只需要读取文件系统,而在客户端则必须从远程服务器下载文件。

ES6 模块

ES6 模块结合了两者的有点:

  • 与 CommonJs 类似,ES6 模块语法相对简单,并且基于文件(每个文件就是一个模块)
  • 与 AMD 类似,ES6 模块支持异步模块加载

更多 JavaScript模块化

第十二章 DOM 操作

DOM 的特性和属性

当访问元素的特性值时,我们可以使用 DOM 方法: setAttribute getAttribute,或使用 DOM 对象上与之相对应的属性(div.id)。

下面的代码说明并非所有的元素特性都能被属性表示,自定义的特性并不能被元素属性表示,必须通过 setAttribute getAttribute 才能访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div data-a="a"></div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const div = document.querySelector("div");

div.setAttribute("id", "ninja-1");

console.log(div.id === "ninja-1")
console.log(div.getAttribute('id') === "ninja-1")

div.id = "ninja-2";

console.log(div.id === "ninja-2")
console.log(div.getAttribute('id') === "ninja-2")

console.log(div['data-a']) // undefined
console.log(div.getAttribute('data-a')) // a
});
</script>

HTML5 中,建议使用 data- 作为自定义属性的前缀。

获取计算后的属性

元素的 style 属性并不包含它在样式表中继承的样式信息。

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
<style>
div {
font-size: 1.8em;
border: 0 solid gold;
}
</style>

<body>

<div style="color:#000;" title="Ninja power!">
忍者パワー
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
const div = document.querySelector("div");

console.log(div.style.color === 'rgb(0, 0, 0)' || div.style.color === '#000'); // true
console.log(div.style.fontSize === '1.8em'); // false
console.log(div.style.borderWidth === '0'); // false

div.style.borderWidth = "4px";

console.log(div.style.borderWidth === '4px'); // true
});
</script>
</body>

使用 window.getComputedStyle 获取计算后的值,最后的 border 是自己加的,在 computedStyles 中是没有的,但还是打印出了 1px solid rgb(220, 20, 60),正则表达式把驼峰转为中横线分隔,因为 getPropertyValue 只接受中横线分隔。

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
<style type="text/css">
div {
background-color: #ffc;
display: inline;
font-size: 1.8em;
border: 1px solid crimson;
color: green;
}
</style>

<body>
<div style="color:crimson;" id="testSubject" title="Ninja power!">
忍者パワー
</div>
<script>
function fetchComputedStyle(element, property) {
// 获取计算样式的接口,保存在一个变量中稍后引用,这样可以提升性能
const computedStyles = window.getComputedStyle(element);
if (computedStyles) {
// 将驼峰转为中横线分隔
property = property.replace(/([A-Z])/g, '-$1').toLowerCase();
return computedStyles.getPropertyValue(property);
}
}
document.addEventListener("DOMContentLoaded", () => {
const div = document.querySelector("div");
console.log("background-color: " + fetchComputedStyle(div, 'background-color'));
console.log("display: " + fetchComputedStyle(div, 'display'));
console.log("font-size: " + fetchComputedStyle(div, 'fontSize'));
console.log("color: " + fetchComputedStyle(div, 'color'));
console.log("border-top-color: " + fetchComputedStyle(div, 'borderTopColor'));
console.log("border-top-width: " + fetchComputedStyle(div, 'border-top-width'));
console.log("border: " + fetchComputedStyle(div, 'border')); // 自己加的
});
</script>
</body>

测量元素的高度和宽度

height 和 width 这样的 style 属性造成了另外一个特殊问题,在不指定值的情况下,它们的默认值是 auto,以便让元素的大小根据其内容进行决定。因此,除非显式提供特性字符串,不能使用 height 和 width 来获取准确的值的。

值得庆幸的是,offsetHeight和offsetWidth都提供了这样的功能:可以相当可靠地访问实际元素的高度和宽度。但是请注意,这两个属性的值都包含了元素的padding值。

然而,需要当心的是,在高度交互的网站中,元素的隐藏(display值设置为none时),可能会花一些时间,而且一个元素如果不显示的话,它就没有尺寸。在非显示元素上,尝试获取offsetWidth或offsetHeight属性值,结果都是0。

对于这样的隐藏元素,如果需要获取它在非隐藏状态时的尺寸,我们可以使用一个技巧,暂时取消元素的隐藏,然后获取值,然后再将其隐藏。当然,我们希望这种做法不要在视觉上漏出破绽,而是在幕后操作。那如何才能将一个隐藏元素,在不可见的情况下编程不隐藏呢?

具体方法如下:

  1. 将display属性设置为block。
  2. 将visibility设置为hidden。
  3. 将position设置为absolute。
  4. 获取元素尺寸。
  5. 恢复先前更改的属性。

将display属性修改为block,可以让我们获取offsetHeight和offsetWidth的真实值,但元素会变成可见。为了使元素不可见,我们将visibility属性设置为hidden。但是这种做法会导致在元素的位置上显示一片空白,所以我们需要将position属性设置为absolute,以便将元素移出正常的可视区。

检查offsetWidth和offsetHeight属性值是否为0,可以非常有效地确定一个元素的可见性

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
39
40
41
42
43
44
45
46
47
<style type="text/css">
div {
width: 100px;
height: 100px;
display: none;
background: red;
}
</style>

<div id="testSubject">
忍者
</div>
<script>
(function () {
const PROPERTIES = {
position: "absolute",
visibility: "hidden",
display: "block"
};
window.getDimensions = element => {
const previous = {};
for (let key in PROPERTIES) {
previous[key] = element.style[key];
element.style[key] = PROPERTIES[key];
}

const result = {
width: element.offsetWidth,
height: element.offsetHeight
};

for (let key in PROPERTIES) {
element.style[key] = previous[key];
}
return result;
};
})();

const testSubject = document.getElementById('testSubject');
console.log(testSubject.offsetWidth)
console.log(testSubject.offsetHeight)

setTimeout(() => { // 如果是图片的话,需要加上 setTimeout,因为网络请求原因
const after = getDimensions(testSubject)
console.log(after.width)
console.log(after.height)
}, 3000)

避免布局抖动

布局抖动的核心问题在于,每当我们修改DOM时,浏览器必须在读取任何布局信息之前先重新计算布局。这种对性能的损耗十分巨大。

避免布局抖动的一种方法,就是尽量使用不会导致浏览器重排的方式编写代码。

元素的尺寸之间不存在依赖关系时可以进行批量读取和写入,这样可以让浏览器进行批量修改DOM的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
const ninja = document.getElementById("ninja");
const samurai = document.getElementById("samurai");
const ronin = document.getElementById("ronin");

// 批量读取所有的布局属性
const ninjaWidth = ninja.clientWidth;
const samuraiWidth = samurai.clientWidth;
const roninWidth = ronin.clientWidth;

// 批次写入所有的布局属性
ninja.style.width = ninjaWidth/2 + "px";
samurai.style.width = samuraiWidth/2 + "px";
ronin.style.width = roninWidth/2 + "px";

布局抖动对于精简页面无需过分考虑,但是在开发复杂的Web应用程序时需要特别注意,特别是在移动设备上。因此最好能记住所有会引起布局抖动的方法和属性。

第十三章 历久弥新的事件

深入事件循环

宏任务、微任务和事件循环

宏任务(或通常称为任务)。

宏任务的例子很多,包括创建主文档对象、解析HTML、执行主线(或全局)JavaScript代码,更改当前URL以及各种事件,如页面加载、输入、网络事件和定时器事件。从浏览器的角度来看,宏任务代表一个个离散的、独立工作单元。运行完任务后,浏览器可以继续其他调度,如重新渲染页面的UI或执行垃圾回收。

而微任务是更小的任务。微任务更新应用程序的状态,但必须在浏览器任务继续执行其他任务之前执行,浏览器任务包括重新渲染页面的UI。微任务的案例包括promise回调函数、DOM发生变化等。微任务需要尽可能快地、通过异步方式执行,同时不能产生全新的微任务。微任务使得我们能够在重新渲染UI之前执行指定的行为,避免不必要的UI重绘,UI重绘会使应用程序的状态不连续。

事件循环基于两个基本原则:

  1. 一次处理一个任务。
  2. 一个任务开始后直到运行完成,不会被其他任务中断。

如图所示:

两个原则

事件循环通常至少需要两个事件队列:宏任务队列和微任务队列。

细节:

  • 全局来看,图中展示了在一次迭代中,事件循环将首先检查宏任务队列,如果宏任务等待,则立即开始执行宏任务。直到该任务运行完成(或者队列为空),事件循环将移动去处理微任务队列。如果有任务在该队列中等待,则事件循环将依次开始执行,完成一个后执行余下的微任务,直到队列中所有微任务执行完毕。注意处理宏任务和微任务队列之间的区别:单次循环迭代中,最多处理一个宏任务(其余的在队列中等待),而队列中的所有微任务都会被处理

  • 当微任务队列处理完成并清空时,事件循环会检查是否需要更新UI渲染,如果是,则会重新渲染UI视图。至此,当前事件循环结束,之后将回到最初第一个环节,再次检查宏任务队列,并开启新一轮的事件循环。

  • 因为JavaScript基于单线程执行模型,所以这两类任务都是逐个执行的。当一个任务开始执行后,在完成前,中间不会被任何其他任务中断。除非浏览器决定中止执行该任务,例如,某个任务执行时间过长或内存占用过大。所有微任务会在下一次渲染之前执行完成,因为它们的目标是在渲染前更新应用程序状态。浏览器通常会尝试每秒渲染60次页面,以达到每秒60帧(60 fps)的速度。60fps通常是检验体验是否平滑流畅的标准,比方在动画里——这意味着浏览器会尝试在16ms内渲染一帧。需要注意图中所示的“更新渲染”是何时发生在事件循环内的,因为在页面渲染时,任何任务都无法再进行修改。这些设计和原则都意味着,如果想要实现平滑流畅的应用,我们是没有太多时间浪费在处理单个事件循环任务的。理想情况下,单个任务和该任务附属的所有微任务,都应在16ms内完成

因此提醒我们注意事件处理函数的发生频率以及执行耗时。例如,处理鼠标移动(mouse-move)事件时应当特别小心。因为移动鼠标将导致大量的事件进入队列,因此在鼠标移动的处理函数中执行任何复杂操作都可能导致Web应用的糟糕体验。

仅含宏任务的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
<button id="firstButton">First button</button>
<button id="secondButton">Second button</button>
<script>
const firstButton = document.getElementById("firstButton");
const secondButton = document.getElementById("secondButton");
firstButton.addEventListener("click", function firstHandler(){
/*Some click handle code that runs for 8 ms*/
});
secondButton.addEventListener("click", function secondHandler(){
/*Click handle code that runs for 5ms*/
});
/*Code that runs for 15ms*/
</script>

代码需要发挥一些想象空间,避免添加不必要的聚合代码,要求读者想象以下内容。

主线程JavaScript代码执行时间需要15ms。第一个单击事件处理器需要运行8ms。第二个单击事件处理器需要运行5ms。假设一个手快的用户在代码执行后5ms时单击第一个按钮,随后在12ms时单击第二个按钮。

如图所示:

事件

由于JavaScript基于单线程执行模型,单击firstButton并不会立即执行对应的处理器。(记住,一个任务一旦开始执行,就不会被另一个任务中断)firstButton的事件处理器则进入任务队列,等待执行。当单击secondButton时发生类似的情况:对应的事件处理器进入队列,等待执行。注意,事件监测和添加任务是独立于事件循环的,尽管主线程仍在执行,仍然可以向队列添加任务

本示例强调如果其他任务正在执行,那么事件则需要按顺序等待执行。例如,尽管在第12ms时单击secondButton,但是其对应的事件处理任务在第23ms时才开始执行

同时含有宏任务和微任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<button id="firstButton">First button</button>
<button id="secondButton">Second button</button>
<script>
const firstButton = document.getElementById("firstButton");
const secondButton = document.getElementById("secondButton");
firstButton.addEventListener("click", function firstHandler(){
Promise.resolve().then(function(){
/*Some promise handling code that runs for 4 ms*/
});

/*Some click handle code that runs for 8 ms*/
});

secondButton.addEventListener("click", function secondHandler(){
/*Click handle code that runs for 5ms*/
});
/*Code that runs for 15ms*/
</script>

事件

与之前的示例唯一的区别是,在firstHandler代码中我们创建立即兑现的promise,并需要运行4ms的传入回调函数。因为promise表示当前未知的一个未来值,因此promise处理函数总是异步执行。

在本例中,我们创建立即兑现的promise。说实话,JavaScript引擎本应立即调用回调函数,因为我们已知promise成功兑现。但是,为了连续性,JavaScript引擎不会这么做,仍然会在firstHandler代码执行(需要运行8ms)完成之后再异步调用回调函数。通过创建微任务,将回调放入微任务队列。

除了宏任务队列之外,本例重点关注微任务队列,在第12ms时微任务队列仍为空

处理计算复杂度高的任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<table><tbody></tbody></table>
<script>
"use strict";
const tbody = document.querySelector("tbody");
for (let i = 0; i < 60000; i++) {
let tr = document.createElement("tr");
for (let t = 0; t < 6; t++) {
const td = document.createElement("td");
td.appendChild(document.createTextNode(i + "," + t));
tr.appendChild(td);
}
tbody.appendChild(tr);
}
</script>

个人理解的就是创建了这么多的微任务(DOM操作),会使页面在渲染前等待的时间特别长。

上面的代码中,我们创建了240 000个DOM节点,创建一个20 000行、每行6列的表格,表格中的每个单元格都包含一个文本节点。这个操作的消耗是惊人的,会导致浏览器挂起一段时间,这段时间内用户无法正常操作(如同Bruce叔叔主宰了家庭聚会中的谈话)。

我们需要做的就是让Bruce叔叔定期闭嘴,这样其他人才有机会加入谈话。在代码中,我们可以引入定时器来创建这样的“中断谈话”。如下所示:

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
<table><tbody></tbody></table>
<script>
"use strict";

const tbody = document.querySelector("tbody");
const rowCount = 60000;
const divideInto = 4;
const chunkSize = rowCount/divideInto;
let iteration = 0;

const table = document.getElementsByTagName("tbody")[0];
setTimeout(function generateRows(){
const base = chunkSize * iteration;
for (let i = 0; i < chunkSize; i++) {
const tr = document.createElement("tr");
for (let t = 0; t < 6; t++) {
const td = document.createElement("td");
td.appendChild(
document.createTextNode((i + base) + "," + t +
"," + iteration));
tr.appendChild(td);
}
table.appendChild(tr);
}
iteration++;
if (iteration < divideInto)
setTimeout(generateRows, 0);
},0);
</script>

令人印象深刻的是,可以使用异步的方法(setTimeout)来优化代码。在本例中,我们使用0作为超时时间。如果关注事件循环是如何工作的,就会知道这并不意味着将在0ms时执行回调。使用0,意味着通知浏览器尽快执行回调,但仍然必须在UI更新之后执行。

通过这种技术,从用户的角度可察觉的最显著的变化是,一个长时间的浏览器挂起,替代为4次(次数可修改)页面更新。尽管浏览器尝试尽可能快地执行代码片段,但仍然是依次执行DOM渲染。在这段代码的初始版本中,页面更新需要等待很长时间。

个人理解的就是借助setTimeout,将这么多的微任务分解成了四个宏任务,每一段宏任务后面有原来总量四分之一的微任务,这样浏览器渲染前等待的时间就会明显变少了。

事件

除了下面的记录,笔记基本完成

第十章 正则表达式用到再看

第十二章 DOM 注入 HTML ,闭合标签用到再看。

第十三章自定义事件用到再看