1. 跨上下文消息(postMessage)
概念
在不同执行上下文(窗口、iframe、Worker、ServiceWorker、MessageChannel 的端口)之间以**结构化克隆(structured clone)或可转移(transferable)**方式安全地传递数据的 API。
原理
- 使用
postMessage(message, targetOrigin[, transfer])发送消息。 - 接收端
message事件包含:event.data(消息体)、event.origin(发送者 origin)、event.source(发送端 window/worker)、event.ports(MessagePort 列表)。 - 支持 Structured Clone(可克隆对象:Object、Array、Date、Map、Set、ArrayBuffer、Blob、File、ImageBitmap 等),不能克隆函数、DOM 节点、某些原生句柄。
- 可传输对象(transferable):例如
ArrayBuffer、MessagePort、ImageBitmap、OffscreenCanvas等,传输后原对象在发送端通常会"neuter"(变成 0 长度或不可用)。
对比
postMessagevs XMLHttpRequest/fetch:postMessage是上下文间通信(不经过网络),而 fetch/XHR 是网络请求。postMessagevsMessageChannel:MessageChannel提供一对端口(port1/port2),可实现双向、独立的消息流;postMessage可携带ports以配合MessageChannel使用。
实践(示例)
父页面向 iframe 发送消息 & 使用 MessageChannel:
xml
<!-- 父页面 -->
<iframe id="f" src="https://example.com/receiver.html"></iframe>
<script>
const iframe = document.getElementById('f');
// 1) 简单 postMessage(确保指定 targetOrigin 提高安全性)
iframe.contentWindow.postMessage({type: 'hello', text: 'hi iframe'}, 'https://example.com');
// 2) 使用 MessageChannel 传递一个专属端口(便于双向流)
const mc = new MessageChannel();
// mc.port1 在父页面使用
mc.port1.onmessage = (e) => { console.log('来自 iframe 的消息(通过 port):', e.data); };
// 把 port2 作为 transferable 发送给 iframe(建立私有双向通道)
iframe.contentWindow.postMessage({type: 'init-port'}, 'https://example.com', [mc.port2]);
</script>
iframe(接收方)示例(receiver.html):
javascript
// iframe 内
window.addEventListener('message', (e) => {
// 验证来源以防 XSS/CSRF
if (e.origin !== 'https://your-parent-origin.example') return;
console.log('收到 data:', e.data, 'ports:', e.ports);
// 如果 parent 发送了端口(MessageChannel.port),使用它
if (e.ports && e.ports[0]) {
const port = e.ports[0];
port.postMessage({ack: true, from: 'iframe'});
port.onmessage = (m) => console.log('port 收到:', m.data);
}
});
Transfer 示例(ArrayBuffer)
javascript
// 发送端
const ab = new ArrayBuffer(8);
const view = new Uint8Array(ab); view.set([1,2,3,4]);
worker.postMessage({type:'buf', buf: ab}, [ab]); // 把 ab 作为 transferable 传走
// 传输后,ab 在发送端通常变成 neutered(长度 0)
拓展
- 可以配合
BroadcastChannel做多窗口广播。 - 与 ServiceWorker/SharedWorker/MessageChannel 结合实现复杂的消息拓扑(pub/sub、RPC)。
潜在问题
- 不验证
origin会导致安全问题(数据泄漏 / 注入)。 - 误用 transferable 会导致发送端数据"消失"。
- 结构化克隆失败的类型(函数、DOM 节点)会报错或忽略。
2. Encoding API(字符串 ↔ 定形数组)
主体:
TextEncoder/TextEncoderStream/TextDecoder/TextDecoderStream
概念
把 JS 字符串(基于 Unicode code points)编码成字节序列 (Uint8Array 等),或者把字节序列解码回字符串。
原理
- 编码(例如 UTF-8)会把 Unicode code point 按编码规则映射为一个或多个字节。编码后的结果是字节序列(
Uint8Array)。它不是"排序",而是对字符的二进制表示。 - 解码则是对字节序列按指定编码(如
utf-8,utf-16le)解析回字符串。 TextEncoderStream/TextDecoderStream提供流式(TransformStream)接口,适合处理网络或大数据流。
对比
TextEncoder.encode()(一次性) vsTextEncoder.encodeInto()(就地写入、避免分配)TextEncoder(批量) vsTextEncoderStream(流式)TextDecoder.decode(buf, {stream:true})(增量解码) vsTextDecoderStream(更直观、可管道)
2.1 文本编码
2.1.1 批量编码(TextEncoder)
TextEncoder.encode(str)返回Uint8Array(UTF-8 编码)。encodeInto(source, destinationUint8Array)尝试把编码写入已分配的目标数组,返回{read, written};避免多次分配。
示例:
javascript
// 批量编码示例
const encoder = new TextEncoder(); // 默认 UTF-8
const bytes = encoder.encode('Hello €'); // -> Uint8Array
console.log(bytes); // [72,101,108,108,111,32,226,130,172]
// 使用 encodeInto 减少分配(适合高频或流式写入)
const target = new Uint8Array(32); // 事先分配好 buffer
const res = encoder.encodeInto('Hello €', target); // { read: x, written: y }
console.log(res, target.subarray(0, res.written)); // 查看写入的部分
(注:encode 总是返回新分配的 Uint8Array,encodeInto 更节省内存)
2.1.2 流编码(TextEncoderStream)
TextEncoderStream返回{ writable, readable }:向writable写字符串,readable输出Uint8Array的流。- 无法直接强制每次输出固定字节数 :流的分片由实现/写入策略决定。若需固定大小,需在写入端分割字符串或在
TransformStream中手动分片。
示例(写字符串到一个可读字节流并读取):
javascript
// TextEncoderStream 示例
const encoderStream = new TextEncoderStream(); // 创建流式编码器
const writer = encoderStream.writable.getWriter(); // 获取写入端(写字符串)
const reader = encoderStream.readable.getReader(); // 获取读取端(读 Uint8Array)
await writer.write('chunk1'); // 写入字符串
await writer.write('chunk2'); // 再写入
writer.close(); // 完成写入,触发 readable 结束
while (true) {
const {value, done} = await reader.read();
if (done) break;
console.log('收到字节块:', value); // value 是 Uint8Array
}
2.2 文本解码
2.2.1 批量解码(TextDecoder)
const decoder = new TextDecoder('utf-8'),decoder.decode(u8)。decoder.decode(u8, { stream: true })可用于增量解码,最后一次调用不需stream:true。
示例:
ini
const decoder = new TextDecoder('utf-8');
const s = decoder.decode(new Uint8Array([0xE2,0x82,0xAC])); // '€'
2.2.2 流解码(TextDecoderStream)
TextDecoderStream把字节ReadableStream转换为字符串ReadableStream,方便与fetch().body配合。- 示例 1:用
Uint16Array.of(102,111,111)(这些值是 UTF-16 code units:102='f',111='o') ------ 用utf-16le解码。
javascript
// 示例:用 Uint16Array 解码(utf-16 little endian)
const u16 = Uint16Array.of(102, 111, 111); // [102, 111, 111] => 'f','o','o'(UTF-16 code units)
const u8 = new Uint8Array(u16.buffer); // 视内存为字节流 (little-endian)
const readable = new ReadableStream({
start(controller) {
controller.enqueue(u8); // 推入字节
controller.close();
}
});
const decoded = await readable
.pipeThrough(new TextDecoderStream('utf-16le')) // 指定 utf-16le
.getReader()
.read();
console.log(decoded.value); // "foo"
- 示例 2:
fetch动态拉取并流式解码(UTF-8):
javascript
// fetch + TextDecoderStream 示例
async function fetchAndDecode(url) {
const res = await fetch(url); // 请求
// res.body 是 ReadableStream<Uint8Array>
const textStream = res.body.pipeThrough(new TextDecoderStream('utf-8'));
const reader = textStream.getReader();
let all = '';
while (true) {
const {value, done} = await reader.read();
if (done) break;
all += value; // value 是字符串片段
}
return all; // 整个响应解码为字符串
}
拓展
TextEncoderStream/TextDecoderStream在处理大文件、实时数据(WebSocket 内容、fetch 流)时非常有用。- 如果需要更细的分片控制,可插入自定义
TransformStream做分片或缓冲管理。
潜在问题
- 编码/解码时选择错误的编码(如用 utf-8 解 utf-16 数据)会导致乱码。
encodeInto如果目标空间不足,会只写部分内容;需检查written并循环写入剩余。- 浏览器兼容性(老浏览器可能不支持
*Stream类型),需降级或 polyfill。
3. File API 与 Blob API(由来与基本用法)
概念
Blob:表示不可变的类文件对象(原始数据的二进制块),可从字符串、ArrayBuffer、TypedArray、Blob 片段构造,用于上传、建立对象 URL、切片等。File:继承自Blob,表示来自用户计算机的文件,包含元数据(name,lastModified)。
这套 API 最初为浏览器端文件上传、处理本地文件提供能力(不用提交到服务器也能读取/预览)。
原理
File就是带有文件名和最后修改时间的Blob。两者提供.size,.type和读取方法(arrayBuffer(),text(),stream())。File最常见来源:<input type="file">、Drag & Drop、File System Access API(更现代的浏览器 API)。
对比
FilevsBlob:File有name和lastModified;Blob更轻量。- 旧的读取方式(
FileReader) vs 新的 Promise 风格(blob.text(),blob.arrayBuffer())。推荐使用新的 Promise API(更清晰、支持 async/await)。
3.1 File 类型(属性与方法)
常见属性:
file.name:文件名(string)。file.size:字节数(number)。file.type:MIME 类型(string)。file.lastModified:最后修改时间戳(ms, number)。(历史上有lastModifiedDate,已不推荐)
方法(继承自 Blob):file.slice(start, end, contentType):返回片段 Blob。file.arrayBuffer()、file.text()、file.stream():现代读取方式。
使用场景:上传、预览图片、读取日志文件内容、将本地文件转为 ArrayBuffer 处理等。最新获取本地文件方式还包括 File System Access API (如 showOpenFilePicker()),但需 HTTPS 与权限。
示例(读取 <input>):
typescript
<input id="f" type="file" />
<script>
const ip = document.getElementById('f');
ip.addEventListener('change', () => {
const file = ip.files[0]; // File 对象
console.log(file.name, file.size, file.type, file.lastModified);
// 现代读取
file.text().then(text => console.log(text));
});
</script>
3.2 FileReader 类型(事件式 API)
方法:
readAsText(file, encoding?):读成字符串(可指定编码)。readAsDataURL(file):读成 base64 data URL(用于直接img.src显示)。readAsArrayBuffer(file):读成ArrayBuffer(二进制处理)。readAsBinaryString(file):已被弃用/不建议使用。
事件/回调:
onprogress、onload(读取成功)、onerror、onabort。abort():取消正在进行的读操作。
示例(展示多种方法):
ini
<input id="file" type="file" />
<script>
const fI = document.getElementById('file');
fI.addEventListener('change', () => {
const file = fI.files[0];
const fr = new FileReader();
fr.onprogress = (e) => console.log('已读字节:', e.loaded, '/', e.total);
fr.onload = () => console.log('读取完成, 结果:', fr.result);
fr.onerror = (err) => console.error('读取出错', err);
fr.onabort = () => console.log('读取被中止');
// 作为文本读取
fr.readAsText(file, 'utf-8');
// 若想读取为 ArrayBuffer 可用 readAsArrayBuffer(file)
// 若想作为 data URL(图片预览)用 readAsDataURL(file)
});
</script>
3.3 FileReaderSync(只能在 Worker 中)
- 同步版本,只能在 Worker 的全局作用域(不是主线程)中使用。
示例(worker.js):
ini
// worker.js
self.onmessage = (e) => {
const file = e.data.file; // 从主线程传来的 File(可能作为 transferable)
const frs = new FileReaderSync(); // 仅 Worker 可用
const text = frs.readAsText(file); // 同步返回字符串
postMessage({ text });
};
3.4 Blob 与部分读取(slice)
blob.slice(start, end, contentType)返回新Blob(子片),便于分片上传。new Blob([part1, part2], {type:'...'})创建 Blob。- Blob 的便利方法:
blob.text()、blob.arrayBuffer()、blob.stream()。
示例(Blob):
ini
const b = new Blob(['Hello',' world'], {type: 'text/plain'});
const part = b.slice(0,5, 'text/plain'); // 'Hello'
const text = await part.text(); // 'Hello'
3.5 对象 URL 与 Blob(URL.createObjectURL)
const url = URL.createObjectURL(blob):创建一个本地的 blob: URL,可赋值给<img>、<a>等。- 使用完后应调用
URL.revokeObjectURL(url)释放资源(避免内存泄漏)。
示例(图片预览并释放):
ini
const img = document.createElement('img');
const blob = new Blob([/* image bytes */], { type: 'image/png' });
const url = URL.createObjectURL(blob);
img.src = url;
img.onload = () => {
URL.revokeObjectURL(url); // 加载完成后释放
};
document.body.appendChild(img);
3.6 读取拖动文件(Drag & Drop)
drop事件中的e.dataTransfer.files提供FileList。- 常见场景:网页支持拖入文件后预览或上传。
示例(拖放文件并读取文件名):
xml
<div id="dropZone">拖放文件到这里</div>
<script>
const dz = document.getElementById('dropZone');
dz.addEventListener('dragover', (e) => { e.preventDefault(); /* 表示接受 drop */ });
dz.addEventListener('drop', async (e) => {
e.preventDefault();
const files = e.dataTransfer.files;
for (const f of files) {
console.log('文件名:', f.name, '大小:', f.size);
// 例如读取文本
const text = await f.text();
console.log(text.slice(0, 200)); // 预览前 200 字符
}
});
</script>
潜在问题(File / Blob)
- 大文件时直接
.text()可能消耗大量内存,推荐流式处理(stream())。 URL.createObjectURL若忘记 revoke 会占内存。- File System Access API 浏览器实现差异与权限限制。
4. 媒体元素(<video> 与 <audio>)
概念
浏览器原生播放音视频的 HTML 元素与 API,支持控制、事件、媒体流等。
原理
浏览器加载媒体资源(通过 <source>、src 或 Media Streams),解码器判断能否播放,通过媒体元素 API 暴露播放控制、元数据、事件与缓冲信息。
对比
<video>支持视频画面,而<audio>仅音频(无画面)。- 原生控件 vs 自定义控件:原生控件简单、兼容好;自定义控件灵活可交互,可与 Accessibility 注意点结合。
4.1 常用属性(示例)
src、currentTime、duration、paused、playbackRate、volume、muted、controls、buffered、readyState、seeking等。
4.2 常用事件
play、pause、ended、timeupdate、loadedmetadata、canplay、canplaythrough、error、seeking、seeked等。
4.3 自定义媒体播放(示例)
一个最小自定义控制示例(按钮 + 进度):
ini
<video id="v" width="480" src="video.mp4"></video>
<button id="play">播放/暂停</button>
<input id="seek" type="range" min="0" max="100" value="0" />
<script>
const v = document.getElementById('v');
const playBtn = document.getElementById('play');
const seek = document.getElementById('seek');
playBtn.addEventListener('click', async () => {
if (v.paused) {
await v.play(); // 返回 Promise,可能因浏览器策略被拒绝
playBtn.textContent = '暂停';
} else {
v.pause();
playBtn.textContent = '播放';
}
});
v.addEventListener('loadedmetadata', () => {
seek.max = Math.floor(v.duration);
});
v.addEventListener('timeupdate', () => {
seek.value = Math.floor(v.currentTime);
});
seek.addEventListener('input', () => {
v.currentTime = Number(seek.value);
});
</script>
(注:play() 返回 Promise,若浏览器阻止自动播放,会 reject)
4.4 检测编解码器(canPlayType)
示例:
javascript
const v = document.createElement('video');
console.log(v.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"')); // 'probably' | 'maybe' | ''
4.5 音频格式
常见支持格式(因浏览器而异):
audio/mpeg(.mp3)audio/wav(.wav)audio/ogg(.ogg,Vorbis)audio/webm(WebM/Opus)
潜在问题
- 编解码器支持受浏览器/平台限制(Safari 对某些格式支持差异)。
- 自动播放策略(需要用户交互或静音)。
- 大文件或流式播放需处理缓冲、带宽变化。
5. 原生拖放(Drag & Drop)
概念
HTML5 提供拖放事件与 DataTransfer 对象用于在页面内部或跨页面拖放数据/文件。
原理
拖动元素时触发 dragstart,目标元素触发 dragenter/dragover,放置时触发 drop。浏览器通过 dataTransfer 在拖放源与目标之间传递数据/文件。
5.1 拖放事件
事件:dragstart、drag、dragend、dragenter、dragover、dragleave、drop。
关键点:在 dragover 中需要 event.preventDefault() 来表示目标接受 drop(否则 drop 不会触发)。
示例(基本拖放):
xml
<div id="drag" draggable="true">拖我</div>
<div id="target">放到这里</div>
<script>
const d = document.getElementById('drag');
const t = document.getElementById('target');
d.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('text/plain', '这是拖动的数据'); // 设置可取回的数据
e.dataTransfer.effectAllowed = 'copyMove'; // 允许的操作
});
t.addEventListener('dragover', (e) => { e.preventDefault(); // 允许 drop
e.dataTransfer.dropEffect = 'copy'; // 鼠标指针效果
});
t.addEventListener('drop', (e) => {
e.preventDefault();
const txt = e.dataTransfer.getData('text/plain'); // 读取数据
console.log('drop got:', txt);
});
</script>
5.2 自定义放置位置(视觉反馈)
通过在 dragenter/dragleave 中添加/移除 CSS 类来突出可放置区域,并在 dragover 中设置 dropEffect 让用户知道会发生什么(copy/move/link)。
5.3 dataTransfer 对象(属性与方法)
- 属性:
dataTransfer.types(可获取数据类型)、dataTransfer.files(FileList)、dataTransfer.items(DataTransferItemList)、dropEffect、effectAllowed。 - 方法:
setData(format, data),getData(format),clearData([format]),setDragImage(img, x, y)(自定义拖拽时的影像)。
示例(自定义拖影):
ini
e.dataTransfer.setDragImage(myCanvasOrImgElement, 10, 10);
注意:
DataTransferItemList.add()在某些浏览器有实现差异(要注意兼容性)。
5.4 dropEffect 与 effectAllowed
effectAllowed(在拖动源设置):'none'|'copy'|'copyLink'|'copyMove'|'link'|'linkMove'|'move'|'all'|'uninitialized'。dropEffect(在目标设置):'none'|'copy'|'link'|'move'。
目标会用dropEffect表示实际会执行哪种操作(例如按住键修改为 copy 或 move)。
5.5 可拖动能力
- 默认可拖动:链接
<a>、图片<img>、选中文本等;其他元素可通过draggable="true"设置为可拖动。 - 某些元素(form 表单内部)行为特殊,需测试。
5.6 其他成员
setDragImage:自定义拖拽光标/影像。types:列出可获取的数据格式(如["text/plain", "text/html"])。clearData:清除特定格式或全部数据。
潜在问题
- 跨 iframe 或跨域拖放需要谨慎安全策略。
- 浏览器实现差异(尤其是 DataTransferItemList 操作)。
- 移动端对拖放支持有限(常用触摸事件替代)。
6. Notifications API(通知)
概念
在浏览器中展示系统/浏览器层级通知(非内嵌 UI),用于提醒用户事件或新的信息(通常需用户许可)。
6.1 通知权限
Notification.permission:返回'default' | 'granted' | 'denied'。Notification.requestPermission():请求用户授权(通常需用户交互触发,且只能在安全上下文 https 下)。
示例:
ini
if (Notification.permission === 'default') {
const perm = await Notification.requestPermission(); // 返回 'granted' 或 'denied'
console.log('权限:', perm);
}
6.2 显示与隐藏通知
new Notification(title, options)创建并显示通知(如果权限为granted)。notification.close()隐藏通知。
示例:
ini
const n = new Notification('新消息', { body: '你有一条新消息', icon: '/icon.png' });
n.onclick = () => window.focus();
setTimeout(() => n.close(), 5000); // 5 秒后自动关闭
在 Push 场景通常在 Service Worker 中用
self.registration.showNotification(...)。
6.3 通知的生命周期事件
onshow、onclick、onclose、onerror--- 可绑定回调处理交互。
潜在问题
- 只能在 HTTPS 下使用(或 localhost)。
- 用户可能拒绝权限;滥用会导致用户关闭/封禁。
- 一些平台(移动)对通知的样式/行为受限。
7. Page Visibility API(页面可见性)
概念
页面可见性 API 允许检测页面是否为"可见状态(visible)"或"隐藏(hidden)",以便做节流或暂停不必要的任务(如动画、定时轮询)。
原理
document.visibilityState:字符串visible/hidden/prerender(可能有其他状态)。document.hidden:布尔值,true表示页面被隐藏。- 事件
visibilitychange:当可见性改变时触发。
实践(节流示例)
scss
let timerId = null;
function startPolling() {
timerId = setInterval(() => {
console.log('轮询后台数据...');
}, 5000);
}
function stopPolling() {
if (timerId) { clearInterval(timerId); timerId = null; }
}
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
stopPolling(); // 页面不可见时停止轮询,节省资源
} else {
startPolling(); // 恢复轮询
}
});
// 启动初始轮询(若页面可见)
if (!document.hidden) startPolling();
潜在问题
- 做"短任务"的节流没必要每次都停止;需根据任务成本来决定是否停止。
- 移动设备的省电模式和浏览器策略可能进一步限制后台工作。
8. Streams API(可读、可写、转换流)
本节是核心:理解流、控制反压(backpressure)、管道(pipe)等。
8.1 理解流(概念)
- 可读流(ReadableStream) :产生数据(chunks),消费者通过
getReader()或for await获取。 - 可写流(WritableStream) :接收数据,写入端通过
getWriter()写入。 - 转换流(TransformStream) :包含
writable与readable,用于对数据进行中间处理(map/filter/batch)。 - 块(chunks) :流中处理的数据单元(可以是字符串、Uint8Array 等)。
- 内部队列 & 反压 :流有内部队列与
queuingStrategy(highWaterMark),当消费者处理不及时时,反压会让生产者放慢写入速率。反压非常重要,防止内存爆炸。
8.2 可读流(controller & reader)
ReadableStream的start(controller)/pull(controller)/cancel(reason)用来推数据。reader = stream.getReader(),await reader.read()得到{value, done}。
示例(自定义可读流):
javascript
const rs = new ReadableStream({
start(controller) {
controller.enqueue('chunk1'); // 推入第一个片段
controller.enqueue('chunk2'); // 推入第二个片段
controller.close(); // 结束流
}
});
const r = rs.getReader();
(async () => {
while (true) {
const {value, done} = await r.read();
if (done) break;
console.log('读到:', value);
}
})();
8.3 可写流(WritableStream 与 writer)
const writer = writable.getWriter();然后await writer.ready(当内部队列空时 resolve),调用writer.write(chunk),最后writer.close()。
示例:
javascript
const ws = new WritableStream({
write(chunk) { console.log('写入:', chunk); },
close() { console.log('写入流已关闭'); }
});
const writer = ws.getWriter();
await writer.ready; // 等待写入器准备好(注意:ready 可以反映内部队列状态)
await writer.write('hello'); // 写入
await writer.write('world');
await writer.close(); // 结束
(注:writer.ready 是一个 Promise,反映何时可以安全写入或内部队列下游已消化)
8.4 转换流(TransformStream)
new TransformStream({ transform(chunk, controller) { ... controller.enqueue(newChunk); } })transform()中可以进行异步处理(可返回 Promise),并enqueue结果。
示例: 读取源(Readable),先把读取全部完成(示范"先读取完再写入"的理念),然后再写入到目标 Writable:
javascript
// 先把可读流全部读完(聚合),然后写入到可写流(示例用于理解)
async function readAllThenWrite(readable, writable) {
const reader = readable.getReader();
const chunks = [];
while (true) {
const {value, done} = await reader.read();
if (done) break;
chunks.push(value); // 收集所有块
}
// 等待读取逻辑完成(现在我们有全部 chunks)
const writer = writable.getWriter();
await writer.ready;
for (const c of chunks) {
await writer.write(c); // 顺序写入
}
await writer.close();
}
8.5 通过管道连接流(pipeThrough / pipeTo)
readable.pipeThrough(transformStream):产生新的可读流(把 transform 的 readable 作为输出),常用于串联处理。readable.pipeTo(writable):把readable直接写到writable,返回 Promise 在管道完成时 resolve。
示例(文本流转大写):
javascript
// 源:ReadableStream 输出字符串
const source = new ReadableStream({
start(controller) {
controller.enqueue('hello ');
controller.enqueue('world');
controller.close();
}
});
// Transform:把字符串转成大写
const t = new TransformStream({
transform(chunk, controller) {
controller.enqueue(chunk.toUpperCase());
}
});
// 目标:输出到控制台(Writable)
const target = new WritableStream({
write(chunk) { console.log('写到目标:', chunk); }
});
// 管道
await source.pipeThrough(t).pipeTo(target); // 连接并等待完成
拓展
fetch()的Response.body是ReadableStream,可以直接与TextDecoderStream或TransformStream链接做实时处理。- 剖析反压参数:
queuingStrategy的highWaterMark与size回调决定何时触发ready/ backpressure。
潜在问题
- 不正确处理 backpressure 可能导致内存飙升。
- 需要谨慎处理错误(
pipeTo默认在错误时会关闭两侧流,可用{ preventClose, preventAbort, preventCancel }选项调整)。 - 浏览器间行为细节(ready Promise、策略)需测试。
9. 计时 API(Performance)
概念
高精度时间测量 API 用于性能度量:performance.now()、performance.timeOrigin,以及 Timeline API(performance.mark、performance.measure、getEntriesByType 等)。
9.1 High Resolution Time API
performance.now():返回以毫秒为单位的高精度时间(相对于timeOrigin)。performance.timeOrigin:表示纪元(通常是Date.now()的基准)用于把now()转为 UNIX 时间戳。
示例:
ini
const t0 = performance.now();
// ... 执行耗时操作
const t1 = performance.now();
console.log('耗时 ms:', t1 - t0);
console.log('time origin(UNIX ms):', performance.timeOrigin);
9.2 Performance Timeline API
performance.mark('name'):打点。performance.measure('mName', 'startMark', 'endMark'):测量两点时间差。performance.getEntriesByType('measure')/getEntriesByName(...)获取测量记录。- 还有
PerformanceResourceTiming(资源加载)、PerformanceNavigationTiming(导航),PerformancePaintTiming(首次绘制相关)等。
示例(User Timing):
arduino
performance.mark('start');
// 做事
performance.mark('end');
performance.measure('my-op', 'start', 'end');
console.log(performance.getEntriesByType('measure'));
潜在问题
- 记得在适当时机清理 mark/measure(
performance.clearMarks()/clearMeasures())以免占用内存。 - 不同浏览器可能在性能条目细节上略有差异。
10. Web 组件(增加 DOM 的工作 & Shadow DOM)
概念
Web Components 是一组技术:Custom Elements(自定义元素)、Shadow DOM(封装样式和 DOM)、HTML Templates(模板)等,用于构建可复用、封装的组件。
原理
- Shadow DOM 提供 DOM/样式封装,避免样式冲突。
- Custom Elements 提供组件生命周期和注册机制。
- Template 提供可克隆内容。
实践与拓展将在下面 Shadow DOM 与 Custom Elements 章节详细展开(见 12 / 13)。
11. HTML 模板(<template> 与 DocumentFragment)
概念
<template> 标签在 DOM 中包含不可渲染的 HTML 片段,可作为复用模板克隆并插入文档。DocumentFragment 是轻量的"文档片段",适合批量 DOM 更新,避免多次回流。
原理
template.content返回DocumentFragment,其中的<script>不会在克隆时自动执行(因此可用于存放模板脚本或需要动态执行的脚本)。document.createDocumentFragment()用作临时容器,插入一次到 DOM 时只触发一次渲染,性能更好。
示例(DocumentFragment + template)
ini
<template id="tpl">
<div class="card">
<h3 class="title"></h3>
<p class="body"></p>
</div>
</template>
<script>
const tpl = document.getElementById('tpl');
const frag = document.createDocumentFragment();
for (let i=0;i<3;i++) {
// 克隆模板内容(深拷贝)
const clone = tpl.content.cloneNode(true);
clone.querySelector('.title').textContent = '标题 #' + (i+1);
clone.querySelector('.body').textContent = '内容示例';
frag.appendChild(clone); // 先放到片段中
}
document.body.appendChild(frag); // 一次性插入(只触发一次 DOM 更新)
</script>
模板脚本
模板内的 <script> 标签不会在克隆时执行;若需要执行脚本,可在插入后动态创建脚本节点并插入(或使用 type="application/javascript" 并手动 eval/insert)。
潜在问题
- 小组件大量使用模板与克隆可带来复杂性(需合理管理事件绑定、内存与清理)。
12. 影子 DOM(Shadow DOM)
12.1 理解影子 DOM(概念)
Shadow DOM 将组件的 DOM 与样式封装在宿主元素内部,外部样式不会穿透进来,内部样式(除 ::slotted)也不会泄露到外界。事件会"重定向(retargeting)",即事件目标会在阴影边界上被重新表示,以保护封装。
12.2 创建影子 DOM(attachShadow)
el.attachShadow({ mode: 'open' }):返回shadowRoot,外部可通过el.shadowRoot访问。mode: 'closed':外部el.shadowRoot返回null(无法访问 shadow root)。
示例:
scala
class MyBox extends HTMLElement {
constructor() {
super();
// open 模式:外部可以通过 el.shadowRoot 访问
const sr = this.attachShadow({mode: 'open'});
sr.innerHTML = `<style>p{color: blue}</style><p>我是影子DOM</p>`;
}
}
customElements.define('my-box', MyBox);
// 使用示例
document.body.appendChild(document.createElement('my-box'));
12.3 使用影子 DOM(多个示例组件)
一个演示多个具有不同 shadow DOM 风格的组件(注意:每个元素只能有一个 shadowRoot,所以使用三个不同元素展示三个不同颜色):
scala
<my-box-red></my-box-red>
<my-box-green></my-box-green>
<my-box-blue></my-box-blue>
<script>
class BaseBox extends HTMLElement {
constructor(color) {
super();
const sr = this.attachShadow({mode: 'open'});
sr.innerHTML = `<style>p{color:${color};}</style><p>颜色:${color}</p>`;
}
}
customElements.define('my-box-red', class extends BaseBox { constructor(){ super('red'); }});
customElements.define('my-box-green', class extends BaseBox { constructor(){ super('green'); }});
customElements.define('my-box-blue', class extends BaseBox { constructor(){ super('blue'); }});
</script>
12.4 合成与影子 DOM 粒位(composed / retargeting)
- 当事件穿过 shadow boundary 时,
event.target在外部会显示为宿主元素(retargeting),但event.composedPath()可以查看完整路径。 composed: true的自定义事件可穿过 shadow boundary 到达外部(new CustomEvent('x',{composed:true}))。
示例(事件重定向):
javascript
const host = document.querySelector('my-box-red');
host.shadowRoot.querySelector('p').addEventListener('click', (e) => {
console.log('shadow 内 target:', e.target);
const ev = new CustomEvent('inner-click', { bubbles: true, composed: true });
host.dispatchEvent(ev);
});
host.addEventListener('inner-click', () => console.log('外部捕获到 inner-click'));
12.5 事件重定向示例
上面已示范:composed: true 使自定义事件穿越影子边界;普通 DOM 事件如 click 默认也能冒泡出 shadow,但 event.target 在宿主外部会被 retarget。
潜在问题
- Shadow DOM 会改变事件的
target表现,调试时需使用event.composedPath()。 closedmode 会影响测试/调试与外部框架交互。- 选择器(如
:host,::slotted)需熟悉以正确做样式封装。
13. 自定义元素(Custom Elements)
13.1 创建自定义元素
- 通过
class MyEl extends HTMLElement { ... }定义类,再用customElements.define('my-el', MyEl)注册。 - 也可以创建"自定义内建元素"
extends HTMLButtonElement等,但需is="..."用法并且兼容性有限。
示例(基本):
scala
class HelloEl extends HTMLElement {
constructor() {
super();
// 通常只在 constructor 中做最少 DOM 初始化(不访问属性或子节点)
this.attachShadow({mode:'open'});
this.shadowRoot.innerHTML = `<p>Hello</p>`;
}
}
customElements.define('hello-el', HelloEl);
13.2 添加 Web 组件内容(在 constructor 中 setup)
在 constructor 中创建 shadow DOM 和初始 DOM。复杂 DOM 与事件绑定可以在 connectedCallback 中进行(保证已插入文档)。
13.3 生命周期钩子
constructor():元素构造(不保证已连接文档)。connectedCallback():元素被插入到 DOM。disconnectedCallback():被移除时调用。attributeChangedCallback(name, old, new):属性变化时调用(需要在static get observedAttributes()中声明要观察的属性)。adoptedCallback():元素被移动到新的文档(如 iframe)。
示例(属性观察):
javascript
class MyEl extends HTMLElement {
static get observedAttributes() { return ['title']; }
constructor() {
super();
this.attachShadow({mode:'open'});
}
connectedCallback() {
this._render();
}
attributeChangedCallback(name, oldV, newV) {
if (name === 'title') this._render();
}
_render() {
this.shadowRoot.innerHTML = `<h3>${this.getAttribute('title') || '默认'}</h3>`;
}
}
customElements.define('my-el', MyEl);
13.4 反向自定义属性(通过 getter/setter 代理)
- 可以在类中定义
get/set以反映属性到属性存储/属性到 DOM attribute 的同步。
示例(属性反射):
javascript
class ReflectEl extends HTMLElement {
get count() { return Number(this.getAttribute('count') || 0); }
set count(v) { this.setAttribute('count', String(v)); }
attributeChangedCallback(name) {
if (name === 'count') this.render();
}
static get observedAttributes() { return ['count']; }
render(){ this.textContent = `count=${this.count}`; }
}
customElements.define('reflect-el', ReflectEl);
13.5 升级自定义元素(customElementRegistry)
customElements.whenDefined(name)返回一个 Promise,在定义完成时 resolve。customElements.get(name)返回已注册的构造函数(或 undefined)。customElements.upgrade(rootOrElement)可以把已存在的未升级元素升级为自定义元素(当您在文档中先插入了标签,后定义时有用)。
示例(whenDefined & upgrade):
scala
// 假设 DOM 中有 <delayed-el></delayed-el>,但还未定义
customElements.whenDefined('delayed-el').then(() => {
console.log('delayed-el 已定义');
});
// 手动定义并 upgrade
class DelayedEl extends HTMLElement { connectedCallback(){ this.textContent='ok'; } }
customElements.define('delayed-el', DelayedEl);
customElements.upgrade(document); // 把 document 下的未升级元素升级(谨慎使用)
潜在问题
- 自定义元素一旦定义不可重复定义同名(
customElements.define会报错)。 extends(自定义内建元素)在部分浏览器不完全支持或行为差异。- 使用
closedshadowRoot 会影响外部访问与测试。