# 两个标签页同时改一份本地缓存，Web Locks 能不能先排队？

**URL:** <https://www.shiqidu.com/t/topic/2001>\
**Category:** 开发调优\
**Tags:** 问与答, HTTP, JavaScript, 前端开发, 浏览器\
**Created:** [2026 年11 月 9 日 11:51 UTC](https://www.shiqidu.com/t/topic/2001 "2026-11-09T11:51:09Z")\
**Posts on this page:** 1\
**Page:** 1

<div class="post-metadata">

作者： ![shiqidu\_editor](https://www.shiqidu.com/user_avatar/www.shiqidu.com/shiqidu_editor/32/528_2.png) [@shiqidu\_editor](https://www.shiqidu.com/u/shiqidu_editor)\
发布日期： [2026 年11 月 9 日 11:51 UTC](https://www.shiqidu.com/t/topic/2001/1 "2026-11-09T11:51:09Z")

</div>

同站点开了好几个标签页，各自往 IndexedDB 或 Cache Storage 里写同一份用户草稿、同步游标、离线队列。单个页里用 `await` 串起来还整齐。跨标签页没有共享的内存锁，后写覆盖先写、游标来回跳，复盘时只看见「偶发脏数据」。浏览器标准里的 [Web Locks API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API) 就是给同源的多标签页 / Worker 协调这类互斥用的，入口是 `navigator.locks.request`。

## 锁名是约定，不是系统资源句柄

MDN 把锁写成一种抽象：名字由应用自己定，表示「这份可能共享的资源」。典型例子是多标签页里只允许一个页去做「网络 ↔ IndexedDB」同步。大家抢同一个名字，例如 `"my_net_db_sync"`，只有拿到的那个继续干（leader election）。锁的作用域是 **源** （origin）：同一站点源上拿到的锁，不影响另一个源上的同名请求。

基本形很短：请求锁 → 在回调里做异步活 → 回调结束（含 async 函数跑完）后自动释放。排队中的同名请求，要等持有者放开才会轮到下一个。

```js
await navigator.locks.request("draft-store", async (lock) => {
  // 此处已拿到锁，同源其他标签页的同名请求会排队
  await readAndWriteIndexedDB();
});

```

`request()` 本身返回的 Promise，会在锁释放、回调结果出来之后才 settle。在 async 函数里 `await` 它，可以把「无锁段 / 持锁段 / 再无锁段」写成线性流程。该 API 只在安全上下文（HTTPS 等）可用，Worker 里对应 `WorkerNavigator.locks`。

## exclusive 和 shared

[`LockManager.request`](https://developer.mozilla.org/en-US/docs/Web/API/LockManager/request) 的 `mode` 默认是 `"exclusive"`：同一名字同时只能有一个持有者，标签页和 Worker 都算在内。读写本地库、改同一游标，用排他模式最直观。

若只是多方只读、偶尔一人写入，可以给读路径要 `"shared"`，写路径仍要 `"exclusive"`。MDN 把它对应到常见的 readers-writer：共享锁之间可以并存，但只要有人持有排他锁，其它同名请求都得等。IndexedDB 自己的 `readonly` / `readwrite` 事务语义与这套相近，但 Web Locks 管的是 **跨执行上下文** 的协调，替代不了事务本身。

```js
await navigator.locks.request(
  "draft-store",
  { mode: "shared" },
  async () => {
    await readSnapshot();
  },
);

```

还有两个常用选项：`ifAvailable: true` 表示拿不到就立刻回调 `null`，不等待。`signal` 可接 `AbortSignal`，给排队加上超时。调试时可用 `navigator.locks.query()` 看当前源上持有中 / 排队中的快照，别把快照当成强一致的实时订阅。

## 和 IndexedDB 事务怎么分工

IndexedDB 事务已经能在 **单个** 标签页里串行化读写。它不管「隔壁标签页是不是也开了同名库」。Web Locks 补的是跨上下文这一层：先抢到应用层锁，再在回调里开事务、读写、提交。顺序别反着来。先开长事务再去 `request` 锁，持锁时间被事务拖长，其它标签页会堵在队列里。

实操上可以把持锁回调写瘦：锁内只做「读必要快照 → 计算 → 写回」，网络请求能挪到锁外就挪出去（或拆成「拉数不用锁、写库再加锁」两段）。队列名建议和资源一一对应，例如草稿库、同步游标、离线出站队列各用各的名字，避免一张大锁串起所有写入。

## 适合干什么，别指望什么

适合：多 Tab 抢同一份本地写入、后台同步选举一个 leader、避免两个页同时清空再重建同一份缓存。不适合：当成跨站互斥（不同源彼此看不见）。也不要指望它帮你「打通」或绕过浏览器的隔离策略。锁本来就按源切开，这是边界，不是缺陷。

复杂一点的死锁（页 A 持锁 1 等锁 2，页 B 相反）只会影响依赖这些锁的逻辑，浏览器其它标签页不受牵连。规避方式按文档：少嵌套请求、固定加锁顺序、或给请求加超时。`steal` 能强行抢走同名锁，MDN 自己标了警告：原先持锁代码仍在跑，容易和后来者打架。日常业务优先用排队和超时，别把抢锁当默认路径。

若你们已经在用 SharedWorker 或服务端会话锁，Web Locks 更像「纯前端、同源、本地资源」这一层补丁，三套机制职责别叠乱。接之前可以先列一张「哪些写入路径必须跨 Tab 互斥」的名单，名单为空就不必为了 API 而 API。

你们现在脏数据是「后打开的标签页覆盖旧稿」，还是「同步任务跑了两份」？两种抢法，锁名和持锁范围会不一样。
