navigator.locks.request不能在顶层await调用,因其必须在活跃文档上下文且由用户交互或生命周期事件触发(如click、fetch回调),否则抛InvalidStateError或静默失败;需确保HTTPS/localhost环境,并包裹于事件处理函数内。

navigator.locks.request 为什么不能直接用 await 写在顶层
因为 navigator.locks.request 必须在「有活跃文档上下文」的环境中调用,且不能脱离用户交互或生命周期事件(如 click、fetch 回调、service worker activate)——直接在模块顶层 await navigator.locks.request(...) 会抛出 InvalidStateError: Lock manager is not available 或静默失败。
常见错误现象:页面加载后立刻执行锁请求,控制台没报错但回调 never 被触发;或者在非 secure context(HTTP 非 localhost)下整个 API 返回 undefined。
- 确保页面运行在 HTTPS 或
localhost(否则navigator.locks为undefined) - 锁请求必须包裹在用户触发的事件处理函数内,比如按钮点击:
button.addEventListener('click', async () => { ... }) - Service Worker 中可用,但需在
activate或fetch等事件中调用,不可在全局作用域执行
如何正确传参:name、mode 和 signal 的取舍
navigator.locks.request() 的签名是 request(name, options?, callback),注意它**不返回 Promise**(尽管 callback 可以是 async 函数),这是最容易混淆的点。现代写法推荐用 callback 形式,避免误以为能 await 它本身。
参数关键细节:
立即学习“前端免费学习笔记(深入)”;
-
name是字符串,用于标识锁名;相同 name 的锁天然互斥,但要注意大小写敏感和空格 ——"upload"和"upload "是两个锁 -
mode默认是"exclusive",可选"shared";只有所有持有者都声明shared,才能并发进入;混用会导致 exclusive 请求被阻塞直到所有 shared 持有者释放 -
signal(AbortSignal)可用来提前取消等待,比如用户点了“取消上传”,调用controller.abort()后 callback 不会执行,也不会占用锁
示例(带超时取消):
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
navigator.locks.request("api-submit", { signal: controller }, async (lock) => {
// 此处才真正拿到锁,可安全执行互斥逻辑
await fetch("/api/submit", { method: "POST" });
});
锁内异步操作完成后必须显式 return,否则锁不释放
navigator.locks.request 的 callback 执行完毕(同步完成 or async 函数 resolve 后)才会自动释放锁。如果 callback 内部抛错、未 return、或漏掉 await,锁可能长期滞留,阻塞后续请求。
典型陷阱:
- 忘记
await异步操作:fetch("/api")写成fetch("/api")(无 await),callback 立即结束,请求还在飞,锁却已释放 → 失去互斥性 - try/catch 吞掉错误但没 re-throw,导致锁释放但业务异常被忽略
- 在 lock callback 中调用另一个
navigator.locks.request(嵌套锁),需确保内层也正确 await,否则外层锁提前释放
安全写法模板:
navigator.locks.request("sync-db", async (lock) => {
try {
const res = await fetch("/api/sync");
const data = await res.json();
localStorage.setItem("lastSync", Date.now().toString());
} catch (err) {
console.error("Sync failed, lock still released", err);
throw err; // 可选:让上层知道失败,但锁必定释放
}
});
浏览器兼容性与降级方案怎么写才不翻车
目前仅 Chromium 114+、Firefox 117+、Edge 114+ 支持 navigator.locks;Safari 完全不支持(截至 Safari 17.5)。不能假设 navigator.locks 存在。
检查和降级的关键点:
- 先判断
if (!navigator.locks),再决定走锁逻辑还是加内存标记(如window.__isSubmitting = true) - 内存标记只能防 UI 层重复点击,无法跨 tab / service worker 互斥,别把它当锁的平替
- 服务端加幂等 key(如 request-id header)仍是必要兜底,客户端锁只是优化体验,不是一致性保障
简单降级示例:
async function withLock(name, fn) {
if (navigator.locks) {
return navigator.locks.request(name, async (lock) => fn());
} else {
// 单页内防重:用 WeakMap 记录 pending promise,避免重复触发
const pending = window.__lockPending || new WeakMap();
const key = Symbol();
pending.set(key, fn());
await pending.get(key);
pending.delete(key);
}
}
锁的语义边界很窄:它只保证同一 origin 下同名锁的 callback 不并行执行。别指望它替代数据库事务或 WebSocket 消息序号。真正需要强一致的地方,server-side 才是唯一可信层。



















