
2025-04-08 日报 Day150

今日的鸡汤
现在的你,是十年前你的决定,十年后的你,是现在的你的决定。种一棵树,最好是十年前,其次是现在。想要改变,从此刻开始,一切还不晚。星光不问赶路人,时光不负有心人,愿十年后的今天不为虚度年华而悔恨,所得皆所愿。我们各自努力,顶峰相见。
今日学习内容
1、JS 红皮书 P636-650 第二十章:JavaScript API
今日笔记
1、 Page Visibility API: Web 开发中一个常见的问题是开发者不知道用户什么时候真正在使用页面。如果页面被最小化或隐藏在其他标签页后面,那么轮询服务器或更新动画等功能可能就没有必要了。Page Visibility API 旨在为开发者提供页面对用户是否可见的信息。
这个 API 本身非常简单,由 3 部分构成。
document.visibilityState 值,表示下面 4 种状态之一。
页面在后台标签页或浏览器中最小化了。
页面在前台标签页中。
实际页面隐藏了,但对页面的预览是可见的(例如在 Windows 7 上,用户鼠标移到任务栏图标上会显示网页预览)。
页面在屏外预渲染。
visibilitychange 事件,该事件会在文档从隐藏变可见(或反之)时触发。
document.hidden 布尔值,表示页面是否隐藏。这可能意味着页面在后台标签页或浏览器中被最小化了。这个值是为了向后兼容才继续被浏览器支持的,应该优先使用 document.visibilityState检测页面可见性。
要想在页面从可见变为隐藏或从隐藏变为可见时得到通知,需要监听 visibilitychange 事件。
document.visibilityState 的值是以下三个字符串之一:
“hidden”
“visible”
“prerender”
2、Streams API: Streams API 是为了解决一个简单但又基础的问题而生的:Web 应用如何消费有序的小信息块而不是大块信息?这种能力主要有两种应用场景。
大块数据可能不会一次性都可用。网络请求的响应就是一个典型的例子。网络负载是以连续信息包形式交付的,而流式处理可以让应用在数据一到达就能使用,而不必等到所有数据都加载完毕。
大块数据可能需要分小部分处理。视频处理、数据压缩、图像编码和 JSON 解析都是可以分成小部分进行处理,而不必等到所有数据都在内存中时再处理的例子。
- 理解流: 提到流,可以把数据想像成某种通过管道输送的液体。JavaScript 中的流借用了管道相关的概念,因为原理是相通的。根据规范,“这些 API 实际是为映射低级 I/O 原语而设计,包括适当时候对字节流的规范化”。Stream API 直接解决的问题是处理网络请求和读写磁盘。
Stream API 定义了三种流。
可读流:可以通过某个公共接口读取数据块的流。数据在内部从底层源进入流,然后由消费者(consumer)进行处理。
可写流:可以通过某个公共接口写入数据块的流。生产者(producer)将数据写入流,数据在内部传入底层数据槽(sink)。
转换流:由两种流组成,可写流用于接收数据(可写端),可读流用于输出数据(可读端)。这两个流之间是转换程序(transformer),可以根据需要检查和修改流内容。
块、内部队列和反压:
流的基本单位是块(chunk)。块可是任意数据类型,但通常是定型数组。
前面提到的各种类型的流都有入口和出口的概念。有时候,由于数据进出速率不同,可能会出现不匹配的情况。为此流平衡可能出现如下三种情形。
流出口处理数据的速度比入口提供数据的速度快。流出口经常空闲(可能意味着流入口效率较低),但只会浪费一点内存或计算资源,因此这种流的不平衡是可以接受的。
流入和流出均衡。这是理想状态。
流入口提供数据的速度比出口处理数据的速度快。这种流不平衡是固有的问题。此时一定会在某个地方出现数据积压,流必须相应做出处理。
流不平衡是常见问题,但流也提供了解决这个问题的工具。所有流都会为已进入流但尚未离开流的块提供一个内部队列。对于均衡流,这个内部队列中会有零个或少量排队的块,因为流出口块出列的速度与流入口块入列的速度近似相等。这种流的内部队列所占用的内存相对比较小。
如果块入列速度快于出列速度,则内部队列会不断增大。流不能允许其内部队列无限增大,因此它使用反压(backpressure)通知流入口停止发送数据,直到队列大小降到某个既定的阈值之下。这个阈值由排列策略决定,这个策略定义了内部队列可以占用的最大内存,即高水位线(high water mark)。 - 可读流: 可读流是对底层数据源的封装。底层数据源可以将数据填充到流中,允许消费者通过流的公共接口读取数据。
- ReadableStreamDefaultController
来看下面的生成器,它每 1000 毫秒就会生成一个递增的整数:这个生成器的值可以通过可读流的控制器传入可读流。访问这个控制器最简单的方式就是创建ReadableStream 的一个实例,并在这个构造函数的 underlyingSource 参数(第一个参数)中定义start()方法,然后在这个方法中使用作为参数传入的 controller。默认情况下,这个控制器参数是ReadableStreamDefaultController 的一个实例:1
2
3
4
5
6async function* ints() {
// 每 1000 毫秒生成一个递增的整数
for (let i = 0; i < 5; ++i) {
yield await new Promise((resolve) => setTimeout(resolve, 1000, i));
}
}调用控制器的 enqueue()方法可以把值传入控制器。所有值都传完之后,调用 close()关闭流:1
2
3
4
5const readableStream = new ReadableStream({
start(controller) {
console.log(controller); // ReadableStreamDefaultController {}
}
});2、ReadableStreamDefaultReader: 前面的例子把 5 个值加入了流的队列,但没有把它们从队列中读出来。为此,需要一个ReadableStreamDefaultReader 的实例,该实例可以通过流的 getReader()方法获取。调用这个方法会获得流的锁,保证只有这个读取器可以从流中读取值:1
2
3
4
5
6
7
8
9
10
11
12
13
14async function* ints() {
// 每 1000 毫秒生成一个递增的整数
for (let i = 0; i < 5; ++i) {
yield await new Promise((resolve) => setTimeout(resolve, 1000, i));
}
}
const readableStream = new ReadableStream({
async start(controller) {
for await (let chunk of ints()) {
controller.enqueue(chunk);
}
controller.close();
}
});消费者使用这个读取器实例的 read()方法可以读出值:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17async function* ints() {
// 每 1000 毫秒生成一个递增的整数
for (let i = 0; i < 5; ++i) {
yield await new Promise((resolve) => setTimeout(resolve, 1000, i));
}
}
const readableStream = new ReadableStream({
async start(controller) {
for await (let chunk of ints()) {
controller.enqueue(chunk);
}
controller.close();
}
});
console.log(readableStream.locked); // false
const readableStreamDefaultReader = readableStream.getReader();
console.log(readableStream.locked); // true1
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
33async function* ints() {
// 每 1000 毫秒生成一个递增的整数
for (let i = 0; i < 5; ++i) {
yield await new Promise((resolve) => setTimeout(resolve, 1000, i));
}
}
const readableStream = new ReadableStream({
async start(controller) {
for await (let chunk of ints()) {
controller.enqueue(chunk);
}
controller.close();
}
});
console.log(readableStream.locked); // false
const readableStreamDefaultReader = readableStream.getReader();
console.log(readableStream.locked); // true
// 消费者
(async function() {
while(true) {
const { done, value } = await readableStreamDefaultReader.read();
if (done) {
break;
} else {
console.log(value);
}
}
})();
// 0
// 1
// 2
// 3
// 4
- 可写流: 可写流是对底层数据槽的封装。生产者可以通过可写流的公共接口向底层数据槽写入数据。
- 转换流: 转换流用于组合可读流和可写流。数据块在两个流之间的转换是通过 transform()方法完成的。
3、通过管道连接流: 流可以通过管道连接成一串。最常见的用例是使用 pipeThrough()方法把 ReadableStream 接入TransformStream。从内部看,ReadableStream 先把自己的值传给 TransformStream 内部的WritableStream,然后执行转换,接着转换后的值又在新的 ReadableStream 上出现。
4、记时API: 页面性能始终是 Web 开发者关心的话题。Performance 接口通过 JavaScript API 暴露了浏览器内部的度量指标,允许开发者直接访问这些信息并基于这些信息实现自己想要的功能。这个接口暴露在window.performance 对象上。所有与页面相关的指标,包括已经定义和将来会定义的,都会存在于这个对象上。
Performance 接口由多个 API 构成:
High Resolution Time API
Performance Timeline API
Navigation Timing API
User Timing API
Resource Timing API
Paint Timing API - High Resolution Time API:
Date.now()方法只适用于日期时间相关操作,而且是不要求计时精度的操作。在下面的例子中,函数 foo()调用前后分别记录了一个时间戳:
const t0 = Date.now();
foo();
const t1 = Date.now();
const duration = t1 – t0;
console.log(duration);
考虑如下 duration 会包含意外值的情况。
duration 是 0。Date.now()只有毫秒级精度,如果 foo()执行足够快,则两个时间戳的值会相等。
duration 是负值或极大值。如果在 foo()执行时,系统时钟被向后或向前调整了(如切换到夏令时),则捕获的时间戳不会考虑这种情况,因此时间差中会包含这些调整。
High Resolution Time API 定义了window.performance.now(),这个方法返回一个微秒精度的浮点值。因此,使用这个方法先后捕获的时间戳更不可能出现相等的情况。而且这个方法可以保证时间戳单调增长。
const t0 = performance.now();
const t1 = performance.now();
console.log(t0); // 1768.625000026077
console.log(t1); // 1768.6300000059418
const duration = t1 – t0;
console.log(duration); // 0.004999979864805937
performance.now()计时器采用相对度量。这个计时器在执行上下文创建时从 0 开始计时。例如,打开页面或创建工作线程时,performance.now()就会从 0 开始计时。由于这个计时器在不同上下文中初始化时可能存在时间差,因此不同上下文之间如果没有共享参照点则不可能直接比较 performance.now()。performance.timeOrigin 属性返回计时器初始化时全局系统时钟的值。
const relativeTimestamp = performance.now();
const absoluteTimestamp = performance.timeOrigin + relativeTimestamp;
console.log(relativeTimestamp); // 244.43500000052154
console.log(absoluteTimestamp); // 1561926208892.4001 - Performance Timeline API: Performance Timeline API 使用一套用于度量客户端延迟的工具扩展了 Performance 接口。性能度量将会采用计算结束与开始时间差的形式。这些开始和结束时间会被记录为 DOMHighResTimeStamp值,而封装这个时间戳的对象是 PerformanceEntry 的实例。
浏览器会自动记录各种 PerformanceEntry 对象,而使用 performance.mark()也可以记录自定义的 PerformanceEntry 对象。在一个执行上下文中被记录的所有性能条目可以通过 performance. getEntries()获取:
console.log(performance.getEntries());
// [PerformanceNavigationTiming, PerformanceResourceTiming, … ]
这个返回的集合代表浏览器的性能时间线(performance timeline)。每个 PerformanceEntry 对象都有 name、entryType、startTime 和 duration 属性:
const entry = performance.getEntries()[0];
console.log(entry.name); // “https://foo.com “
console.log(entry.entryType); // navigation
console.log(entry.startTime); // 0
console.log(entry.duration); // 182.36500001512468
不过,PerformanceEntry 实际上是一个抽象基类。所有记录条目虽然都继承 PerformanceEntry,但最终还是如下某个具体类的实例:
PerformanceMark
PerformanceMeasure
PerformanceFrameTiming
PerformanceNavigationTiming
PerformanceResourceTiming
PerformancePaintTiming - Navigation Timing API: Navigation Timing API 提供了高精度时间戳,用于度量当前页面加载速度。浏览器会在导航事件发生时自动记录 PerformanceNavigationTiming 条目。这个对象会捕获大量时间戳,用于描述页面是何时以及如何加载的。
下面的例子计算了 loadEventStart 和 loadEventEnd 时间戳之间的差: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
39const [performanceNavigationTimingEntry] = performance.getEntriesByType('navigation');
console.log(performanceNavigationTimingEntry);
// PerformanceNavigationTiming {
// connectEnd: 2.259999979287386
// connectStart: 2.259999979287386
// decodedBodySize: 122314
// domComplete: 631.9899999652989
// domContentLoadedEventEnd: 300.92499998863786
// domContentLoadedEventStart: 298.8950000144541
// domInteractive: 298.88499999651685
// domainLookupEnd: 2.259999979287386
// domainLookupStart: 2.259999979287386
// duration: 632.819999998901
// encodedBodySize: 21107
// entryType: "navigation"
// fetchStart: 2.259999979287386
// initiatorType: "navigation"
// loadEventEnd: 632.819999998901
// loadEventStart: 632.0149999810383
// name: " https://foo.com "
// nextHopProtocol: "h2"
// redirectCount: 0
// redirectEnd: 0
// redirectStart: 0
// requestStart: 7.7099999762140214
// responseEnd: 130.50999998813495
// responseStart: 127.16999999247491
// secureConnectionStart: 0
// serverTiming: []
// startTime: 0
// transferSize: 21806
// type: "navigate"
// unloadEventEnd: 132.73999997181818
// unloadEventStart: 132.41999997990206
// workerStart: 0
// }
console.log(performanceNavigationTimingEntry.loadEventEnd –
performanceNavigationTimingEntry.loadEventStart);
// 0.805000017862767 - Resource Timing API: Resource Timing API 提供了高精度时间戳,用于度量当前页面加载时请求资源的速度。浏览器会在加载资源时自动记录 PerformanceResourceTiming。这个对象会捕获大量时间戳,用于描述资源加载的速度。
下面的例子计算了加载一个特定资源所花的时间:通过计算并分析不同时间的差,可以更全面地审视浏览器加载页面的过程,发现可能存在的性能瓶颈。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
29const performanceResourceTimingEntry = performance.getEntriesByType('resource')[0];
console.log(performanceResourceTimingEntry);
// PerformanceResourceTiming {
// connectEnd: 138.11499997973442
// connectStart: 138.11499997973442
// decodedBodySize: 33808
// domainLookupEnd: 138.11499997973442
// domainLookupStart: 138.11499997973442
// duration: 0
// encodedBodySize: 33808
// entryType: "resource"
// fetchStart: 138.11499997973442
// initiatorType: "link"
// name: "https://static.foo.com/bar.png",
// nextHopProtocol: "h2"
// redirectEnd: 0
// redirectStart: 0
// requestStart: 138.11499997973442
// responseEnd: 138.11499997973442
// responseStart: 138.11499997973442
// secureConnectionStart: 0
// serverTiming: []
// startTime: 138.11499997973442
// transferSize: 0
// workerStart: 0
// }
console.log(performanceResourceTimingEntry.responseEnd –
performanceResourceTimingEntry.requestStart);
// 493.9600000507198
5、Web组件: 这里所说的 Web 组件指的是一套用于增强 DOM 行为的工具,包括影子 DOM、自定义元素和 HTML 模板。这一套浏览器 API 特别混乱。 - HTML模版: 在 Web 组件之前,一直缺少基于 HTML 解析构建 DOM 子树,然后在需要时再把这个子树渲染出来的机制。一种间接方案是使用 innerHTML 把标记字符串转换为 DOM 元素,但这种方式存在严重的安全隐患。另一种间接方案是使用 document.createElement()构建每个元素,然后逐个把它们添加到孤儿根节点(不是添加到 DOM),但这样做特别麻烦,完全与标记无关。
相反,更好的方式是提前在页面中写出特殊标记,让浏览器自动将其解析为 DOM 子树,但跳过渲染。这正是 HTML 模板的核心思想,而标签正是为这个目的而生的。下面是一个简单的HTML 模板的例子:I'm inside a template!
使用DocumentFragment: DocumentFragment 也是批量向 HTML 中添加元素的高效工具。比如,我们想以最快的方式给某个 HTML 元素添加多个子元素。如果连续调用 document.appendChild(),则不仅费事,还会导致多次布局重排。而使用 DocumentFragment 可以一次性添加所有子节点,最多只会有一次布局重排:
1 | // 开始状态: |
使用标签: 注意,在前面的例子中,DocumentFragment 的所有子节点都高效地转移到了 foo 元素上,转移之后 DocumentFragment 变空了。同样的过程也可以使用标签重现:
1 | const fooElement = document.querySelector('#foo'); |
如果想要复制模板,可以使用 importNode()方法克隆 DocumentFragment:
1 | const fooElement = document.querySelector('#foo'); |
模版脚本: 脚本执行可以推迟到将 DocumentFragment 的内容实际添加到 DOM 树。下面的例子演示了这个过程:
1 | // 页面 HTML: |