Shared Worker 是目前前端唯一能跨标签页实现中心化锁机制的原生方案,因其单线程串行执行可保障锁的申请、持有与释放原子性;而 BroadcastChannel 仅广播、localStorage 轮询有延迟且无互斥语义,均无法阻塞操作或避免竞态。

Shared Worker 是目前前端唯一能跨标签页建立中心化锁机制的原生方案,它把锁的申请、持有、释放逻辑完全收归一个独立线程管理,避免各页面各自为政导致的竞态与冲突。
为什么必须用 Shared Worker 而不是 BroadcastChannel 或 localStorage?
其他方式本质是“通知+各自执行”,不具备原子性:
- BroadcastChannel 只能广播消息,无法阻塞后续操作——A 页面发完“我要写”,B 页面还没收到就已开始写入
- localStorage 轮询有延迟且无互斥语义,两个页面可能同时读到“未加锁”状态,然后一起写入
- 只有 Shared Worker 能真正实现“请求→排队→授锁→回调”的同步流程,所有锁操作串行化在单线程中执行
锁的核心实现:基于 Map 的资源键 + 等待队列
Worker 内维护两个关键结构:
-
锁状态表:
Map<resourcekey holderport: messageport timestamp: number></resourcekey>,记录当前哪个页面持有了哪把锁 -
等待队列:
Map<resourcekey array port: messageport requestid: string>></resourcekey>,按资源键分组排队,FIFO 保证公平性
当页面发送 { type: "acquire", resourceKey: "user-profile", requestId: "req-8a2f" },Worker 检查该 key 是否已被占用;若空闲则立即 port.postMessage({ type: "granted", requestId });否则推入对应队列,等前一个持有者释放后依次唤醒。
页面端安全集成:自动清理 + 容错降级
页面不能假设锁一定可用,需主动管理生命周期:
- 连接 SharedWorker 时检查
navigator.sharedWorker是否存在,不支持时直接 fallback 到本地独占操作(无锁但功能不中断) - 每次 acquire 都附带唯一
requestId,Worker 回复时原样返回,页面靠它 resolve 对应 Promise,避免响应错乱 - 监听
port.onclose,一旦页面关闭或刷新,Worker 自动从锁表和队列中移除其所有条目,防止僵尸锁长期占用 - 可选添加超时机制:Worker 对每个等待请求记时间戳,超过 5 秒未获锁则主动回复
{ type: "timeout", requestId }
典型写冲突场景下的行为保障
例如多个标签页同时尝试更新用户头像:
- 页面 A 发起
acquire("avatar")→ Worker 授予锁,A 开始上传 - 页面 B、C 几乎同时发起相同请求 → 全部进入等待队列
- A 上传完成并发送
{ type: "release", resourceKey: "avatar" }→ Worker 唤醒队列首项(B),B 获锁开始上传 - C 继续等待,直到 B 显式释放或异常断连被 Worker 清理
整个过程对业务层透明,开发者只需调用 acquireDownload("avatar.jpg") 和 release("avatar"),无需关心谁先谁后。

















