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 长度或不可用)。
对比
postMessage
vs XMLHttpRequest/fetch:postMessage
是上下文间通信(不经过网络),而 fetch/XHR 是网络请求。postMessage
vsMessageChannel
: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)。
对比
File
vsBlob
: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()
。 closed
mode 会影响测试/调试与外部框架交互。- 选择器(如
: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
(自定义内建元素)在部分浏览器不完全支持或行为差异。- 使用
closed
shadowRoot 会影响外部访问与测试。