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

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

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

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

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

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

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

exclusive 和 shared

LockManager.request 的 mode 默认是 "exclusive":同一名字同时只能有一个持有者,标签页和 Worker 都算在内。读写本地库、改同一游标,用排他模式最直观。

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

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。

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