Highlight
Safari 15 在 JavaScript、WebAssembly 和 Web API 三个方向做了大规模更新,包括私有类字段、WeakRef、WebGL2、MediaRecorder 和语音识别 API。
核心内容
Web 开发者在 Safari 上长期面临一个痛点:新特性支持总是慢半拍。WWDC2021 这次一次性补上了大量缺口。
JavaScript 引擎方面,私有类字段终于落地。以前用下划线前缀 _startTime 只是约定,外部代码仍然能访问。现在用 #startTime,语言层面强制执行封装,访问会抛错。
WeakRef 和 FinalizationRegistry 让内存管理更精细。你可以持有对象的弱引用,不阻止垃圾回收;对象被回收时还能收到通知做清理。这对缓存系统、测试框架很有用。
WebAssembly 引擎升级了内存指令、i64/BigInt 互转、引用类型和流式编译。C++/Rust 编译到 Web 的性能又近了一步。
Web API 方面,WebGL2 全平台可用,MediaRecorder 可以录制音视频,Speech Recognition 用 Siri 同款引擎做语音转文字,Web Share 支持分享文件。
详细内容
JavaScript 私有类字段
以前写类,私有成员靠约定:
class StopwatchWithOneButton {
constructor() {
this._startTime = null; // 只是约定,外部仍能访问
}
click() {
if (this._startTime === null) {
this._startTime = Date.now();
} else {
const duration = Date.now() - this._startTime;
this._startTime = null;
return duration;
}
}
}
现在用 # 前缀,语言强制执行私有:
class StopwatchWithOneButton {
#startTime = null; // 真正的私有字段
click() {
if (this.#startTime === null) {
this.#startTime = Date.now();
} else {
const duration = Date.now() - this.#startTime;
this.#startTime = null;
return duration;
}
}
// 私有方法
#start() { this.#startTime = Date.now(); }
#stop() {
const duration = Date.now() - this.#startTime;
this.#startTime = null;
return duration;
}
// 静态私有字段
static #startedCount = 0;
// 公共静态字段
static totalCreated = 0;
}
关键点:
#field是语言级私有,外部访问会抛出 TypeError(01:49)- 私有方法同样用
#前缀,只能在类内部调用(03:15) - 支持静态私有字段
#staticField和公共静态字段staticField(03:39) - 不需要 WeakMap hack 来实现私有属性了
WeakRef 和 FinalizationRegistry
需要跟踪一批对象,但又不想阻止它们被垃圾回收:
const allStopwatches = new Set();
const registry = new FinalizationRegistry((id) => {
// 对象被回收时自动清理
for (const ref of allStopwatches) {
if (ref.id === id) {
allStopwatches.delete(ref);
break;
}
}
});
function addStopwatch(sw) {
const id = generateId();
const ref = new WeakRef(sw);
ref.id = id;
allStopwatches.add(ref);
registry.register(sw, id); // sw 被回收时触发回调
}
function clickAllStopwatches() {
for (const ref of allStopwatches) {
const sw = ref.deref(); // 获取对象,可能返回 undefined
if (sw) sw.click();
}
}
关键点:
WeakRef持有弱引用,不阻止垃圾回收(04:19)deref()获取对象,如果已被回收则返回undefined(05:56)FinalizationRegistry在对象被回收时触发回调做清理(06:21)- 垃圾回收时机不确定,不要依赖它做关键逻辑
Top-level await 和 Module Workers
模块里可以直接用 await:
// stopwatch.mjs
const config = await fetch('/config.json').then(r => r.json());
export class Stopwatch { /* 使用 config */ }
// main.mjs
import { Stopwatch } from './stopwatch.mjs';
// stopwatch.mjs 中的 await 完成后才会执行到这里
const sw = new Stopwatch();
Workers 也支持 ES Modules:
// 创建 module worker
const worker = new Worker('./worker.mjs', { type: 'module' });
// Service Worker
navigator.serviceWorker.register('./sw.mjs', { type: 'module' });
// Audio Worklet
await audioContext.audioWorklet.addModule('./processor.mjs');
关键点:
- top-level await 只在 ES Module 中可用,普通脚本会报语法错误(07:37)
- 被导入的异步模块会阻塞依赖它的模块执行(08:38)
- module workers 支持动态导入、优化加载和依赖管理(09:21)
- web worker、service worker、worklet 都支持 module(09:51)
WebGL2 和 WebM/VP9 支持
WebGL2 新增特性:
- 3D 纹理,支持体积效果如云雾(19:03)
- Sampler 对象,着色器中使用纹理更灵活(19:09)
- Transform feedback,GPU 上实现粒子系统(19:16)
- 后端从 OpenGL 迁移到 Metal,iOS Simulator 也能用 GPU(20:23)
- 可用 Xcode Frame Debugger 分析 WebGL 代码(20:40)
WebM 和 VP9:
- macOS 11.3+ 支持 VP8/VP9 视频 + Vorbis 音频(21:19)
- macOS 12 增加 Opus 音频支持(21:30)
- iPadOS 15 支持 MSE 流式播放 WebM(21:40)
- 可用 MediaCapabilities API 检测具体支持情况(21:47)
MediaRecorder 录制音视频
let mediaRecorder;
let recordedChunks = [];
async function startRecording() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm',
audioBitsPerSecond: 128000
});
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) recordedChunks.push(event.data);
};
mediaRecorder.onstop = () => {
const blob = new Blob(recordedChunks, { type: 'audio/webm' });
const url = URL.createObjectURL(blob);
document.querySelector('audio').src = url;
};
mediaRecorder.start();
}
function stopRecording() {
mediaRecorder.stop();
}
关键点:
MediaRecorder从 media element 或 MediaStream 捕获数据(25:23)- 可指定 MIME type 和比特率(25:39)
ondataavailable收集数据片段,onstop组装成 Blob(26:52)- 支持从麦克风、摄像头或屏幕录制
Audio Worklet 实时音频处理
// distortion-processor.mjs
class DistortionProcessor extends AudioWorkletProcessor {
process(inputs, outputs, parameters) {
const input = inputs[0];
const output = outputs[0];
for (let channel = 0; channel < input.length; channel++) {
const inputChannel = input[channel];
const outputChannel = output[channel];
for (let i = 0; i < inputChannel.length; i++) {
outputChannel[i] = this.distort(inputChannel[i]);
}
}
return true; // 保持活跃
}
distort(x) {
return x > 0 ? Math.min(x * 2, 1) : Math.max(x * 2, -1);
}
}
registerProcessor('distortion', DistortionProcessor);
// main.js
async function startRecordingWithEffect() {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const audioContext = new AudioContext();
await audioContext.audioWorklet.addModule('./distortion-processor.mjs');
const source = audioContext.createMediaStreamSource(stream);
const effectNode = new AudioWorkletNode(audioContext, 'distortion');
const destination = audioContext.createMediaStreamDestination();
source.connect(effectNode).connect(destination);
// 录制处理后的音频
mediaRecorder = new MediaRecorder(destination.stream);
mediaRecorder.start();
}
关键点:
- Audio Worklet 在音频渲染线程运行,减少主线程和渲染线程的切换(27:37)
- 比旧的 ScriptProcessorNode 延迟更低(27:49)
- 处理器必须继承
AudioWorkletProcessor并实现process()方法(29:01) - 用
registerProcessor()注册后才能在主线程通过AudioWorkletNode使用(29:38)
Web Share 分享文件
async function shareAudio(blob, filename) {
const file = new File([blob], filename, { type: 'audio/webm' });
const shareData = {
files: [file],
title: 'Voice Memo',
text: 'Check out my recording'
};
if (navigator.canShare && navigator.canShare(shareData)) {
await navigator.share(shareData);
}
}
关键点:
- Web Share API 今年新增文件分享支持(30:32)
- 可分享图片、视频、音频等文件类型(30:36)
- 先调用
canShare()检测是否支持,再调用navigator.share()(31:37) - 分享界面和系统风格一致,支持 Messages、Mail、AirDrop 等目标(30:17)
Speech Recognition 语音识别
const recognition = new webkitSpeechRecognition();
recognition.continuous = true;
recognition.interimResults = false;
recognition.onresult = (event) => {
let transcript = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
if (event.results[i].isFinal) {
transcript += event.results[i][0].transcript;
}
}
console.log(transcript);
};
recognition.onend = () => {
console.log('Recognition ended');
};
function startRecognition() {
recognition.start();
}
function stopRecognition() {
recognition.stop();
}
关键点:
- 使用 Siri 同款语音识别引擎,支持多语言(32:23)
- 目前仍需
webkit前缀:new webkitSpeechRecognition()(33:25) continuous属性控制是否持续识别直到手动停止(33:36)- 首次使用会弹出隐私授权提示(32:41)
- 结果按概率排序,取第一个就是最佳识别结果(34:02)
核心启发
- 用私有类字段替换 WeakMap 封装。
#field语法比 WeakMap 方案简单直观,且语言层面保证私有性。 - Web Share 文件分享让 Web App 更像原生。几行代码就能调用系统分享面板发送文件,用户体验和原生 App 一致。
- MediaRecorder + Audio Worklet 组合做音频应用。录制、实时处理、分享一条链路都通了,可以在 Web 上实现完整的音频编辑工具。
- Speech Recognition 降低语音交互门槛。用 Siri 引擎做语音转文字,不需要自己训练模型或调用第三方 API。
- Module Workers 把重计算移到后台。配合 ES Module 的依赖管理和动态导入,复杂计算不再阻塞主线程。
关联 Session
- Explore WKWebView additions — WKWebView 最新更新和最佳实践
- Accelerate networking with HTTP/3 and QUIC — Web 网络加速
- Meet Safari Web Extensions — Safari 扩展开发入门
评论
GitHub Issues · utterances