【重走JavaScript之高级程序设计】JavaScript Api

JavaScript API

随着 Web 浏览器能力的增加,其复杂性也在迅速增加。从很多方面看,现代 Web 浏览器已经成为构建于诸多规范之上、集不同 API于一身的“瑞士军刀”。浏览器规范的生态在某种程度上是混乱而无序的。一些规范如 HTML5,定义了一批增强已有标准的 API 和浏览器特性。而另一些规范如 WebCryptography APINotifications API,只为一个特性定义了一个 API。不同浏览器实现这些新 API的情况也不同,有的会实现其中一部分,有的则干脆尚未实现。

最终,是否使用这些比较新的 API还要看项目是支持更多浏览器,还是要采用更多现代特性。有些API可以通过腻子脚本来模拟,但腻子脚本通常会带来性能问题,此外也会增加网站 Javascript 代码的体积。

1 Atomics 与 ShareArrayBuffer


多个上下文访问 sharedArrayBuffer 时,如果同时对缓冲区执行操作,就可能出现资源争用问题。Atomics API 通过强制同一时刻只能对缓冲区执行一个操作,可以让多个上下文安全地读写一个 SharedArrayBufferAtomics API是 ES2017 中定义的。仔细研究会发现 Atomics API 非常像一个简化版的指令集架构(ISA),这并非意外。原子操作的本质会排斥操作系统或计算机硬件通常会自动执行的优化(比如指令重新排序)。原子操作也让并发访的内存变得不可能,如果应用不当就可能导致程序执行变慢。为此, Atomics API 的设计初衷是在最少但很稳定的原子行为基础之上,构建复杂的多线程 JavaScript 程序。

1.1 ShareArrayBuffer

SharedArrayBufferArrayBuffer 具有同样的 API。二者的主要区别是 ArrayBuffer 必须在不同执行上下文间切换, SharedArrayBuffer 则可以被任意多个执行上下文同时使用。

在多个执行上下文间共享内存意味着并发线程操作成为了可能。传统JavaScript 操作对于并发内存访问导致的资源争用没有提供保护。下面的例子演示了4 个专用工作线程访问同-个 SharedArrayBuffer 导致的资源争用问题:

const workerScript = `
	self.onmessage = ({data})=> {
		const view = new Uint32Array(data)

		// 执行1 000 000 次加操作
		for(let i = 0;i < 1E6; ++i){
			// 线程不安全加操作会导致资源争用
			view[0] +=1
		}

		self.postMessage(null)
	}
`;

const workerScriptBlobUrl = URL.createObjectURL(new Blob([workerScript]));

// 创建容量为4的工作线程池
const workers = [];
for (let i = 0; i < 4; ++i) {
  workers.push(new Worker(workerScriptBlobUrl));
}

// 在最后一个工作线程完成后打印出最终值
let responseCount = 0;
for (const worker of workers) {
  worker.onmessage = () => {
    if (++responseCount == workers.length) {
      console.log(`final buffer value:${view[0]}`);
    }
  };
}

// 初始化 SharedArrayBuffer
const SharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(SharedArrayBuffer);
view[0] = 1;

// 把 SharedArrayBuffer 发送到每个工作线程
for (const worker of workers) {
  worker.postMessage(SharedArrayBuffer);
}

// (期待结果为 40000001。实际输出可能类似这样)
// Final buffer value: 2145106

为解决这个问题,Atomics API 应运而生。Atomics API 可以保证 SharedArrayBuffer 上的JavaScript操作是线程安全的。注意 SharedArrayBuffer API等同于 ArrayBuffer API。


1.2 原子操作基础

任何全局上下文中都有 Atomics 对象,这个对象上暴露了用于执行线程安全操作的一套静态方法,其中多数方法以一个 TypedArray 实例(一个 SharedArrayBuffer 的引用) 作为第一个参数,以相关操作数作为后续参数。

1.2.1 算术及位操作方法

Atomics API 提供了一套简单的方法用以执行就地修改操作。在 ECMA 规范中,这些方法被定义为 AtomicReadModifyWrite 操作。在底层,这些方法都会从 SharedArravBuffer 中某个位置读取值,然后执行算术或位操作,最后再把计算结果写回相同的位置。这些操作的原子本质意味着上述读取、修改、写回操作会按照顺序执行,不会被其他线程中断。

以下代码演示了所有算术方法:

// 创建大小为 1 的缓冲区
let sharedArrayBuffer = new SharedArrayBuffer(1);

// 基于缓冲创建 Uint8Array
let typedArray = new Uint8Array(sharedArrayBuffer);

// 所有ArrayBuffer 全部初始化为 0
console.log(typedArray); // Uint8Array[0]

const index = 0;
const increment = 5;
// 对索引0处的值执行原子 加 5
Atomics.add(typedArray, index, increment);
console.log(typedArray); // Uint8Array[5]

// 对索引0处的值执行原子 减 5
Atomics.sub(typedarray, index, increment);
console.log(typedArray); // Uint8Array[0]

以下代码演示了所有位方法:

// 创建大小为1 的缓冲区
let sharedArrayBuffer = new SharedArrayBuffer(1);

// 基于缓冲创建 Uint8Array
let typedArray = new Uint8array(sharedArrayBuffer);

// 所有 ArrayBuffer 全部初始化为 0
onsole.log(typedarray); // Uint8Array[0]
const index = 0;
// 对索引0处的值执行原子或 0b1111
Atomics.or(typedArray, index, 0b1111);
console.log(typedArray); // Uint8Array[15]

// 对索引0处的值执行原子与 0b1111
Atomics.and(typedArray, index, 0b1100);
console.log(typedArray); // uint8Array[12]

// 对索引0处的值执行原子异或 0b1111
Atomics.xor(typedArray, index, 0b1111);
console.log(typedArray); // Uint8Array[3]

前面线程不安全的例子可以改写为下面这样:

const workerScript = `
	self.onmessage = ({data})=> {
		const view = new Uint32Array(data)

		// 执行1 000 000 次加操作
		for(let i = 0;i < 1E6; ++i){
			// 线程安全的加操作
			Atomics.add(view, 0, 1)
		}

		self.postMessage(null)
	}
`;

const workerScriptBlobUrl = URL.createObjectURL(new Blob([workerScript]));

// 创建容量为4的工作线程池
const workers = [];
for (let i = 0; i < 4; ++i) {
  workers.push(new Worker(workerScriptBlobUrl));
}

// 在最后一个工作线程完成后打印出最终值
let responseCount = 0;
for (const worker of workers) {
  worker.onmessage = () => {
    if (++responseCount == workers.length) {
      console.log(`final buffer value:${view[0]}`);
    }
  };
}

// 初始化 SharedArrayBuffer
const SharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(SharedArrayBuffer);
view[0] = 1;

// 把 SharedArrayBuffer 发送到每个工作线程
for (const worker of workers) {
  worker.postMessage(SharedArrayBuffer);
}

// (期待结果为 40000001。实际输出可能类似这样)
// Final buffer value: 40000001

1.2.2 原子读和写

浏览器的 JavaScript 编译器和 CPU 架构本身都有权限重排指令以提升程序执行效率。正常情况下,JavaScript 的单线程环境是可以随时进行这种优化的。但多线程下的指令重排可能导致资源争用,而且极难排错。

Atomics API通过两种主要方式解决了这个问题。

  • 所有原子指令相互之间的顺序永远不会重排。
  • 使用原子读或原子写保证所有指令(包括原子和非原子指令)都不会相对原子读/写重新排序。这意味着位于原子读/写之前的所有指令会在原子读/写发生前完成,而位于原子读/写之后的所有指令会在原子读/写完成后才会开始。

除了读写缓冲区的值, Atomics.load()Atomics.store() 还可以构建“代码围栏”。JavaScrip引擎保证非原子指令可以相对于 load()store() 本地重排,但这个重排不会侵犯原子读/写的边界。以下代码演示了这种行为:

const sharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(sharedArrayBuffer);

// 执行非原子写
view[0] = 1;

// 非原子写可以保证在这个读操作之前完成,因此这里一定会读到 1
console.log(Atomics.load(view, 0)); // 1

// 执行原子写
Atomics.store(view, 0, 2);

// 非原子读可以保证在原子写完成后发生,因此这里一定会读到 2
console.log(view[0]); // 2

1.2.3 原子交换

为了保证连续、不间断的先读后写,Atomics API 提供了两种方法: exchange()compareExchange()Atomics.exchange() 执行简单的交换,以保证其他线程不会中断值的交换:

const sharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(sharedArrayBuffer);

// 在索引0处写入3
Atomics.store(view, 0, 3);

// 从索引0处读取值,然后在索引0处写入4
console.log(Atomics.exchange(view, 0, 4)); // 3

// 从索引0处读取值
console.log(Atomics.load(view, 0));

在多线程程序中,一个线程可能只希望在上次读取某个值之后没有其他线程修改该值的情况下才对共享缓冲区执行写操作。如果这个值没有被修改,这个线程就可以安全地写人更新后的值;如果这个值被修改了,那么执行写操作将会破坏其他线程计算的值。对于这种任务, Atomics API 提供了 compareExchange() 方法。这个方法只在目标索引处的值与预期值匹配时才会执行写操作。来看下面这个例子:

const sharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(sharedArrayBuffer);

// 在索引0处写入5
Atomics.store(view, 0, 5);
// 从缓冲区读取值
let initial = Atomics.load(view, 0);

// 对这个值执行非原子操作
let result = initial ** 2;

// 只在缓冲区未被修改的情况下才会向缓冲区写入新值
Atomics.compareExchange(view, 0, initial, result);

// 检查写入成功
console.log(Atomics.load(view, 0)); // 25

如果值不匹配,compareExchange() 调用则什么也不做:

const sharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(sharedArrayBuffer);

// 在索引0处写入5
Atomics.store(view, 0, 5);
// 从缓冲区读取值
let initial = Atomics.load(view, 0);

// 对这个值执行非原子操作
let result = initial ** 2;

// 只在缓冲区未被修改的情况下才会向缓冲区写入新值
Atomics.compareExchange(view, 0, -1, result);

// 检查写入失败
console.log(Atomics.load(view, 0)); // 5

1.2.4 原子Futex 操作与加锁

如果没有某种锁机制,多线程程序就无法支持复杂需求。为此, Atomics API 提供了模仿 Linux Futex (快速用户空间互斥量,fastuser-space mutex) 的方法。这些方法本身虽然非常简单,但可以作为更复杂锁机制的基本组件。

注意 所有原子 Futex 操作只能用于 Int32Array 视图。而且,也只能用在工作线程内部。

Atomics.wait()Atomics.notify() 通过示例很容易理解。下面这个简单的例子创建了 4个工作线程,用于对长度为 1 的 Int32Array 进行操作。这些工作线程会依次取得锁并执行自己的加操作:

const workerScript = `
	self.onmessage = ({data})=> {
		const view = new Uint32Array(data)

		console.log("Waiting to obtain lock")

		Atomics.wait(view, 0, 0, 1E5)

		console.log("Obtained lock")

		// 在索引 0 处 加 1
		Atomics.add(view, 0, 1)

		console.log("Releasing lock")

		// 只允许 1 个工作线程继续执行
		Atomics.notify(view, 0, 1)

		self.postMessage(null)
	}
`;

const workerScriptBlobUrl = URL.createObjectURL(new Blob([workerScript]));

// 创建容量为4的工作线程池
const workers = [];
for (let i = 0; i < 4; ++i) {
  workers.push(new Worker(workerScriptBlobUrl));
}

// 在最后一个工作线程完成后打印出最终值
let responseCount = 0;
for (const worker of workers) {
  worker.onmessage = () => {
    if (++responseCount == workers.length) {
      console.log(`final buffer value:${view[0]}`);
    }
  };
}

// 初始化 SharedArrayBuffer
const SharedArrayBuffer = new SharedArrayBuffer(4);
const view = new Uint32Array(SharedArrayBuffer);
view[0] = 1;

// 把 SharedArrayBuffer 发送到每个工作线程
for (const worker of workers) {
  worker.postMessage(SharedArrayBuffer);
}

// 1000 毫秒后释放第一个锁
setTimeout(() => {
  Atomics.notify(view, 0, 1);
}, 1000);

// Waiting to obtain lock
// Waiting to obtain lock
// Waiting to obtain lock
// Waiting to obtain lock
// Obtained lock
// Releasing lock
// Obtained lock
// Releasing lock
// Obtained lock
// Releasing lock
// Obtained lock
// Releasing lock
// final buffer value:4

因为是使用 0 来初始化 SharedArrayBuffe r,所以每个工作线程都会到达 Atomics.wait() 并停止执行。在停止状态下,执行线程存在于一个等待队列中,在经过指定时间或在相应索引上调用 Atomics.notify() 之前,一直保持暂停状态。1000 毫秒之后,顶部执行上下文会调用 Atomics.notify() 释放其中一个等待的线程。这个线程执行完毕后会再次调用 Atomics.notify() 释放另一个线程。这个过程会持续到所有线程都执行完毕并通过 postMessage() 传出最终的值。

Atomics API 还提供了 Atomics.isLockFree() 方法。不过我们基本上应该不会用到。这个方法在高性能算法中可以用来确定是否有必要获取锁。规范中的介绍如下:

Atomics.isLockFree() 是一个优化原语。基本上,如果一个原子原语(compareExchange、load、store、add、sub、and、or、xor 或 exchange)在n字节大小的数据上的原子步骤在不调用代理在组成数据的n字节之外获得锁的情况下可以执行,则 Atomics.isLockFree(n) 会返回 true。高性能算法会使用 Atomics.isLockFree 确定是否在关键部分使用锁或原子操作。如果原子原语需要加锁,则算法提供自己的锁会更高效。

Atomics.isLockFree(4) 始终返回 true,因为在所有已知的相关硬件上都是支持的。能够如此假设通常可以简化程序。



2 跨上下文消息


🚀 站内跳转,前端实现跨标签页通信

跨文档消息,有时候也简称为 XDM(cross-document messaging) ,是一种在不同执行上下文(如不同工作线程或不同源的页面)间传递信息的能力。例如, www.wrox.com 上的页面想要与包含在内嵌窗格中的 p2p.wrox.com 上面的页面通信。在 XDM 之前,要以安全方式实现这种通信需要很多工作。XDM以安全易用的方式规范化了这个功能。

XDM 的核心是 postMessage() 方法。除了 XDM,这个方法名还在 HTML5 中很多地方用到过,但目的都一样,都是把数据传送到另一个位置。

postMessage() 方法接收3个参数:消息、表示目标接收源的字符串和可选的可传输对象的数组(只与工作线程相关)。第二个参数对于安全非常重要,其可以限制浏览器交付数据的目标。下面来看一个例子:

let iframewindow = document.getElementById("myframe").contentWindow;
iframeWindow.postMessage("A secret", "http://www.wrox.com");

最后一行代码尝试向内嵌窗格中发送一条消息,而且指定了源必须是 http://www.wrox.com 如果源匹配,那么消息将会交付到内嵌窗格;否则, postmessage() 什么也不做。这个限制可以保护信息不会因地址改变而泄露。如果不想限制接收目标,则可以给 postmessage() 的第二个参数传 * 但不推荐这么做。

接收到 XDM 消息后, window 对象上会触发 message 事件。这个事件是异步触发的,因此从消息发出到接收到消息(接收口触发 message 事件)可能有延迟。传给 onmessage 事件处理程序的 event 对象包含以下3方面重要信息。

  • data:作为第一个参数传递给 postMessage() 的字符串数据。
  • origin:发送消息的文档源,例如 http://www.wrox.com
  • source:发送消息的文档中 window 对象的代理。这个代理对象主要用于在发送上一条消息的窗口中执行 postMessage() 方法。如果发送窗口有相同的源,那么这个对象应该就是 windows对象。

接收消息之后验证发送窗口的源是非常重要的。与 postMessage() 的第二个参数可以保证数据不会意外传给未知页面一样,在 onmessage 事件处理程序中检查发送窗口的源可以保证数据来自正确的地方。基本的使用方式如下所示:

window.addEventListener("message", event => {
  // 确保来自预期发送者
  if (event.origin == "http://www.wrox.com") {
    // 对数据进行一些处理
    postMessage(event.data);
    // 可选:向来源窗口发送一条消息
    event.source.postMessage("Received", "http://p2p.wrox.com");
  }
  //...
});

大多数情况下, event.source 是某个 window 对象的代理,而非实际的 window 对象。因此不能通过它访问所有窗口下的信息。最好只使用 postMessage() ,这个方法永远存在而且可以调用。

XDM 有一些怪异之处。首先, postMessage() 的第一个参数的最初实现始终是一个字符串。后来,第一个参数改为允许任何结构的数据传人,不过并非所有浏览器都实现了这个改变。为此,最好就是只通过 postMessage() 发送字符串。如果需要传递结构化数据,那么最好先对该数据调用 JSON.stringify() ,通过 postMessage() 传过去之后,再在 onmessage 事件处理程序中调用JSON.parse()。

在通过内嵌窗格加载不同域时,使用 XDM 是非常方便的。这种方法在混搭(mashup)和社交应用中非常常用。通过使用 XDM 与内嵌窗格中的网页通信,可以保证包含页面的安全。XDM 也可以用于同源页面之间通信。



3 Encoding API


Encoding API 主要用于实现字符串与定型数组之间的转换。规范新增了4个用于执行转换的全局类: TextEncoder 、TextEncoderStream 、TextDecoderTextDecoderStream

注意 相比于批量(bulk)的编解码,对流(stream)编解码的支持很有限。

3.1 文本编码

Encoding API 提供了两种将字符串转换为定型数组二进制格式的方法:批量编码和流编码。把字符串转换为定型数组时,编码器始终使用 UTF-8。

3.1.1 批量编码

所谓批量,指的是 JavaScript引擎会同步编码整个字符串。对于非常长的字符串,可能会花较长时间。批量编码是通过 TextEncoder 的实例完成的:

const textEncoder = new TextEncoder();

这个实例上有一个 encode() 方法,该方法接收一个字符串参数,并以 uint8Array 格式返回每个字符的 UTF-8 编码:

const textEncoder = new TextEncoder();
const decodedText = "foo";
const encodedText = textEncoder.encode(decodedText);

// f 的 UTF-8 编码是 0x66(即十进制102)
// o 的 UTF-8 编码是 0x6F(即二进制111)
console.log(encodedText); // Uint8Array(3) [102,111,111]

编码器是用于处理字符的,有些字符(如表情符号)在最终返回的数组中可能会占多个索引

const textEncoder = new TextEncoder();
const decodedText = "😊";
const encodedText = textEncoder.encode(decodedText);

// 😊 的U TF-8 编码是 0xFO 0x9F 0x98 0x8A(即十进制 240、159、152、138)
console.log(encodedText); // Uint8Array(4) [240,159,152,138]

编码器实例还有一个 encodeInto() 方法,该方法接收一个字符串和目标 Unit8Array ,返回-个字典,该字典包含 readwritten 属性,分别表示成功从源字符串读取了多少字符和向目标数组写人了多少字符。如果定型数组的空间不够,编码就会提前终止,返回的字典会体现这个结果:

const textEncoder = new TextEncoder();
const fooArr = new Uint8Array(3);
const barArr = new Uint8Array(2);
const fooResult = textEncoder.encodeInto("foo", fooArr);
const barResult = textEncoder.encodeInto("bar", barArr);

console.log(fooArr); // Uint8Array(3) [102,111,111]
console.log(fooResult); // { read: 3,written: 3 )

console.log(barArr); // Uint8Array(2) [98,97]
console.log(barResult); // { read: 2, written: 2 }

encode() 要求分配一个新的 Unit8Array, encodeInto() 则不需要。对于追求性能的应用,这个差别可能会带来显著不同。

注意,文本编码会始终使用 UTF-8 格式,而且必须写入 Unit8Array 实例。使用其他类型数组会导致 encodeInto() 抛出错误。

3.1.2 流编码

TextEncoderStream 其实就是 TransformStream 形式的 TextEncoder 。将解码后的文本流避过管道输人流编码器会得到编码后文本块的流:

async function* chars() {
  const decodedText = "foo";
  for (let char of decodedText) {
    yield await new Promise(resolve => setTimeout(resolve, 1000, char));
  }
}

const decodedTextStream = new ReadableStream({
  async start(controller) {
    for (let chunk of chars()) {
      controller.enqueue(chunk);
    }
    controller.close();
  }
});

const encodedTextStream = decodedTextStream.pipeThrough(new TextEncoderStream());

const readableStreamDdefaultReader = encodedTextStream.getReader();

(async function () {
  while (true) {
    const { done, value } = await readableStreamDdefaultReader.read();
    if (done) {
      break;
    } else {
      console.log(value);
    }
  }
})();

// Uint8Array[102]
// Uint8Array[111]
// Uint8Array[111]

3.2 文本解码

Encoding API 提供了两种将定型数组转换为字符串的方式:批量解码和流解码。与编码器类不同,在将定型数组转换为字符串时,解码器支持非常多的字符串编码,可以参考 Encoding Standard 规范的"Names and labels”一节。

默认字符编码格式是 UTF-8。

3.2.1 批量解码

所谓批量,指的是 JavaScript引擎会同步解码整个字符串。对于非常长的字符串,可能会花较长时间。批量解码是通过 TextDecoder 的实例完成的:

const textDecoder = new TextDecoder();

这个实例上有一个 decode() 方法,该方法接收一个定型数组参数,返回解码后的字符串

const textDecoder = new TextDecoder();

// f的UTF-8 编码是0x66(即十进制 102)/10的UTF-8编码是0x6F(即二进制 111)



const encodedText = Uint8Array.of(102,111, 111):
const decodedText = textDecoder.decode (encodedText);

console.log(decodedText); // foo

解码器不关心传入的是哪种定型数组,它只会专心解码整个二进制表示。在下面这个例子中,只包含8位字符的32位值被解码为 UTF-8 格式,解码得到的字符串中填充了空格:

const textDecoder = new TextDecoder();

// f的UTF-8编码是0x66(即十进制102)
// o的UTF-8编码是0x6F (即二进制111)

const encodedText = Uint32Array.of(102, 111, 111);
const decodedText = textDecoder.decode(encodedText);

console.log(decodedText); // "f  o   o"

解码器是用于处理定型数组中分散在多个索引上的字符的,包括表情符号

const textDecoder = new TextDecoder();

// 😊的 UTE-8 编码是 OxF0 0x9F 0x98 0x8A(即十进制 240、159、152、138)
const encodedText = Uint8Array.of(240, 159, 152, 138);

const decodedText = textDecoder.decode(encodedText);
console.log(decodedText); // 😊

TextEncoder 不同,TextDecoder 可以兼容很多字符编码。比如下面的例子就使用了 UTF-16 而非默认的 UTF-8:

const textDecoder = new TextDecoder("utf-16");

//f的UTE-8编码是0x0066(即十进制102)
//o的UTF-8编码是0x006F(即二进制111)
const encodedText = Uint16Array.of(102, 111, 111);
const decodedText = textDecoder.decode(encodedText);

console.log(decodedText); // foo

3.2.2 流解码

TextDecoderStream 其实就是 TransformStream 形式的 TextDecoder 。将编码后的文本流通过管道输人流解码器会得到解码后文本块的流:

async function* chars() {
  // 每个块必须是一个定型数组
  const encodedText = [102, 111, 111].map(x => Uint8Array.of(x));

  for (let char of encodedText) {
    yield await new Promise(resolve => setTimeout(resolve, 1000, char));
  }
}

const encodedTextStream = new ReadableStream({
  async start(controller) {
    for await (let chunk of chars()) {
      controller.enqueue(chunk);
    }
    controller.close();
  }
});

const decodedTextStream = encodedTextStream.pipeThrough(new TextDecoderStream());

const readableStreamDefaultReader = decodedTextStream.getReader();

(async function () {
  while (true) {
    const { done, value } = await readableStreamDefaultReader.read();
    if (done) {
      break;
    } else {
      console.log(value);
    }
  }
})();

// Promise {<pending>}
// f
// o
// o

文本解码器流能够识别可能分散在不同块上的代理对。解码器流会保持块片段直到取得完整的字符。比如在下面的例子中,流解码器在解码流并输出字符之前会等待传入4个块:

async function* chars() {
  // 😊的UTF-8 编码是OxFO 0x9F 0x98 0x8A(即十进制 240、159、152、138)
  const encodedText = [240, 159, 152, 138].map(x => Uint8Array.of(x));

  for (let char of encodedText) {
    yield await new Promise(resolve => setTimeout(resolve, 1000, char));
  }
}
const encodedTextStream = new ReadableStream({
  async start(controller) {
    for await (let chunk of chars()) {
      controller.enqueue(chunk);
    }
    controller.close();
  }
});

const decodedTextStream = encodedTextStream.pipeThrough(new TextDecoderStream());

const readableStreamDefaultReader = decodedTextStream.getReader();

(async function () {
  while (true) {
    const { done, value } = await readableStreamDefaultReader.read();
    if (done) {
      break;
    } else {
      console.log(value);
    }
  }
})();
// Promise {<pending>}
// 😊

文本解码器流经常与 fetch() 一起使用,因为响应体可以作为 Readablestream 来处理。比如:

response = await fetch(url);
const stream = response.body.pipeThrough(new TextDecoderstream());
const decodedstream = stream.getReader();
for await (let decodedChunk of decodedstream) {
  console.log(decodedChunk);
}


4 File API 与 Bolb API


🚀 站内跳转 上传你需要知道的所有前置知识点

Web 应用程序的一个主要的痛点是无法操作用户计算机上的文件。2000 年之前,处理文件的唯一方式是把 <input type="file"> 放到一个表单里,仅此而已。File APIBlob API是为了让 Web开发者能以安全的方式访问客户端机器上的文件,从而更好地与这些文件交互而设计的。

4.1 File 类型

File API 仍然以表单中的文件输人字段为基础,但是增加了直接访问文件信息的能力。HTML5 在DOM上为文件输人元素添加了 files 集合。当用户在文件字段中选择一个或多个文件时,这个 files 集合中会包含一组 File 对象,表示被选中的文件。每个 File 对象都有一些只读属性。

4.1.1 File 属性

属性 是否只读 描述
File.name 只读 返回当前 File 本地系统中的文件。
File.size 只读 返回文件的大小。File.webkitRelativePath 只读 非标准返回 File 相关的 path 或 URL。
File.type 只读 返回文件的 含文件 MIME 类型的字符串(MIME Type)
File.lastModified 只读 返回当前 File 对象所引用文件最后修改时间,自 UNIX 时间起始值(1970 年 1 月 1 日 00:00:00 UTC)以来的毫秒数。
File.lastModifiedDate 只读 返回当前 File 对象所引用文件最后修改时间的 Date 对象。只有Chrome 实现了。

例如,通过监听 change 事件然后遍历 files 集合可以取得每个选中文件的信息:

let filesList = document.getElementById("files-list");
filesList.addEventListener("change", event => {
  let files = event.target.files,
    i = 0,
    len = files.length;

  while (i < len) {
    const f = files[i];
    console.log(`${f.name} (${f.type}, ${f.size} bytes)`);
    i++;
  }
});

4.1.2 File 方法

方法 描述
File.slice(start, end) 返回一个新的 Blob 对象,它包含有源 Blob 对象中指定范围内的数据(不包括结束位置)。

4.2 FileList 对象

一个 FileList 对象通常来自于一个 HTML<input>元素的 files 属性,你可以通过这个对象访问到用户所选择的文件。该类型的对象还有可能来自用户的拖放操作,查看 DataTransfer (en-US) 对象了解详情。

4.2.1 FileList 属性:length

FileList 是一个类数组对象,每个成员都是一个 File 对象 实例,通过.length来获取当前类表中的文件数量

ps: 类数组也可以用数组解构的方式

4.2.2 FileList 方法:item()

根据给定的索引值,返回 FileList 对象中对应的 File 对象。也可以直接数组索引获取,因为是类数组。

// 遍历所有文件
for (var i = 0; i < files.length; i++) {
  // 取得一个文件
  file = files.item(i);
  // 这样也行
  file = files[i];
  // 取得文件名
  alert(file.name);
}

4.2 FileReader 类型

FileReader 类型表示一种异步文件读取机制。可以把 FileReade 想象成类似于 XMLHttpRequest 只不过是用于从文件系统读取文件,而不是从服务器读取数据。

4.2.1 FileReader 属性

属性 是否只读 描述
FileReader.error 只读 一个DOMException,表示在读取文件时发生的错误。
FileReader.readyState 只读 表示FileReader状态的数字,EMPTY 0 还没有加载任何数据。LOADING 1 数据正在被加载。DONE 2 已完成全部的读取请求。
FileReader.result 只读 文件的内容。该属性仅在读取操作完成后才有效,数据的格式取决于使用哪个方法来启动读取操作。

4.2.3 FileReader 方法

这些读取数据的方法为处理文件数据提供了极大的灵活性。例如,为了向用户显示图片,可以将图片读取为数据 URI,而为了解析文件内容,可以将文件读取为文本。

方法 描述
FileReader.readAsText(file, encoding) 从文件中读取纯文本内容并保存在 result 属性中。第二个参数表示编码,是可选的。默认为 UTF-8。
FileReader.abort() 中止读取操作。在返回时,readyState属性为DONE。
FileReader.readAsArrayBuffer(file) 读取文件并将文件内容以 ArrayBuffer 形式保存在 result 属性
FileReader.readAsDataURL(file) 读取文件并将内容的数据 URI 保存在 result 属性中。
FileReader.readAsBinaryString() 非标准 读取文件并将每个字符的二进制数据保存在 result 属性中。(已废弃)

4.2.1 FileReader 事件

因为这些读取方法是异步的,所以每个 FileReader 会发布几个事件。因为 FileReader 继承自EventTarget,所以所有这些事件也可以通过addEventListener方法使用。

事件 描述
FileReader.onabort 处理abort事件。该事件在读取操作被中断时触发。
FileReader.onerror (en-US) 处理error事件。该事件在读取操作发生错误时触发。
FileReader.onload 处理load事件。该事件在读取操作完成时触发。
FileReader.onloadstart 处理loadstart事件。该事件在读取操作开始时触发。
FileReader.onloadend 处理loadend事件。该事件在读取操作结束时(要么成功,要么失败)触发。
FileReader.onprogress 处理progress事件。该事件在读取Blob时触发。

其中3 个最有用的事件是 progresserrorload ,分别表示还有更多数据、发生了错误和读取完成。

progress 事件每 50 毫秒就会触发一次,其与 XHRprogress 事件具有相同的信息: lengthComputableloadedtotal。此外, 在progress 事件中可以读取 FileReaderresult 属性,即使其中尚未包含全部数据。

error 事件会在由于某种原因无法读取文件时触发。触发 error 事件时, FileReadererror 属性会包含错误信息。这个属性是一个对象,只包含一个属性: code 。这个错误码的值可能是1(未找到文件)、2(安全错误)、3(读取被中断)、4(文件不可读)或5(编码错误)。

load 事件会在文件成功加载后触发。如果 error 事件被触发,则不会再触发 load 事件。下面的例子演示了所有这3个事件:

let filesList = document.getElementById("files-list");
filesList.addEventListener("change", event => {
  let info = "",
    output = document.getElementById("output"),
    progress = document.getElementById("progress"),
    files = event.target.files,
    type = "default",
    reader = new FileReader();
  if (/image/.test(files[0].type)) {
    reader.readAsDataURL(files[0]);
    type = "image";
  } else {
    reader.readAsText(files[0]);
    type = "text";
  }

  reader.onerror = function () {
    output.innerHTML = "Could not read file, error code is " + reader.error.code;
  };

  reader.onprogress = function (event) {
    if (event.lengthComputable) {
      progress.innerHTML = `${event.loaded}/${event.total}`;
    }
  };

  reader.onload = function () {
    let html = "";
    switch (type) {
      case "image":
        html = `<img src="${reader.result}">`;
        break;
      case "text":
        html = reader.result;
        break;
    }
    output.innerHTML = html;
  };
});

以上代码从表单字段中读取一个文件,并将其内容显示在了网页上。如果文件的 MIME 类型表示是一个图片,那么就将其读取后保存为数据 URI ,在 1oad 事件触发时将据 URI 作为图片插人页面中。如果文件不是图片、则读取后将其保存为文本并原样输出到网页上。progress 事件用于跟踪和显示和读取文件的进度,而 error 事件用于监控错误。

如果想提前结束文件读取,则可以在过程中调用 abort() 方法,从而触发 abort 事件。在 load.errorabort 事件触发后,还会触发 loadend 事件。 loadend 事件表示在上述3种情况下,所有读取操作都已经结束。readAsText()readAsDataURL() 方法已经得到了所有主流测览器支持。


4.3 FileReaderSync 类型

顾名思义, FileReaderSync 类型就是 FileReader 的同步版本。这个类型拥有与 FileReader 相同的方法,只有在整个文件都加载到内存之后才会继续执行。 FileReaderSync 只在工作线程中可用因为如果读取整个文件耗时太长则会影响全局(在主线程里进行同步 I/O 操作可能会阻塞用户界面)。

假设通过 postMessage() 向工作线程发送了一个 File 对象。以下代码会让工作线程同步将文件读取到内存中,然后将文件的数据 URL 发回来:

// worker.js
self.omessage = messageEvent => {
  const syncReader = new FileReaderSync();
  console.log(syncReader); // FileReaderSync {}
  // 读取文件时阻塞工作线程
  const result = syncReader.readAsDataUrl(messageEvent.data);
  // PDF文件的示例响应
  console.log(result); // data:application/pdf;base64,JVBERi0xLjQK..
  //把URL 发回去
  self.postMessage(result);
};

4.4 Blob 与部分读取

某些情况下,可能需要读取部分文件而不是整个文件。为此, File 对象提供了一个名为 slice() 的方法。 slice() 方法接收两个参数:起始字节和要读取的字节数。这个方法返回一个 Blob 的实例,而 Blob 实际上是 File 的超类

blob 示二进制大对象(binary largetobject),是 JavaScript对不可修改一进制数据的封装类型。包含字符串的数组ArrayBuffersArrayBufferViews,甚至其他 Blob 都可以用来创建 blob

4.4.1 Blob 构造函数

Blob 构造函数可以接收一个 options 参数,并在其中指定 MIME 类型:

congolo.log(new Blob(["foo"])); // Blob {size: 3,type:""}

console.log(new Blob(['("a": "b")'], { type: "application/json" })); // Blob {size: 10, type: "application/json"}

console.log(new Blob(["<p>Foo</p>", "<p>Bar</p>"], { type: "text/html" })); // Blob {size: 20,type: "**text**/htm1"}

4.4.2 Blob 属性

属性 是否只读 描述
Blob.size 只读 Blob 对象中所包含数据的大小(字节)。
Blob.type 只读 一个字符串,表明该 Blob 对象所包含数据的 MIME 类型。如果类型未知,则该值为空字符串。

4.4.3 Blob 方法

方法名 描述
Blob.arrayBuffer() 返回一个 promise,其会兑现一个包含 Blob 所有内容的二进制格式的 ArrayBuffer。
Blob.slice() 返回一个新的 Blob 对象,包含了源 Blob 对象中指定范围内的数据。
Blob.stream() 返回一个能读取 Blob 内容的 ReadableStream。
Blob.text() 返回一个 promise,其会兑现一个包含 Blob 所有内容的 UTF-8 格式的字符串。

还有一个 slice() 方法用于进一步切分数据(file slice() 切分完是Blob,Blob也可以继续 slice() 切分 )。另外也可以使用 FileReader 从中读取数据。下面的例子只会读取文件的前 32 字节:

只读取部分文件可以节省时间,特别是在只需要数据特定部分比如文件头的时候

let filesList = document.getElementById("files-list");
filesList.addEventListener("change", event => {
  let info = "",
    output = document.getElementById("output"),
    progress = document.getElementById("progress"),
    files = event.target.files,
    reader = new FileReader(),
    blob = blobSlice(files[0], 0, 32);

  if (blob) {
    reader.readAsText(blob);
    reader.onerror = function () {
      output.innerHTML = "Could not read file, error code is " + reader.error.code;
    };
    reader.onload = function () {
      output.innerHTML = reader.result;
    };
  } else {
    console.log("Your browser doesn't support slice().");
  }
});

4.5 对象URL 与 Blob

方法名 描述
URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的 URL。这个 URL 的生命周期和创建它的窗口中的 document 绑定。这个新的 URL 对象表示指定的 File 对象或 Blob 对象。
URL.revokeObjectURL() 静态方法用来释放一个之前已经存在的、通过调用 URL.createObjectURL() 创建的 URL 对象。

对象URL 有时候也称作 Blob URL ,是指引用存储在 FileBlob 中数据的 URL对象URL 的优点是不用把文件内容读取到JavaScript也可以使用文件。只要在适当位置提供 对象URL 即可。要创建 对象URL ,可以使用 window.URL.createObjectURL()方法并传人 File 或 Blob 对象。这个函数返回的值是一个指向内在中地的字符。 因为这个字符串是 URL ,所以可以在 DOM 中直接使用。例如,以下代码使用对象 URL 在页面中显示了一张图片:

let filesList = document.getElementById("files-list");
filesList.addEventListener("change", event => {
  let info = "",
    output = document.getElementById("output"),
    progress = document.getElementById("progress"),
    files = event.target.files,
    reader = new FileReader(),
    url = window.URL.createObjectURL(files[0]);

  if (url) {
    if (/image/.test(files[0].type)) {
      output.innerHTML = `<img src="${url}" />`;
    } else {
      output.innerHTML = `Not an image.`;
    }
  } else {
    console.log("Your browser doesn't support object URLs.");
  }
});

如果把 对象URL 直接放到 <img> 标签,就不需要把数据先读到 JavaScript 中了。<img> 标签可以自接从相应的内存位置把数据读取到页面上。
使用完数据之后,最好能释放与之关联的内存。只要 对象URL 在使用中,就不能释放内存。如果想表明不再使用某个 对象URL ,则可以把它传给 window.URL.revokeobjectURL() 。页面卸载时.所有对象 URL 占用的内存都会被释放。不过,最好在不使用时就立即释放内存,以便尽可能保持页面占用最少资源。


4.6 读取拖放文件

组合使用 HTML5拖放APIFileAPI 可以创建读取文件信息的有趣功能。在页面上创建放置目标后,可以从桌面上把文件拖动并放到放置目标。这样会像拖放图片或链接一样触发 drop 事件。被放置的文件可以通过事件的event.dataTransfer.files 属性读到,这个属性保存着一组 File对象 ,就像文本输入字段一样。

ps:DataTransfer 对象用于保存拖动并放下(drag and drop)过程中的数据。它可以保存一项或多项数据,这些数据项可以是一种或者多种数据类型。关于拖放的更多信息,请参见 Drag and Drop.

下面的例子会把拖放到页面放置目标上的文件信息打印出来:

let droptarget = document.getElementById("droptarget");
droptarget.addEventListener("change", event => {
  let info = "",
    output = document.getElementById("output"),
    files,
    i,
    len;
  event.preventDefault();

  if (event.type === "drop") {
    files = event.dataTransfer.files;
    i = 0;
    len = files.length;

    while (i < len) {
      info += `${files[i].name} (${files[i].type}, ${files[i].size} bytes) <br>`;
      i++;
    }

    output.innerHTML = info;
  }

  droptarget.addEventListener("dragenter", handleEvent);
  droptarget.addEventListener("dragover", handleEvent);
  droptarget.addEventListener("drop", handleEvent);
});

与后面要介绍的拖放的例子一样,必须取消 dragenteraragoverarop 的默认行为,在 drop 事件处理程序中,可以通过 event.dataTransfer.files 读到文件,此时可以获取文件的相关信息。



5 媒体元素


随着嵌入音频和视频元素在 Web 应用上的流行,大多数内容提供商会强迫使用 Flash 以便达到最佳的跨浏览器兼容性。HTML5 新增了两个与媒体相关的元素,即 <audio ><video> ,从而为浏览器提供了嵌人音频和视频的统一解决方案。

这两个元素既支持 Web 开发者在页面中嵌人媒体文件,也支持JavaScript实现对媒体的自定义控制。以下是它们的用法:

<!-- 嵌入视频 -->
<video src="conference.mpg" id="myVideo">Video player not available.</video>
<!-- 嵌入音频 -->
<audio src="song.mp3" id="myAudio">Audio player not available.</audio>

每个元素至少要求有一个 src 属性,以表示要加载的媒体文件。我们也可以指定表示视频播放器大小的 widthheight 属性,以及在视频加载期间显示图片 URI 的 poster 属性。另外, controls 属性如果存在,则表示浏览器应该显示播放界面,让用户可以直接控制媒体。开始和结束标签之间的内容是在媒体播放器不可用时显示的替代内容。

由于浏览器支持的媒体格式不同,因此可以指定多个不同的媒体源。为此,需要从元素中删除 src 属性,使用一个或多个 <source> 元素代替,如下面的例子所示:

<!-- 嵌入视频 -->
<video id="myVideo">
  <source src="conference.webm" type="video/webm; codecs='vp8,vorbis'" />
  <source src="conference.ogv" type="video/ogg; codecs='theora, vorbis'" />
  <source src="conference.mpg" />
  Video player not available.
</video>
<!-- 嵌入音频 -->
<audio id="myAudio">
  <source src="song.ogg" type="audio/ogg" />
  <source src="song.mp3" type="audio/mpeg" />
  Audio player not available.
</audio>

讨论不同音频和视频的编解码器超出了本书范畴,但浏览器支持的编解码器确实可能有所不同,因此指定多个源文件通常是必需的。

5.1 属性

<video><audio> 元素提供了稳健的 JavaScript 接口。这两个元素有很多共有属性,可以用于确定媒体的当前状态,如下表所示。

属性 数据类型 说明
autoplay Boolean 取得或设置 autoplay 标签
buffered TimeRanges 对象,表示已下载缓冲的时间范围
bufferedBytes ByteRanges 对象,表示已下载缓冲的字节范围
bufferingRate Integer 平均每秒下载的位数
bufferingThrottled Boolean 表示缓冲是否被浏览器截流
controls Boolean 取得或设置 controls 属性,用于显示或隐藏浏览器内置控件
currentLoop Integer 媒体已经播放的循环次数
currentSrc String 当前播放媒体的 URL
currentTime Float 已经播放的秒数
defaultPlaybackRate Float 取得或设置默认回放速率。默认为1.0秒
duration Float 媒体的总秒数
ended Boolean 表示媒体是否播放完成
loop Boolean 取得或设置媒体是否应该在播放完再循环开始
muted Boolean 取得或设置媒体是否静音
networkState Integer 表示媒体当前网络连接状态。0表示空,1表示加载中,2表示加载元数据,3表示加载了第一帧,4表示加载完成
paused Float 表示播放器是否暂停
playbackRate Float 取得或设置当前播放速率。用户可能会让媒体播放快一些或慢一些。与defaultPlaybackRate 不同,该属性会保持不变,除非开发者修改
played TimeRanges 到目前为止已经播放的时间范围
readyState Integer 表示媒体是否已经准备就绪。0表示媒体不可用,1表示可以显示当前帧, 2表示媒体可以开始播放,3表示媒体可以从头播到尾
seekable TimeRanges 可以跳转的时间范围
seeking Boolean 表示播放器是否正移动到媒体文件的新位置
src String 媒体文件源。可以在任何时候重写
start Float 取得或设置媒体文件中的位置,以秒为单位,从该处开始播放
totalBytes Integer 资源需要的字节总数(如果知道的话)
videoHeight Integer 返回视频(不一定是元素)的高度。只适用于 <video>
videoWidth Integer 返回视频(不一定是元素)的宽度。只适用于 <video>
volume Float 取得或设置当前音量,值为0.0到1.0

5.2 事件

除了有很多属性,媒体元素还有很多事件。这些事件会监控由于媒体回放或用户交互导致的不同属性的变化。下表列出了这些事件。

属性 说明
abort 下载被中断
canplay 回放可以开始,readystate为2
canplaythrough 回放可以继续,不应该中断,readstate 为3
canshowcurrentframe 已经下载当前帧,readystate为1
autoplay 取得或设置
autoplay 取得或设置
autoplay 取得或设置
autoplay 取得或设置
dataunavailable 不能回放,因为没有数据,readystate 为0
durationchange duration 属性的值发生变化
emptied 网络连接关闭了
empty 发生了错误,阻止媒体下载
ended 媒体已经播放完一遍,且停止了
error 下载期间发生了网络错误
load 所有媒体已经下载完毕。这个事件已被废弃,使用 canplaythrough 代替
loadeddata 媒体的第一帧已经下载
loadedmetadata 媒体的元数据已经下载
loadstart 下载已经开始
pause 回放已经暂停
play 媒体已经收到开始播放的请求
playing 媒体已经实际开始播放了
progress 下载中
ratechange 媒体播放速率发生变化
seeked 跳转已结束
seeking 回放已移动到新位置
stalled 浏览器尝试下载,但尚未收到数据
timeupdate currentTime 被非常规或意外地更改了
volumechange volume或mutea 属性值发生了变化
waiting 回放暂停,以下载更多数据

这些事件被设计得尽可能具体,以便 Web 开发者能够使用较少的 HTML 和JavaScript 创建自定义的音频/视频播放器(而不是创建新 Flash 影片 )。


5.3 自定义媒体播放器

使用 <audio><video>play()pause() 方法,可以手动控制媒体文件的播放。综合使用属性、事件和这些方法,可以方便地创建自定义的媒体播放器,如下面的例子所示:

<div class="mediaplayer">
  <div class="video">
    <video id="player" src="movie.mov" poster="mymovie.jpg" width="300" height="200">Video player not available.</video>
  </div>
  <div class="controls">
    <input type="button" value="Play" id="video-btn" />
    <span id="curtime">0</span>/<span id="duration">0</span>
  </div>
</div>

通过使用 JavaScipt创建一个简单的视频播放器,上面这个基本的 HTML就可以被激活了,如下所示

//取得元素的引用
let player = document.getElementById("player"),
  btn = document.getElementById("video-btn"),
  curtime = document.getElementById("curtime"),
  duration = document.getElementById("duration");

//更新时长
duration.innerHTML = player.duration;

// 为按钮添加事件处理程序
btn.addEventListener("click", event => {
  if (player.paused) {
    player.play();
    btn.value = "Pause";
  } else {
    player.pause();
    btn.value = "Play";
  }
});

// 周期性更新当前时间
setInterval(() => {
  curtime.innerHTML = player.currentTime;
}, 250);

5.4 检测编码器

如前所述,并不是所有浏览器都支持 <video><audio> 的所有编解码器,这通常意味着必须提供多个媒体源。为此,也有JavaScript API可以用来检测浏览器是否支持给定格式和编解码器。这两个媒体元素都有一个名为 canPlayType() 的方法,该方法接收一个格式/编解码器字符串,返回一个字符串值:"probably"、"maybe"或""(空字符串),其中空字符串就是假值,意味着可以在if 语句中像这样使用 canPlayType() :

if (audio.canPlayType("audio/mpeg")) {
  // 执行某些操作
}

"probably"和"maybe"都是真值,在 if 语句的上下文中可以转型为 true。

在只给 canplayType() 提供一个 MIME类型的情况下,最可能返回的值是"maybe"和空字符串。这是因为文件实际上只是一个包装音频和视频数据的容器,而真正决定文件是否可以播放的是编码。在同时提供 MIME类型和编解码器的情况下,返回值的可能性会提高到"probably"。下面是几个例子:

let audio = document.getElementById("audio-player");

//很可能是"maybe"
if (audio.canPlayType("audio/mpeg")) {
  //执行某些操作
}
// 可能是"probably"
if (audio.canplayType('audio/ogg; codecs="vorbis"')) {
  //执行某些操作
}

注意,编解码器必须放到引号中。同样,也可以在视频元素上使用 canplayType() 检测视频格式。


5.5 音频类型

<audio> 元素还有一个名为 Audio 的原生avaScript构造函数,支持在任何时候播放音频。Audio 类型与 Image 类似,都是 DOM 元素的对等体,只是不需插人文档即可工作。要通过 Audio 播放音频,只需创建一个新实例并传入音频源文件:

let audio = new Audio("sound.mp3");
EventUtil.addHandler(audio, "canplaythrough", function (event) {
  audio.play();
});

创建 Audio 的新实例就会开始下载指定的文件。下载完毕后,可以调用 play() 来播放音频。

在IOS 中调用 play() 方法会弹出一个对话框,请求用户授权播放声音。为了连续播放,必须在 onfinish 事件处理程序中立即调用 play()



6 原生拖放


IE4 最早在网页中为 JavaScript 引人了对拖放功能的支持。当时,网页中只有两样东西可以触发拖放:图片和文本。拖动图片就是简单地在图片上按住鼠标不放然后移动鼠标。而对于文本,必须先选中,然后再以同样的方式拖动。在4中,唯一有效的放置目标是文本框。IE5 扩展了拖放能力,添加了新的事件,让网页中几乎一切都可以成为放置目标。IE5.5 又进一步,允许几乎一切都可以拖动(IE6也支持这个功能)。HTML5在IE 的拖放实现基础上标准化了拖放功能。所有主流浏览器都根据 HTML5 规范实现了原生的拖放。

关于拖放最有意思的可能就是可以跨窗格、跨浏览器容器,有时候甚至可以跨应用程序拖动元素浏览器对拖放的支持可以让我们实现这些功能。

6.1 拖放事件

拖放事件几乎可以让开发者控制拖放操作的方方面面。关键的部分是确定每个事件是在哪里触发的。有的事件在被拖放元素上触发,有的事件则在放置目标上触发。在某个元素被拖动时,会(按顺序)触发以下事件:

  • dragstart
  • drag
  • dragend

在按住鼠标键不放并开始移动鼠标的那一刻,被拖动元素上会触发 dragstart 事件。此时光标会变成非放符号(圆环中间一条斜杠),表示元素不能放到自身上。拖动开始时,可以在 ondragstart 事件处理程序中通过 JavaScript 执行某些操作。

dragstart 事件触发后,只要目标还被拖动就会持续触发 drag 事件。这个事件类似于 mousemove ,即随着鼠标移动而不断触发。当拖动停止时(把元素放到有效或无效的放置目标上),会触发 dragend 事件。

所有这3个事件的目标都是被拖动的元素。默认情况下,浏览器在拖动开始后不会改变被拖动元素的外观,因此是否改变外观由你来决定。不过,大多数浏览器此时会创建元素的一个半透明副本,始终跟随在光标下方。
在把元素拖动到一个有效的放置目标上时,会依次触发以下事件:

  • dragenter
  • dragover
  • dragleave或drop

只要一把元素拖动到放置目标上, dragenter 事件(类似于 mouseover 事件)就会触发。 dragente 事件触发之后,会立即触发 dragover 事件,并且元素在放置目标范围内被拖动期间此事件会持续触发.当元素被拖动到放置日标之外, dragover 事件停止触发, ß 事件触发(类似于 mouseout 事件)。如果被拖动元素被放到了目标上,则会触发 drop 事件而不是 dragleave 事件。这些事件的目标是放置目标元素。


6.2 自定义放置目标

在把某个元素拖动到无效放置目标上时,会看到一个特殊光标(圆环中间一条斜杠)表示不能放下。即使所有元素都支持放置目标事件,这些元素默认也是不允许放置的。如果把元素拖动到不允许放置的目标上,无论用户动作是什么都不会触发 drop 事件。不过,通过覆盖 dragenterdragover 事件的默认行为,可以把任何元素转换为有效的放置目标。例如,如果有一个ID为"droptarget"的<div>元素,那么可以使用以下代码把它转换成一个放置目标:

let droptarget = document.getElementById("droptarget");
droptarget.addEventListener("dragover", event => {
  event.preventDefault();
});
droptarget.addEventListener("dragenter", event => {
  event.preventDefault();
});

执行上面的代码之后,把元素拖动到这个<div>上应该可以看到光标变成了允许放置的样子。另外 drop 事件也会触发。

在 Firefox中,放置事件的默认行为是导航到放在放置目标上的URL。这意味着把图片拖动到放置目标上会导致页面导航到图片文件,把文本拖动到放置目标上会导致无效 URL 错误。为阻止这个行为,在 Firefox 中必须取消 drop 事件的默认行为:

droptarget.addEventListener("drop", event => {
  event.preventDefault();
});

6.3 dataTransfer 对象

除非数据受影响,否则简单的拖放并没有实际意义。为实现拖动操作中的数据传输,IE5在event对象上暴露了 dataTransfer 对象,用于从被拖动元素向放置目标传递字符串数据。因为这个对象是 event 的属性,所以在拖放事件的事件处理程序外部无法访问 dataTransfer 。在事件处理程序内部可以使用这个对象的属性和方法实现拖放功能, dataTransfer 对象现在已经纳人了 HTML5 工作草案。

dataTransfer 对象有两个主要方法: getData()setData() 。顾名思义, getData() 用于获取 setData() 存储的值。 setData() 的第-个参数以及 getData() 的唯一参数是一个字符串,表示要设置的数据类型:"text"或"URL",如下所示:

// 传递文本;
event.dataTransfer.setData("text", "some text");
let text = event.dataTransfer.getData("text");
// 传递URL
event.dataTransfer.setData("URL", "http://www.wrox.com/");
let url = event.dataTransfer.getData("URL");

虽然这两种数据类型是 IE 最初引入的,但 HTMIL5 已经将其扩展为允许任何 MIME 类型。为向后兼容,HIML5 还会继续支持"text"和"URL",但它们会分别被映射到 "text/plain""text/uri-list"

dataTransfer 对象实际上可以包含每种 MIME 类型的一个值,也就是说可以同时保存文本和URL,两者不会相互覆盖。存储在 dataTransfer 对象中的数据只能在放置事件中读取。如果没有在 ondrop 事件处理程序中取得这些数据, dataTransfer 对象就会被销毁,数据也会丢失。

在从文本框拖动文本时,浏览器会调用 setData() 并将拖动的文本以"text"格式存储起来。类似地,在拖动链接或图片时,浏览器会调用 setData() 并把 URL 存储起来。当数据被放置在目标上时,可以使用 getData() 获取这些数据。当然,可以在 dragstart 事件中手动调用 setData() 存储自定义数据,以便将来使用。

作为文本的数据和作为 URL 的数据有一个区别。当把数据作为文本存储时,数据不会被特殊对待。而当把数据作为 URL 存储时,数据会被作为网页中的一个链接,意味着如果把它放到另一个浏览器窗口,浏览器会导航到该URL。

直到版本5,Firefox都不能正确地把 "url" 映射为 "text/uri-list" 或把 "text" 映射为 "text/plain" 。不过,它可以把 "Text" (第一个字母大写)正确映射为 "text/plain" 。在通过 dataTransfer 获取数据时,为保持最大兼容性,需要对 URL 检测两个值并对文本使用 "Text" :

let dataTransfer = event.dataTransfer; //读取 URL
let url = dataTransfer.getData("url") || dataTransfer.getData("text/uri-list");
// 读取文本
let text = dataTransfer.getData("Text");

这里要注意,首先应该尝试短数据名。这是因为直到版本 10,IE 都不支持扩展的类型名,而且会在遇到无法识别的类型名时抛出错误。


6.4 dropEffect 与 effectAllowed

dataTransfer 对象不仅可以用于实现简单的数据传输,还可以用于确定能够对被拖动元素和放置目标执行什么操作。为此,可以使用两个属性: dropEffecteffectallowed
dropEffect 属性可以告诉浏览器允许哪种放置行为。这个属性有以下4种可能的值。

  • "none":被拖动元素不能放到这里。这是除文本框之外所有元素的默认值。
  • "move":被拖动元素应该移动到放置目标。
  • "copy":被拖动元素应该复制到放置日标。
  • "link":表示放置目标会导航到被拖动元素(仅在它是 URL 的情况下 )。

在把元素拖动到放置目标上时,上述每种值都会导致显示一种不同的光标。不过,是否导致光标示意的动作还要取决于开发者。换句话说,如果没有代码参与,则没有什么会自动移动、复制或链接。唯一不用考虑的就是光标自己会变。为了使用 dropEffect 属性,必须在放置目标的 ondragenter 事件处理程序中设置它。

除非同时设置 effectAllowed ,否则 dropEffect 属性也没有用。effectAllowed 属性表示对被拖动元素是否允许 dropEffect 。这个属性有如下几个可能的值。

  • "uninitialized":没有给被拖动元素设置动作。
  • "none":被拖动元素上没有允许的操作。
  • "copy":只允许"copy"这种 dropEffect。
  • "link":只允许"link"这种 dropEffect。
  • "move":只允许"move"这种 dropEffect。
  • "copyLink":允许"copy"和"link"两种 dropEffect
  • "copyMove":允许"copy"和"move"两种 dropEffect
  • "linkMove":允许"link"和"move"两种 dropEffect
  • "all":允许所有dropEffect。

必须在 ondragstart 事件处理程序中设置这个属性。

假设我们想允许用户把文本从一个文本框拖动到一个 <div> 元素。那么必须同时把 dropEffecteffectAllowed 属性设置为 "move"。因为 <div> 元素上放置事件的默认行为是什么也不做,所以文本不会自动地移动自己。如果覆盖这个默认行为,文本就会自动从文本框中被移除。然后是否把文本插人 <div> 元素就取决于你了.如果是把 dropEffecteffectAllowed 属性设置为 "copy" ,那么文本框中的文本不会自动被移除。


6.5 可拖动能力

默认情况下,图片、链接和文本是可拖动的,这意味着无须额外代码用户便可以拖动它们。文本只有在被选中后才可以拖动,而图片和链接在任意时候都是可以拖动的。
我们也可以让其他素变得可以拖动。HTML5 在所有 HTML元素上规定了一个 draggable 属性表示元素是否可以拖动。图片和链接的 draggable 属性自动被设置为 true,而其他所有元素此属性的默认值为 false。如果想让其他元素可拖动,或者不允许图片和链接被拖动,都可以设置这个属性。例如:

<!-- 禁止拖动图片 -->
<img src="smile.gif" draggable="false" alt="Smiley face" />
<!-- 让元素可以拖动 -->
<div draggable="true">...</div>

6.6 其他成员

HTML5 规范还为 dataTransfer 对象定义了下列方法。

  • addElement(element):为拖动操作添加元素。这纯粹是为了传输数据,不会影响拖动操作的外观。

  • clearData(format):清除以特定格式存储的数据。

  • setDragImage(element, x, y):允许指定拖动发生时显示在光标下面的图片。这个方法接口 ,收3个参数:要显示的 HTML元素及标识光标位置的图片上的x和y坐标。这里的 HTML元素可以是一张图片,此时显示图片;也可以是其他任何元素,此时显示渲染后的元素。

  • types:当前存储的数据类型列表。这个集合类似数组,以字符串形式保存数据类型,比如"text"。



7 Notifications API


Notifications API 用于向用户显示通知。无论从哪个角度看,这里的通知都很类似 alert() 对话框!都使用 JavaScript API 触发页面外部的浏览器行为,而且都允许页面处理用户与对话框或通知弹层的交互。不过,通知提供更灵活的自定义能力。

Notifications API 在 Service Worker 中非常有用。渐进 Web应用(PWA,Progressive Web Application )通过触发通知可以在页面不活跃时向用户显示消息,看起来就像原生应用。

7.1 通知权限

Notifications API 有被滥用的可能,因此默认会开启两项安全措施:

  • 通知只能在运行在安全上下文的代码中被触发
  • 通知必须按照每个源的原则明确得到用户允许

用户授权显示通知是通过浏览器内部的一个对话框完成的。除非用户没有明确给出允许或拒绝的答复,否则这个权限请求对每个域只会出现一次。浏览器会记住用户的选择,如果被拒绝则无法重来。

页面可以使用全局对象 Notification 向用户请求通知权限。这个对象有-个 requestPemission() 方法,该方法返回一个期约,用户在授权对话框上执行操作后这个期约会解决。

Notification.requestPermission().then(permission => {
  console.log("User responded to permission request:", permission); // granted or denied
});

"granted" 值意味着用户明确授权了显示通知的权限。除此之外的其他值意味着显示通知会静默失
败。如果用户拒绝授权,这个值就是 "denied" 。触发授权提示。


7.2 显示和隐藏通知

Notification 构造函数用于创建和显示通知。最简单的通知形式是只显示一个标题,这个标题内容可以作为第一个参数传给 Notification 构造函数。以下面这种方式调用 Notification ,应该会立即显示通知:

new Notification('Title text!');

可以通过 options 参数对通知进行自定义,包括设置通知的主体、图片和振动等。

new Notification("Title text!", {
  body: "Body text!",
  icon: "/images/icon.png",
  vibrate: true
});

调用这个构造函数返回的 Notification 对象的 close() 方法可以关闭显示的通知。下面的例展示了显示通知后 1000 毫秒再关闭它:

const n = new Notification("I will close in 1000ms");
setTimeout(() => n.close(), 1000);

7.3 通知生命周期回调

通知并非只用于显示文本字符串,也可用于实现交互。 Notifications API 提供了4个用于添加回调的生命周期方法:

  • onshow 在通知显示时触发:
  • onclick 在通知被点击时触发;
  • onclose 在通知消失或通过 close()关闭时触发;
  • onerror 在发生错误阻止通知显示时触发。

下面的代码将每个生命周期事件都通过日志打印了出来:

const n = new Notification("foo");

n.onshow = () => console.log("Notification was shown!");
n.onclick = () => console.log("Notification was clicked!");
n.onclose = () => console.log("Notification was closed!");
n.onerror = () => console.log("Notification experienced an error!");


8 Page Visibility API


-个常见的问题是开发者不知道用户什么时候真正在使用页面。如果页面被最小化或隐Web开发中-藏在其他标签页后面,那么轮询服务器或更新动画等功能可能就没有必要了。 PageVisibility API 旨在为开发者提供页面对用户是否可见的信息。

这个 API本身非常简单,由3部分构成。

  • document.visibilityState 值,表示下面4种状态之一
    • 页面在后台标签页或浏览器中最小化了。
    • 页面在前台标签页中。
    • 实际页面隐藏了,但对页面的预览是可见的(例如在Windows7上,用户鼠标移到任务栏图标上会显示网页预览)。
    • 页面在屏外预渲染。
  • visibilitychange 事件,该事件会在文档从隐藏变可见(或反之)时触发。
  • document.hidden 布尔值,表示页面是否隐藏。这可能意味着页面在后台标签页或浏览器中被最小化了。这个值是为了向后兼容才继续被测览器支持的,应该优先使用 document.visibilitystate 检测页面可见性。

要想在页面从可见变为隐藏或从隐藏变为可见时得到通知,需要监听 visibilitychange 事件。
document.visibilityState 的值是以下三个字符串之一:

  • "hidden"
  • "visible"
  • "prerender"


9 Streams API


Streams API 是为了解决一个简单但又基础的问题而生的:Web 应用如何消费有序的小信息块而不是大块信息?这种能力主要有两种应用场景。

  • 大块数据可能不会一次性都可用。网络请求的响应就是一个典型的例子。网络负载是以连续信息包形式交付的,而流式处理可以让应用在数据一到达就能使用,而不必等到所有数据都加载完毕。
  • 大块数据可能需要分小部分处理。视频处理、数据压缩、图像编码和 JSON 解析都是可以分成小部分进行处理,而不必等到所有数据都在内存中时再处理的例子。

9.1 理解流

提到流,可以把数据想像成某种通过管道输送的液体。JavaScript 中的流用了管道相关的概念因头原理是相通的。根据规范,“这些 API实际是为映射低级 I/O 原语而设计,包括适当时候对字节流的规范化”。 StreamAPI 直接解决的问题是处理网络请求和读写磁盘。

Stream API定义了三种流。

  • 可读流: 可以通过某个公共接口读取数据块的流。数据在内部从底层源进入流,然后由消费者(consumer)进行处理。
  • 可写流: 可以通过某个公共接口写人数据块的流。生产者(producer)将数据写人流,数据在内部传人底层数据槽(sink)。
  • 转换流: 由两种流组成,可写流用于接收数据(可写端),可读流用于输出数据(可读端)。这两个流之间是转换程序(transformer),可以根据需要检查和修改流内容。

块、内部队列和反压
流的基本单位是块(chunk)。块可是任意数据类型,但通常是定型数组。 每个块都是离散的流片段可以作为一个整体来处理。更重要的是,块不是固定大小的,也不一定按固定间隔到达。在理想的流当中,块的大小通常近似相同,到达间隔也近似相等。不过好的流实现需要考虑边界情况。

前面提到的各种类型的流都有入口和出口的概念。有时候,由于数据进出速率不同,可能会出现不匹配的情况。为此流平衡可能出现如下三种情形。

  • 流出口外理数据的速度比入口提供数据的速度快。流出口经常空闲(可能意味着流人口效率较低)但只会浪费一点内存或计算资源,因此这种流的不平衡是可以接受的。
  • 流人和流出均衡。这是理想状态。
  • 流入口提供数据的速度比出口处理数据的速度快。这种流不平衡是固有的问题。此时一定会在某个地方出现数据积压,流必须相应做出处理。

流不平衡是常见问题,但流也提供了解决这个问题的工具。所有流都会为已进人流但尚未离开流的块提供一个内部队列。对于均衡流,这个内部队列中会有零个或少量排队的块,因为流出口块出列的速度与流入口块入列的速度近似相等。这种流的内部队列所占用的内存相对比较小。

如果块入列速度快于出列速度,则内部队列会不断增大。流不能允许其内部队列无限增大,因此它会使用反压(backpressure)通知流人口停止发送数据,直到队列大小降到某个既定的闽值之下。这个值由排列策略决定,这个策略定义了内部队列可以占用的最大内存,即高水位线(high water mark)。


9.2 可读流 ReadableStream

Stream API 中的 ReadableStream 接口表示可读的字节数据流。Fetch API 通过 Response 的属性 body 提供了一个具体的 ReadableStream 对象。

可读流 ReadableStream 是对 底层数据源 underlyingSource 的封装。底层数据源 underlyingSource 可以将数据填充到流中,允许消费者通过流的公共接口读取数据。

9.2.1 构造函数

创建并从给定的处理程序返回一个可读流对象。

语法
// 返回ReadableStream对象的实例。
new ReadableStream();
new ReadableStream(underlyingSource?);
new ReadableStream(underlyingSource?, queuingStrategy?);
第一个参数 underlyingSource(底层数据源 可选)

underlyingSource 是一个底层的数据源,它为 可读流 ReadableStream 提供数据。当 可读流 ReadableStream 需要数据时,它会从 底层数据源 underlyingSource 中获取数据并将其提供给消费者。 底层数据源 underlyingSource 可以是任何能够提供数据的数据源,例如一个文件、一个网络请求或一个内存中的数据缓冲区。

underlyingSource 为一个包含定义了构造流行为方法和属性的对象。underlyingSource对象 包括:

  • start(controller)? :这是一个当 underlyingSource 对象被构造时立刻调用的方法,应着眼于访问流,并执行其他任何必需的设置流功能。如果这个过程是异步完成的,它可以返回一个 promise ,表明成功或失败。 controller 参数传入 ReadableStreamDefaultController 详见9.2.4。

  • pull(controller)? : 当流的内部队列不满时会重复调用这个方法直到队列补满。如果 pull() 返回一个 promise ,那么它将不会再被调用,直到 promise 完成;如果 promise 失败,该流将会出现错误。controller 参数传入 ReadableStreamDefaultController 详见9.2.4。

  • cancel(reason)? :该方法用于取消流。如果这个过程是异步的,它可以返回一个 promise ,表明成功或失败。reason 原因参数包含一个 DOMString,它描述了流被取消的原因。

  • type? : 该属性控制正在处理的可读类型的流。如果它包含一个设置为 bytes 的值,则传递的控制器对象将是一个 ReadableByteStreamController (en-US),能够处理 BYOB(带你自己的缓冲区)/字节流。如果未包含,则传递的控制器将默认为 ReadableStreamDefaultController

  • autoAllocateChunkSize? :对于字节流,开发人员可以使用正整数值设置 autoAllocateChunkSize 以打开流的自动分配功能。启用此功能后,流实现将自动分配一个具有给定整数大小的 ArrayBuffer ,并调用底层源代码,就好像消费者正在使用 BYOB reader 一样。

第二个参数 queuingStrategy(队列策略 可选)

queuingStrategy 是一个队列策略,用于确定如何将数据存储在队列中,以便稍后提供给消费者。它决定了数据在队列中的存储方式和优先级。不同的queuingStrategy 可能会导致不同的性能和行为。

一个可选择定义流的队列策略的对象。这需要两个参数,两者乘积为缓存中最大数据量:

  • highWaterMark :非负整数 - 可读流队列中可以缓存的最大数据量,定义了在应用背压之前可以包含在内部队列中的块的总数。

  • size(chunk) :包含参数 chunk 的方法——这表示每个分块使用的大小(以字节为单位)。

9.2.2 实例属性

属性 说明
ReadableStream.locked locked 返回该可读流是否被锁定到一个 reader。(只读 )

9.2.3 实例方法

方法 说明
ReadableStream.cancel(reason?) 完全结束一个流,方法返回一个 Promise ,这个 Promise 会在流被取消的时候兑现 reason 。可以传入 reason 参数表示人类可读的取消原因,底层源代码可能会使用它。
ReadableStream.getReader(mode?) 方法会创建一个 reader ,并将流锁定。只有当前 reader 将流释放后,其他 reader 才能使用。 mode 参数值为 DOMString 类型,用来指定要创建的 reader 的类型 "byob"|undefined
ReadableStream.pipeThrough(transformStream,options?) 方法提供了一种链式的方式,将当前流通过转换流或者其他任何一对可写/可读的流进行管道传,传输一个流通常在管道传输的时间内锁定这个流,以阻止其他 reader 锁定它。
ReadableStream.pipeTo(destination,option?) 方法通过管道将当前的 ReadableStream 中的数据传递给给定的 WritableStream 并且返回一个 PromisePromise 在传输成功完成时兑现,在遇到任何错误时则会被拒绝。传输一个流通常在管道传输的时间内锁定这个流,以阻止其他 reader 锁定它。
ReadableStream.tee() 方法对当前的可读流进行拷贝(tee),返回包含两个 ReadableStream 实例分支的数组。

9.2.4 可读流控制器 ReadableStreamDefaultController(controller)

Stream API 中的 ReadableStreamDefaultController 接口是一个控制器,该控制器允许控制 ReadableStream 的状态和内部队列。默认控制器用于不是字节流的流(ReadableStream 属性 type相关)。

9.2.4.1 构造函数

无。 ReadableStreamDefaultController 实例会在构造 ReadableStream 时被自动创造。

访问这个控制器最简单的方式就是创建 ReadableStream 的一个实例,并在这个构造函数的underlyingSource参数(第一个参数底层数据源)中定义的 start() 方法,然后在这个方法中使用作为参数传入的 controller 。默认情况下,这个控制器参数(controller)是ReadableStreamDefaultController 的一个实例:

const readablestream = new ReadableStream({
  start(controller) {
    console.log(controller); // controller 就是 ReadableStreamDefaultController
  }
});

9.2.4.2 实例属性

属性 说明
ReadableStreamDefaultController.desiredSize 回填充满流的内部队列所需要的大小,只读。

9.2.4.3 实例方法

方法 说明
ReadableStreamDefaultController.enqueue(chunk) 调用控制器的 enqueue() 方法可以把数据块chunk传入控制器
ReadableStreamDefaultController.close() 所有值都传完之后,调用 close() 关闭流
ReadableStreamDefaultController.error(error) 所导致未来任何与关联流交互都会出错。

9.2.5 默认流读取器 ReadableStreamDefaultReader

为此,需要一个 ReadableStreamDefaultReader 的实例,该实例可以通过流实例 ReadableStreamgetReader() 方法获取。调用这个方法会获得流的锁,保证只有这个读取器可以从流中读取值:

9.2.5.1 构造函数

你通常不需要手动创建,可以使用 ReadableStream.getReader() 方法代替。

// 通常不这么做,直接使用流实例的方法ReadableStream.getReader()
new ReadableStreamDefaultReader(stream);

9.2.5.2 实例属性

属性(只读) 说明
ReadableStreamDefaultReader.closed 返回一个 promise ,该 promise 在流关闭时兑现,如果流抛出错误或 reader 的锁被释放,则拒绝。此属性使你能够编写响应流过程结束时执行的代码。

9.2.5.3 实例方法

方法 说明
ReadableStreamDefaultReader.read() 返回一个 promise ,提供对流内部队列中下一个分块的访问权限。如果有分块可用,则 promise 将使用 { value: theChunk, done: false } 形式的对象来兑现。如果流已经关闭,则 promise 将使用 { value: undefined, done: true } 形式的对象来兑现。如果流发生错误, promise 将因相关错误被拒绝。
ReadableStreamDefaultReader.cancel(reason) 方法返回一个 promise ,这个 promise 在流被取消时兑现。消费者在流中调用该方法发出取消流的信号。cancel 用于在不再需要来自一个流的任何数据的情况下完全结束这个流,即使仍有排队等待的数据块。调用 cancel 后该数据丢失,并且流不再可读。为了仍然可以读这些数据块而不完全结束这个流,你应该使用 ReadableStreamDefaultController.close()
ReadableStreamDefaultReader.releaseLock() 方法用于释放 reader 对流的锁定

9.2.6 可读流相关示例

使用 for await...of 对流进行异步迭代
async 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();
    }
  });
}
转换异步迭代器到流
function iteratorToStream(iterator) {
  return new ReadableStream({
    // pull()方法当流的内部队列不满时会重复调用这个方法直到队列补满
    async pull(controller) {
      const { value, done } = await iterator.next();
      if (done) {
        controller.close();
      } else {
        controller.enqueue(value);
      }
    }
  });
}
获取流读取器并读取值
async function* ints() {
  //每1000毫秒生成一个递增的整数
  for (let i = 0; i < 5; ++i) {
    yield await new Promise(resolve => setTimeout(resolve, 1000, i));
  }
}

const readableStream = new ReadableStream({
  // 将5 个值加入流的队列中
  async start(controller) {
    for await (let chunk of ints()) {
      // 调用控制器的enqueue()方法可以把数据块chunk传入控制器
      controller.enqueue(chunk);
    }
    // 所有值都传完之后,调用控制器的close()方法可以关闭流
    controller.close();
  }
});
console.log(readableStream.locked); // 检查流是否被锁定,false
const readableStreamDefaultReader = readableStream.getReader(); // 获取默认流读取器
console.log(readableStream.locked); // 因为当前流在读取中锁定流,true

// 消费者 从队列把值读出
(async function () {
  while (true) {
    // 消费者使用这个读取器实例的 `read()` 方法可以读出值:
    const { done, value } = await readableStreamDefaultReader.read();
    if (done) {
      break;
    } else {
      console.log(value);
    }
  }
})();
随机流展示
// Store reference to lists, paragraph and button
const list1 = document.querySelector(".input ul");
const list2 = document.querySelector(".output ul");
const para = document.querySelector("p");
const button = document.querySelector("button");

// 创建变量存储字符串
let result = "";

// 生成随机字符串
function randomChars() {
  let string = "";
  let choices = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()";

  for (let i = 0; i < 8; i++) {
    string += choices.charAt(Math.floor(Math.random() * choices.length));
  }
  return string;
}

const stream = new ReadableStream({
  // start() 函数每秒生成一个随机字符串并且将他们送入流中。
  start(controller) {
    interval = setInterval(() => {
      let string = randomChars();
      // enqueue()方法将随机字符串添加到流
      controller.enqueue(string);
      // 在屏幕上展示
      let listItem = document.createElement("li");
      listItem.textContent = string;
      list1.appendChild(listItem);
    }, 1000);

    // 当按下按钮时,将停止生成,使用 ReadableStreamDefaultController.close() 关闭流,并运行另一个将数据读回流中的函数。
    button.addEventListener("click", function () {
      clearInterval(interval);
      fetchStream();
      controller.close();
    });
  },
  // 在这个例子中不使用 pull
  pull(controller) {},
  // cancel() 函数用于在 ReadableStream.cancel() 被调用时停止随机字符串的生成。
  cancel() {
    // 如果取消了 reader,则会调用该函数,
    // 所以我们应该在这里停止生成字符串
    clearInterval(interval);
  }
});

// 在读取 ReadableStream 前,使用 getReader() 创建一个 ReadableStreamDefaultReader。
// 按顺序读取每个分块,并传递给 UI,当整个流被读取完毕后,从递归方法中退出,并在 UI 的另一部分输出整个流。
function fetchStream() {
  const reader = stream.getReader();
  let charsReceived = 0;

  // read() 返回了一个 promise
  // 当数据被接收时 resolve
  reader.read().then(function processText({ done, value }) {
    // Result 对象包含了两个属性:
    // done  - 当 stream 传完所有数据时则变成 true
    // value - 数据片段。当 done 变为 true 时,value始终为 undefined
    if (done) {
      console.log("Stream complete");
      para.textContent = value;
      return;
    }

    // value for fetch streams is a Uint8Array
    charsReceived += value.length;
    const chunk = value;
    let listItem = document.createElement("li");
    listItem.textContent = "Received " + charsReceived + " characters so far. Current chunk = " + chunk;
    list2.appendChild(listItem);

    result += chunk;

    // 再次调用这个函数以读取更多数据
    return reader.read().then(processText);
  });
}
创建了一个智能的 Response 将从另一个资源获取的 HTML 片段流式的传输到浏览器。
fetch("https://www.example.org")
  .then(response => response.body)
  .then(rb => {
    const reader = rb.getReader();
    return new ReadableStream({
      start(controller) {
        // The following function handles each data chunk
        function push() {
          // "done" is a Boolean and value a "Uint8Array"
          reader.read().then(({ done, value }) => {
            // If there is no more data to read
            if (done) {
              console.log("done", done);
              controller.close();
              return;
            }
            // Get the data and send it to the browser via the controller
            controller.enqueue(value);
            // Check chunks by logging to the console
            console.log(done, value);
            push();
          });
        }
        push();
      }
    });
  })
  .then(stream =>
    // Respond with our stream
    new Response(stream, { headers: { "Content-Type": "text/html" } }).text()
  )
  .then(result => {
    // Do things with result
    console.log(result);
  });

9.3 可写流 WritableStream

Stream API 的 WritableStream 接口为将流数据写入目的地(称为接收器)提供了一个标准的抽象。该对象带有内置的背压和队列。

可写流 WritableStream底层数据槽 underlyingSink 的封装。底层数据槽 underlyingSink 处理通过流的公共接口写人的数据。

9.3.1 构造函数

创造一个新的 WritableStream 对象。

语法
// 返回WritableStream对象的实例。
new WritableStream(underlyingSink?);
new WritableStream(underlyingSink?, queuingStrategy?);
第一个参数 underlyingSink(底层数据槽 可选)

underlyingSink 是一个底层数据槽,属于流的实际进行数据传输的部分,用于接收数据并将其写入某个目的地(如文件、网络连接、内存缓冲区等)的接口或能力。在前端开发中,这通常涉及到将数据从 underlyingSource 源流传输到目标流,例如将数据从网络请求传输到浏览器或用户界面 underlyingSink 通常与 underlyingSource 相对应,后者是提供数据的源头。

underlyingSink 为一个包含定义了构造流行为方法和属性的对象。underlyingSink对象 包括:

  • start(controller)? :这是一个当 underlyingSink 对象被构造时立刻调用的方法,应着眼于访问流,并执行其他任何必需的设置流功能。如果这个过程是异步完成的,它可以返回一个 promise ,表明成功或失败。 传入 WritableStreamDefaultController 详见9.2.4。

  • write(chunk, controller)? :当一个新的数据块(指定为 chunk 参数传入)准备好写入底层接收器时,将调用此方法。它可以返回一个 promise 来表示写入操作的成功或者失败。controller参数传入 WritableStreamDefaultController

  • close(controller)? :这个方法只有在所有等待的写入操作都成功后才会被调用。controller 参数传入 WritableStreamDefaultController

  • abort(reason)? : 调用此方法清理任何被占用的资源,就像 close() 一样,但是即使存在等待的写入操作,abort() 也将被调用——那些分块将被丢弃。如果这个过程是异步完成的,它可以返回一个 promise,以表明操作成功或失败。reason 参数包含一个字符串,用于指定流被中止的原因。

第二个参数 underlyingSource(队列策略 可选)

一个可选择定义流的队列策略的对象。这需要两个参数,两者乘积为缓存中最大数据量:

  • highWaterMark :非负整数 - 可读流队列中可以缓存的最大数据量,定义了在应用背压之前可以包含在内部队列中的块的总数。

  • size(chunk) :包含参数 chunk 的方法——这表示每个分块使用的大小(以字节为单位)。

9.3.2 实例属性

属性 说明
WritableStream.locked locked 返回该可写流是否被锁定到一个 writer。(只读 )

9.3.3 实例方法

方法 说明
WritableStream.abort() 方法用于中止流,表示生产者不能再向流写入数据(会立刻返回一个错误状态),并丢弃所有已入队的数据。
WritableStream.getWriter() 方法会创建一个 writer ,并将流锁定。只有当前 writer 将流释放后,其他 writer 才能使用。 mode 参数值为 DOMString 类型,用来指定要创建的 reader 的类型 "byob"|undefined
ReadableStream.pipeThrough(transformStream,options?) 方法提供了一种链式的方式,将当前流通过转换流或者其他任何一对可写/可读的流进行管道传,传输一个流通常在管道传输的时间内锁定这个流,以阻止其他 reader 锁定它。
ReadableStream.pipeTo(destination,option?) 方法通过管道将当前的 ReadableStream 中的数据传递给给定的 WritableStream 并且返回一个 PromisePromise 在传输成功完成时兑现,在遇到任何错误时则会被拒绝。传输一个流通常在管道传输的时间内锁定这个流,以阻止其他 reader 锁定它。

9.3.4 可写流控制器 WritableStreamDefaultController(controller)

Stream API 中的 WritableStreamDefaultController 接口表示一个允许控制 WritableStream 状态的控制器。当构造 WritableStream 时,会为底层的接收器提供一个相应的 WritableStreamDefaultController 实例以进行操作。

9.3.4.1 构造函数

无。 WritableStreamDefaultController 实例会在构造 WritableStream 时被自动创建。

访问这个控制器最简单的方式就是创建 WritableStream 的一个实例,并在这个构造函数的underlyingSink参数(第一个参数底层数据源)中定义的 start() 方法,然后在这个方法中使用作为参数传入的 controller 。默认情况下,这个控制器参数(controller)WritableStreamDefaultController 的一个实例:

const writableStream = new WritableStream({
  start(controller) {
    console.log(controller); // controller 就是 WritableStreamDefaultController()
  }
});

9.3.4.2 实例属性

属性 说明
WritableStreamDefaultController.signal 返回与 controller 相关联的 AbortSignal 。,只读。

9.3.4.3 实例方法

方法 说明
WritableStreamDefaultController.error() 导致未来任何与关联的流的交互都会出错。

9.3.5 可写流写入器 WritableStreamDefaultWriter(reader)

为此,需要一个 WritableStreamDefaultWriter 的实例,该实例可以通过流实例 WritableStreamgetWriter() 方法获取。调用这个方法会获得流的锁,保证只有这个写入器可以写入底层sink:

9.3.5.1 构造函数

你通常不需要手动创建,可以使用 ReadableStream.getReader() 方法代替。

// 通常不这么做,直接使用流实例的方法ReadableStream.getReader()
new WritableStreamDefaultWriter(stream);

9.3.5.2 实例属性

属性(只读) 说明
WritableStreamDefaultWriter.closed 返回一个 promise ,这个 promise 在流关闭时兑现,而在流抛出错误或者 writer 的锁被释放时拒绝。
WritableStreamDefaultWriter.desiredSize 返回填充满流的内部队列需要的大小。
WritableStreamDefaultWriter.ready 返回一个 Promise ,当流填充内部队列的所需大小从非正数变为正数时兑现,表明它不再应用背压。

9.3.5.3 实例方法

方法 说明
WritableStreamDefaultWriter.abort(reason?) 中止流,表示生产者不能再向流写入数据(会立刻返回一个错误状态),并丢弃所有已入队的数据。
WritableStreamDefaultWriter.close() 关闭关联的可写流。
WritableStreamDefaultWriter.releaseLock() 释放 writer 对相应流的锁定。释放锁后, writer 将不再处于锁定状态。如果释放锁时关联流出错, writer 随后也会以同样的方式发生错误;此外, writer 将关闭。
WritableStreamDefaultWriter.write(chunk) 将传递的数据块写入 WritableStream 和它的底层接收器,然后返回一个 promisepromise 的状态由写入操作是否成功来决定。

9.3.6 可写流相关示例

写入流写入数据
async function* ints() {
  //每1000毫秒生成一个递增的整数
  for (let i = 0; i < 5; ++i) {
    yield await new Promise(resolve => setTimeout(resolve, 1000, i));
  }
}

const writableStream = new WritableStream({
  // 这些值通过可写流的公共接口可以写人流。
  // 在传给 WritableStream 构造函数的 underlyingSink 参数中,通过实现 `write()` 方法可以获得写人的数据:
  write(value) {
    console.log(value);
  }
});
console.log(writableStream.locked); // 检查写入流是否被锁定,false
const writableStreamDefaultWriter = writableStream.getWriter(); // 获取默认流写入器,获取流的锁保证只有一个写入器向流中写入数据
console.log(writableStream.locked); // 因为当前流在写入中锁定,true

// 生产者 写入值
(async function () {
  for await (let chunk of ints()) {
    // ready()返回一个promise 确保能够像流写入数据
    await writableStreamDefaultWriter.ready;
    // 写入数据
    writableStreamDefaultWriter.write(chunk);
  }
  // 写完可写流
  writableStreamDefaultWriter.close();
})();
自定义 sink 和由 API 提供的队列策略创建的 WritableStream
const list = document.querySelector("ul");

function sendMessage(message, writableStream) {
  // 获取默认写入器
  const defaultWriter = writableStream.getWriter();
  const encoder = new TextEncoder();
  const encoded = encoder.encode(message, { stream: true });
  encoded.forEach(chunk => {
    // 第一种ready用法, 确保 WritableStream 已完成写入,因此能够保证其已经准备好继续接收新的数据。
    defaultWriter.ready
      .then(() => {
        return defaultWriter.write(chunk);
      })
      .then(() => {
        console.log("Chunk written to sink.");
      })
      .catch(err => {
        console.log("Chunk error:", err);
      });
  });

  // 这里的ready 和上面不同,这里是为了确定
  // 第二种ready用法, 也检查 WritableStream 是否完成写入,但是这一次是因为确保全部写入操作必须在 writer 关闭之前完成。
  defaultWriter.ready
    .then(() => {
      defaultWriter.close();
    })
    .then(() => {
      console.log("All chunks written");
    })
    .catch(err => {
      console.log("Stream error:", err);
    });
}

// 这行代码创建了一个新的TextDecoder实例,用于将二进制数据解码为文本。这里使用的是UTF-8编码。
const decoder = new TextDecoder("utf-8");
// 设置 写入的 队列策略,只允许同时写入一个
const queuingStrategy = new CountQueuingStrategy({ highWaterMark: 1 });
let result = "";
const writableStream = new WritableStream(
  {
    // 这个方法接收一个数据块(chunk),并解码它。解码后的数据被添加到一个HTML列表中,并累加到result字符串中。这个方法返回一个Promise,当数据块被处理完毕后,Promise会被解析。
    write(chunk) {
      return new Promise((resolve, reject) => {
        var buffer = new ArrayBuffer(1);
        var view = new Uint8Array(buffer);
        view[0] = chunk;
        var decoded = decoder.decode(view, { stream: true });
        var listItem = document.createElement("li");
        listItem.textContent = "Chunk decoded: " + decoded;
        list.appendChild(listItem);
        result += decoded;
        resolve();
      });
    },
    // 当流关闭时,这个方法会被调用
    close() {
      // 它创建一个新的HTML列表项,显示接收到的所有数据,并将其添加到列表中。
      var listItem = document.createElement("li");
      listItem.textContent = "[MESSAGE RECEIVED] " + result;
      list.appendChild(listItem);
    },
    // 当流发生错误时,这个方法会被调用。
    abort(err) {
      console.log("Sink error:", err);
    }
  },
  // 添加队列策略
  queuingStrategy
);
sendMessage("Hello, world.", writableStream);

9.4 转换流 TransformStream

Stream API 的 TransformStream 接口表示链式管道传输(pipe chain)转换流(transform stream)概念的具体实现。

它可以传递给 ReadableStream.pipeThrough() 方法,以便将流数据从一种格式转换成另一种。例如,它可以用于解码(或者编码)视频帧,解压缩数据或者将流从 XML 转换到 JSON。

转换流用于组合可读流和可写流。数据块在两个流之间的转换是通过 transform() 方法完成的。

9.4.1 构造函数

构造函数创建一个新的 TransformStream 对象。

语法
// 该对象表示一对流:一个 WritableStream 表示可写端,和一个 ReadableStream 表示可读端。
new TransformStream();
new TransformStream(transformer?);
new TransformStream(transformer?, writableStrategy?);
new TransformStream(transformer?, writableStrategy?, readableStrategy?);
第一个参数 transformer(转换器 可选)

一个表示 transformer 的对象。如果未提供,则生成的流将是一个恒等变换流,它将所有写入可写端的分块转发到可读端,不会有任何改变。

transformer 对象可以包含以下任何方法。每个方法的 controller 都是一个 TransformStreamDefaultController 实例。

  • start(controller) :当 TransformStream 被构造时调用。它通常用于使用 TransformStreamDefaultController.enqueue() 对分块进行排队。

  • transform(chunk, controller) :当一个写入可写端的分块准备好转换时调用,并且执行转换流的工作。如果没有提供 transform() 方法,则使用恒等变换,并且分块将在没有更改的情况下排队。

  • flush(controller) :当所有写入可写端的分块成功转换后被调用,并且可写端将会关闭。

第二个参数 writableStrategy(写入队列策略 可选)

一个定义了队列策略的可选对象。它需要两个参数:

  • highWaterMark :非负整数 - 可读流队列中可以缓存的最大数据量,定义了在应用背压之前可以包含在内部队列中的块的总数。

  • size(chunk) :包含参数 chunk 的方法——这表示每个分块使用的大小(以字节为单位)。

第三个参数 readableStrategy(读取队列策略 可选)

一个定义了队列策略的可选对象。它需要两个参数:

  • highWaterMark :非负整数 - 可读流队列中可以缓存的最大数据量,定义了在应用背压之前可以包含在内部队列中的块的总数。

  • size(chunk) :包含参数 chunk 的方法——这表示每个分块使用的大小(以字节为单位)。

9.4.2 实例属性

属性 说明
TransformStream.readable 转换流的 readable 端。(只读 )
TransformStream.writable 转换流的 writable 端。(只读 )

9.4.3 实例方法

9.4.4 转换流控制器 TransformStreamDefaultController

Stream API 的 TransformStreamDefaultController 接口提供了操作关联的 ReadableStream 和 WritableStream 的方法。

9.4.4.1 构造函数

无。TransformStreamDefaultController 实例会在构造 ReadableStream 时被自动创造。

访问这个控制器最简单的方式就是创建 TransformStream 的一个实例,并在这个构造函数的transformer(第一个参数转换器)中定义的 start() 方法,然后在这个方法中使用作为参数传入的 controller 。默认情况下,这个控制器参数(controller)是TransformStreamDefaultController 的一个实例:

const transformStream = new TransformStream({
  start(controller) {
    console.log(controller); // controller 就是 TransformStreamDefaultController
  }
});

9.4.4.2 实例属性

属性 说明
TransformStreamDefaultController.desiredSize 返回填充满流内部队列的可读端所需要的大小,只读。

9.4.4.3 实例方法

方法 说明
TransformStreamDefaultController.enqueue(chunk) 调用控制器的 enqueue() 方法可以把数据块chunk传入控制器
TransformStreamDefaultController.terminate() 转换流的可写端和可读端都出现错误。
TransformStreamDefaultController.error(error) 关闭流的可读端并且流的可写端出错。

9.4.5 转换流相关例子

将文本逐块转换为大写
function upperCaseStream() {
  return new TransformStream({
    transform(chunk, controller) {
      controller.enqueue(chunk.toUpperCase());
    }
  });
}

function appendToDOMStream(el) {
  return new WritableStream({
    write(chunk) {
      el.append(chunk);
    }
  });
}

fetch("./lorem-ipsum.txt").then(response =>
  response.body.pipeThrough(new TextDecoderStream()).pipeThrough(upperCaseStream()).pipeTo(appendToDOMStream(document.body))
);
每个值翻倍

向转换流的组件流(可读流和可写流)传人数据和从中获取数据,与本章前面介绍的方法相同:

async function* ints() {
  //每1000毫秒生成一个递增的整数
  for (let i = 0; i < 5; ++i) {
    yield await new Promise(resolve => setTimeout(resolve, 1000, i));
  }
}

const { writable, readable } = new TransformStream({
  transform(chunk, controller) {
    controller.enqueue(chunk * 2);
  }
});

const readableStreamDefaultReader = readable.getReader();
const writableStreamDefaultWriter = writable.getWriter();

// 消费者
(async function () {
  while (true) {
    const { done, value } = await readableStreamDefaultReader.read();
    if (done) {
      break;
    } else {
      console.log(value);
    }
  }
})();

//生产者
(async function () {
  for await (let chunk of ints()) {
    await writableStreamDefaultWriter.ready;
    writableStreamDefaultWriter.write(chunk);
  }
  writablestreamDefaultWriter.close();
})();

9.5 通过管道连接流

9.5.1 pipeThrough() 方法

ReadableStream 接口的 pipeThrough() 方法提供了一种链式的方式,将当前流通过转换流或者其他任何一对可写/可读的流进行管道传输。

流可以通过管道连接成一串。最常见的用例是使用 pipeThrough() 方法把 ReadableStream 接人TransformStream 。从内部看,ReadableStream 先把自己的值传给 TransformStream 内部的 WritableStream ,然后执行转换,接着转换后的值又在新的 ReadableStream 上出现。

语法
// 返回transformStream 的 readable 端。
pipeThrough(transformStream);
pipeThrough(transformStream, options);

参数

  • transformStream : 由一对可读流和可写流组成的 TransformStream(或者结构为 {writable, readable} 的对象),它们共同工作以对数据进行转换。 writable 流写入的数据在某些状态下可以被 readable 流读取。例如,向 TextDecoder 写入字节并从中读取字符串,而视频解码器则是写入编码的字节数据,并从中读取解压后的视频帧。

  • options? :传输至 writable 流应该被使用的选项。可用选项是:

    • preventClose :如果设置为 true,源 ReadableStream 的关闭将不再导致目标 WritableStream 关闭。一旦此过程完成,该方法返回的 promise 将被兑现;除非在关闭目标流时遇到错误,在这种情况下,它将因为该错误被拒绝。
    • preventAbort :如果设置为 true,源 ReadableStream 中的错误将不再中止目标 WritableStream。该方法返回的 promise 将因源流的错误或者任何在中止目地流期间的错误而被拒绝。
    • preventClose :如果设置为 true,目标 WritableStream 的错误将不再取消源 ReadableStream。在这种情况下,该方法返回的 promise 将因源流的错误或者在取消源流期间发生的任何错误而被拒绝。此外,如果目标可写流开始关闭或者正在关闭,则源可读流将不再被取消。在这种情况下,方法返回的 promise 也将被拒绝,其错误为连接到一个已关闭的流或者在取消源流期间发生的任何错误。
    • preventClose :用于设置一个 AbortSignal 对象,然后可以通过相应的 AbortController 中止正在进行的传输操作。

9.5.2 pipeTo() 方法

ReadableStream 接口的 pipeTo() 方法通过管道将当前的 ReadableStream 中的数据传递给给定的 WritableStream 并且返回一个 Promise,promise 在传输成功完成时兑现,在遇到任何错误时则会被拒绝。

语法
// 返回一个 Promise,其在传输完成时兑现。
pipeTo(destination);
pipeTo(destination, options);

参数

  • destination :充当 ReadableStream 最终目标的 WritableStream。

  • options? :传输至 writable 流应该被使用的选项。可用选项是:

    • preventClose :如果设置为 true,源 ReadableStream 的关闭将不再导致目标 WritableStream 关闭。一旦此过程完成,该方法返回的 promise 将被兑现;除非在关闭目标流时遇到错误,在这种情况下,它将因为该错误被拒绝。
    • preventAbort :如果设置为 true,源 ReadableStream 中的错误将不再中止目标 WritableStream。该方法返回的 promise 将因源流的错误或者任何在中止目地流期间的错误而被拒绝。
    • preventClose :如果设置为 true,目标 WritableStream 的错误将不再取消源 ReadableStream。在这种情况下,该方法返回的 promise 将因源流的错误或者在取消源流期间发生的任何错误而被拒绝。此外,如果目标可写流开始关闭或者正在关闭,则源可读流将不再被取消。在这种情况下,方法返回的 promise 也将被拒绝,其错误为连接到一个已关闭的流或者在取消源流期间发生的任何错误。
    • preventClose :用于设置一个 AbortSignal 对象,然后可以通过相应的 AbortController 中止正在进行的传输操作。

9.5.3 示例

使用 fetch 获取图像并将响应的 body 作为 ReadableStream 。记录可读流的内容,并且使用 pipeThrough() 将它发送到一个转换灰度图的转换流,然后记录新的流中的内容。

// 通过 fetch 获取原始图像
function logReadableStream(name, rs) {
  const [rs1, rs2] = rs.tee();

  rs2.pipeTo(new WritableStream(new LogStreamSink(name))).catch(console.error);

  return rs1;
}

fetch("png-logo.png")
  // 将响应的 body 作为 ReadableStream
  .then(response => response.body)
  .then(rs => logReadableStream("Fetch Response Stream", rs))
  // 从原始图像创造一个 PNG 的灰度图像
  .then(body => body.pipeThrough(new PNGTransformStream()))
  .then(rs => logReadableStream("PNG Chunk Stream", rs));

下面的例子将一个整数的 Readablestream 传人 TransformStream , TransformStream 对每个值做加倍处理

async function* ints() {
  //每1000毫秒生成一个递增的整数
  for (let i = 0; i < 5; ++i) {
    yield await new Promise(resolve => setTimeout(resolve, 1000, i));
  }
}

// 创建一个可读流 整数流
const integerStream = new ReadableStream({
  async start(controller) {
    for await (let chunk of ints()) {
      controller.enqueue(chunk);
    }
    controller.close();
  }
});

// 创建一个转换流 对数据块chunk作翻倍处理
const doublingStream = new TransformStream({
  transform(chunk, controller) {
    controller.enqueue(chunk * 2);
  }
});

// 通过管道连接流,将可读流的pipeThrough()接入转换流
const pipedStream = integerStream.pipeThrough(doublingStream);

// 从连接流的输出获得读取器
const pipedStreamDefaultReader = pipedStream.getReader();

// 消费者
(async function () {
  while (true) {
    const { done, value } = await pipedStreamDefaultReader.read();
    if (done) {
      break;
    } else {
      console.log(value);
    }
  }
})();

// 0
// 2
// 4
// 6
// 8

另外,使用 pipeTo() 方法也可以将 ReadableStream 连接到 WritableStream 。整个过程与使用 pipeThrough() 类似:

async function* ints() {
  //每1000毫秒生成一个递增的整数
  for (let i = 0; i < 5; ++i) {
    yield await new Promise(resolve => setTimeout(resolve, 1000, i));
  }
}

// 创建一个可读流 整数流
const integerStream = new ReadableStream({
  async start(controller) {
    for await (let chunk of ints()) {
      controller.enqueue(chunk);
    }
    controller.close();
  }
});

// 创建一个可写流
const writableStream = new WritableStream({
  write(value) {
    console.log(value);
  }
});

// 通过管道连接流 将可读流的pipeTo()接入可写流
const pipedStream = integerStream.pipeTo(writableStream);
// 0
// 1
// 2
// 3
// 4

注意,这里的管道连接操作隐式从 ReadableStream 获得了一个读取器,并把产生的值填充到 WritableStream



10 计时 API

终是 Web 开发者关心的话题。 Performance 接口通过 JavaScriptAPI暴露了浏览器内部的度量指标,允许开发者直接访问这些信息并基于这些信息实现自己想要的功能。这个接口暴露在 window.performance 对象上。所有与页面相关的指标,包括已经定义和将来会定义的,都会存在于这个对象上。

Performance接口由多个 API构成.

  • High Resolution Time APl
  • Performance Timeline API
  • Navigation Timing API
  • User Timing API
  • Resource Timing API
  • Paint Timing API

10.1 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() 执行时,系统时钟被向后或向前调整了(如切换到复令时),则捕获的时间戳不会考虑这种情况,因此时间差中会包含这些调整。

为此,必须使用不同的计时 API 来精确且准确地度量时间的流逝。Higb 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.43500000052154console.log(absoluteTimestamp); // 1561926208892.4001

注意:通过使用 performance.now()测量Ll缓存与主内存的延迟差,幽灵漏洞(Spectre)可以执行缓存推断攻击。为弥补这个安全漏洞,所有的主流浏览器有的选择降低performance.now()的精度,有的选择在时间戳里混入一些随机性。WebKit博客上有一篇相关主题的不错的文章“What Spectre and MeltdownMean For WebKit”,作者是Filip Pizlo


10.2 Performance Timeline API

PerformanceTimeline API 使用-套用于度量客户端延迟的工具扩展了 Performance 接口。性能度量将会采用计算结束与开始时间差的形式。这些开始和结束时间会被记录为 DOMHighResTimeStamp 值,而封装这个时间戳的对象是 PerformanceEntry 的实例。

浏览器会自动记录各种 PerformanceEntry 对象,而使用 performance.mark() 也可以记录自定义的 PerformanceEntry 对象。在一个执行上下文中被记录的所有性能条目可以通过 performance.getEntries() 获取:

console.log(performance.getEntries());
// [PerformanceNavigationTiming, PerformanceResourceTiming, ... ]

这个返回的集合代表浏览器的性能时间线(performance timeline )。每个 PerformanceEntry 对象都有 name 、entryType 、startTimeduration 属性:

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

上面每个类都会增加大量属性,用于描述与相应条目有关的元数据。每个实例的 nameentryType 属性会因为各自的类不同而不同。

10.2.1 User Timing API

User Timing API 用于记录和分析自定义性能条目。如前所述,记录自定义性能条目要使用 performance.mark() 方法:

performance.mark(" foo");

console.log(performance.getEntriesByType("mark")[0]);
// PerformanceMark {
// name: "foo",
// entryType: "mark",
// startTime: 269.8800000362098,
// duration: 0
// }

在计算开始前和结束后各创建一个自定义性能条目可以计算时间差。最新的标记(mark)会被推到 getEntriesByType() 返回数组的开始:

performance.mark(" foo");
for (leti = 0; i < 1e6; ++i) {}
performance.mark("bar");

const [endMark, startMark] = performance.getEntriesByType("mark");
console.log(startMark.startTime - endMark.startTime); // 1.3299999991431832

除了自定义性能条目,还可以生成 PerformanceMeasure (性能度量)条目,对应由名字作为标识的两个标记之间的持续时间。 PerformanceMeasure 的实例由 performance.measure() 方法生成

performance.mark("foo");
for (leti = 0; i < 1e6; ++i) {}
performance.mark("bar");
performance.measure("baz", "foo", "bar");
const [differenceMark] = performance.getEntriesByType("measure");
console.log(differenceMark);
// PerformanceMeasure {
// name: "foo",
// entryType: "mark",
// startTime: 269.8800000362098,
// duration: 0
// }

10.2.2 Navigation Timing API

Navigation Timing API 提供了高精度时间戳,用于度量当前页面加载速度。浏览器会在导航事件发生时自动记录 PerformanceNavigationTiming 条目。这个对象会捕获大量时间狱,用于描述页面是何时以及如何加载的。

下面的例子计算了 loadEventstartloadEventEnd 时间戳之间的差:

const [performanceNavigationTimingEntry] = performance.getEntriesByType("navigation");
console.log(performanceNavigationTimingEntry);

// PerformanceNavigationTiming = {
//   name: "https://foo.com",
//   entryType: "navigation",
//   startTime: 0,
//   duration: 851.8999999910593,
//   initiatorType: "navigation",
//   deliveryType: "",
//   nextHopProtocol: "http/1.1",
//   renderBlockingStatus: "non-blocking",
//   workerStart: 0,
//   redirectStart: 0,
//   redirectEnd: 0,
//   fetchStart: 1.699999988079071,
//   domainLookupStart: 1.699999988079071,
//   domainLookupEnd: 1.699999988079071,
//   connectStart: 1.699999988079071,
//   secureConnectionStart: 1.699999988079071,
//   connectEnd: 1.699999988079071,
//   requestStart: 8.599999994039536,
//   responseStart: 50,
//   firstInterimResponseStart: 0,
//   responseEnd: 567.0999999940395,
//   transferSize: 115167,
//   encodedBodySize: 114867,
//   decodedBodySize: 649506,
//   responseStatus: 200,
//   serverTiming: [],
//   unloadEventStart: 0,
//   unloadEventEnd: 0,
//   domInteractive: 655.8999999910593,
//   domContentLoadedEventStart: 657.5,
//   domContentLoadedEventEnd: 659.8999999910593,
//   domComplete: 851.6999999880791,
//   loadEventStart: 851.6999999880791,
//   loadEventEnd: 851.8999999910593,
//   type: "navigate",
//   redirectCount: 0,
//   activationStart: 0,
//   criticalCHRestart: 0
// };

console.log(performanceNavigationTimingEntry.loadEventEnd - performanceNavigationTimingEntry.loadEventStart); // 0.805000017862767

10.2.3 Resource Timing API

Resource Timing API 提供了高精度时间戳,用于度量当前页面加载时请求资源的速度。浏览器会在加载资源时自动记录 PerformanceResourceTiming 。这个对象会捕获大量时间戳,用于描述资源加载的速度。

下面的例子计算了加载一个特定资源所花的时间:

const performanceResourceTimingEntry = performance.getEntriesByType("resource")[0];
console.log(performanceResourceTimingEntry);

// PerformanceResourceTiming = {
//   name: "https://www.baidu.com/img/bd_logo1.png",
//   entryType: "resource",
//   startTime: 246.29999999701977,
//   duration: 1.7999999970197678,
//   initiatorType: "img",
//   deliveryType: "cache",
//   nextHopProtocol: "http/1.1",
//   renderBlockingStatus: "non-blocking",
//   workerStart: 0,
//   redirectStart: 0,
//   redirectEnd: 0,
//   fetchStart: 246.29999999701977,
//   domainLookupStart: 246.29999999701977,
//   domainLookupEnd: 246.29999999701977,
//   connectStart: 246.29999999701977,
//   secureConnectionStart: 246.29999999701977,
//   connectEnd: 246.29999999701977,
//   requestStart: 247.5,
//   responseStart: 247.69999998807907,
//   firstInterimResponseStart: 247.69999998807907,
//   responseEnd: 248.09999999403954,
//   transferSize: 0,
//   encodedBodySize: 7877,
//   decodedBodySize: 7877,
//   responseStatus: 200,
//   serverTiming: []
// };



11 Web 组件


11.1 HTML 模版


11.2 影子 DOM


11.3 自定义元素



12 Web Crytography API


12.1 生成随机数


12.2 生成 SubtleCrypto 对象

posted @   wanglei1900  阅读(70)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 25岁的心里话
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示