前端怎样用 Web Locks 协调多标签页任务
多个同源标签页同时刷新资料、同步草稿或更新缓存时,真正需要协调的不是“哪个标签页更重要”,而是“同一份共享资源在同一时刻只能由谁处理”。Web Locks API 用一个由业务自己命名的锁,把这类互斥范围交给浏览器管理:拿到锁的异步回调继续工作,其他同名请求排队,回调返回后锁自动释放。
官方资料地址:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Locks_API。下面只讨论锁名、等待和释放,不把 Web Locks 当成服务端分布式锁,也不涉及浏览器截图或真实运行结果。
先把共享资源映射成稳定锁名
锁名是协调协议的核心。假设多个标签页都可能刷新当前用户资料,那么它们应该共同使用 refresh-profile,而不是把标签页编号拼进锁名。前者表达“同一份资料刷新任务互斥”,后者会让每个标签页拿到不同的锁,失去协调作用。
锁的范围还受源约束:同一个源下使用相同名字的请求才会进入同一套锁管理关系。锁名要从资源本身来,而不是从调用者来;如果任务是按项目区分的,可以使用 refresh-project-42 这类稳定命名,但不要把随机数作为互斥键。

request 回调就是临界任务的入口
navigator.locks.request() 接收锁名和回调。回调被调用时,说明当前请求已经获得这个锁;还没有获得锁的同名请求不会同时进入自己的回调。把共享资源的读取、刷新或写入放进回调,才能让互斥边界覆盖真正需要保护的部分。
async function refreshProfileOnce() {
// 锁名描述共享资料,而不是描述当前标签页。
return navigator.locks.request("refresh-profile", async (lock) => {
// lock 可能为空的情况只在特定选项下出现,这里使用默认等待模式。
if (!lock) {
return { kind: "not-acquired" };
}
// 只有持有同名锁的回调进入这段共享任务。
const response = await fetch("/api/profile", { credentials: "include" });
if (!response.ok) {
throw new Error("资料请求失败:HTTP " + response.status);
}
const profile = await response.json();
// 返回值会成为 request 返回 Promise 的结果。
return { kind: "refreshed", profile };
});
}
// 这里只展示调用入口,不自动发起网络请求。
// const result = await refreshProfileOnce();
示例中的 lock 主要用于说明当前回调已经取得锁;业务通常不需要手动保存它,也没有一个配套的 release() 方法。真正要保护的工作应放在回调内部,不能先在回调外完成写入,再把一个空回调交给锁管理器。
同名请求会等待,而不是抢占当前任务
当标签页 A 已经持有 refresh-profile,标签页 B 再用同名请求时,B 会等待。这个等待是浏览器对同名锁请求的排队管理,不等于业务层已经完成了去重:如果 A 的任务本身重复写入,或者 B 等待结束后又读取了过期数据,仍然需要在业务代码里设计结果处理。
如果两个任务保护的是不同资源,就应该使用不同的锁名。例如资料刷新使用 refresh-profile,草稿同步使用 sync-draft。把所有任务都塞进一个全局锁会扩大互斥范围,让互不相关的工作相互等待;把同一资源拆成多个名字又会失去保护。
回调返回就是释放边界
Web Locks 的释放时机跟回调返回值绑定。同步回调返回后立即释放;异步回调返回的 Promise 完成后释放。因此只要把需要保护的异步工作写在 async 回调里,成功和异常都能走到同一个生命周期出口。

async function syncDraft() {
return navigator.locks.request("sync-draft", async () => {
// Promise 未完成前,sync-draft 仍由当前回调持有。
const draft = await readLocalDraft();
await sendDraft(draft);
// 返回后,浏览器才有机会释放这个锁。
return { kind: "synced" };
});
}
async function runWithLock() {
try {
// 这里等待 request 的结果;失败时保留原始异常给上层处理。
return await syncDraft();
} catch (error) {
// 异常不会把锁永久留在当前回调中,但业务仍要记录或展示失败。
console.error("草稿同步失败", error);
throw error;
}
}
// readLocalDraft、sendDraft 由业务提供;本文不在本机执行这些函数。
不要用一个没有等待内部任务的回调包住异步工作,例如先启动 Promise、马上返回“已开始”,再期待锁保护 Promise 的剩余部分。那样回调已结束,锁的持有边界也已经结束。正确做法是直接 await,或返回代表完整任务的 Promise。
不想排队时用 ifAvailable
有些任务只想“能立刻执行就执行”,不愿意等待另一个标签页完成。这时可以传入 ifAvailable: true。拿不到锁时,回调会收到 null,调用方应把它当作一次明确的“本次未执行”,而不是把它当成任务失败或已经执行成功。
async function refreshIfIdle() {
return navigator.locks.request(
"refresh-profile",
{ ifAvailable: true },
async (lock) => {
// 锁正在被其他标签页持有时,直接返回未执行结果。
if (lock === null) {
return { kind: "skipped", reason: "busy" };
}
// 只有立即取得锁才刷新资料。
await fetch("/api/profile", { credentials: "include" });
return { kind: "refreshed" };
},
);
}
// 调用方可根据 kind 决定是否稍后重试;本文不自动调度重试。
ifAvailable 只改变等待策略,不改变锁名,也不替你决定重试频率。若业务必须最终完成,应该在更上层安排可解释的重试或提示;不要在回调里无间隔地递归请求,否则多个标签页可能把一次互斥任务变成不断争抢。
等待时间太长时交给 AbortSignal
默认等待适合短任务,但页面关闭、用户切换工作区或本次操作已经失去意义时,排队请求不应无限等待。可以把 AbortSignal 放进请求选项,让等待中的锁请求在信号取消时结束。取消等待不会证明其他标签页的任务已失败,也不会回滚已经取得锁的任务。
async function waitForProfile(signal) {
// signal 只约束本次等待;它不是锁的拥有者,也不能主动释放别人的锁。
return navigator.locks.request(
"refresh-profile",
{ signal },
async () => {
// 获得锁后仍要检查业务是否愿意接受结果。
signal.throwIfAborted();
const response = await fetch("/api/profile", {
credentials: "include",
signal,
});
if (!response.ok) {
throw new Error("资料请求失败:HTTP " + response.status);
}
return response.json();
},
);
}
// 由调用方决定何时 controller.abort();这里不启动定时器或网络请求。
这里有两个边界:signal 可以让排队请求放弃等待,也可以传给支持取消的网络请求;但它不是一个手动锁句柄。拿到锁后的回调仍应在关键异步边界检查取消状态,并让自己的 Promise 结束,这样释放时机才清楚。
query 适合诊断,不是业务同步信号
navigator.locks.query() 可以得到锁管理器在某一时刻的快照,用来区分哪些锁正在持有、哪些请求正在等待。它适合诊断“为什么这个标签页一直没有进入回调”,但查询完成和业务任务完成不是同一件事,不能把快照当成通知机制。
async function inspectProfileLock() {
// query 返回观察时刻的快照,不会等待 refresh-profile 变为空闲。
const snapshot = await navigator.locks.query();
const held = snapshot.held.filter(
(lock) => lock.name === "refresh-profile",
);
const pending = snapshot.pending.filter(
(lock) => lock.name === "refresh-profile",
);
return { held: held.length, pending: pending.length };
}
// 该函数只用于诊断展示;它不替代 request,也不改变锁状态。
诊断时要注意快照可能在返回后立即过时。若页面需要向用户解释状态,应把“当前看到的等待数量”和“业务最终是否刷新成功”分开表达,避免把一次观察结果写成确定结论。
把锁的边界收敛成三条检查
| 检查点 | 正确做法 | 常见误区 |
|---|---|---|
| 锁名 | 同一共享资源使用稳定、可解释的名字 | 给每个标签页拼不同随机后缀 |
| 等待 | 默认排队,或按场景使用 ifAvailable、AbortSignal | 把未取得锁误报成已完成 |
| 释放 | 让回调返回完整 Promise,异常也交给上层处理 | 启动异步任务后立即返回 |
| 诊断 | 用 query 观察 held 与 pending 的快照 | 把查询快照当成业务事件 |
前端多标签页协调的关键不是手动维护一个全局布尔变量,而是先定义资源,再给资源选择稳定锁名,最后让回调的 Promise 覆盖完整临界任务。这样等待和释放都有明确边界,未等待、可取消和诊断需求也能分别落到对应选项上。
同一个锁名一定会让所有任务串行吗?
默认的 exclusive 模式会让同名请求互斥;只有共享读场景才考虑 shared 模式。不要为了减少等待把所有请求都改成共享模式,先确认任务确实不会互相写冲突。
能不能在回调外手动释放锁?
常规用法不需要,也不能把 Lock 当作带有释放方法的句柄。让回调返回或返回的 Promise 结束,浏览器据此结束持有范围。
取消排队后,已经持锁的标签页会被中断吗?
不会。取消某个等待请求只影响这个请求;已经取得锁的回调是否停止,要由它自己的取消信号和业务逻辑决定。
前端多个标签页怎样同步登录状态变化
- 上一篇
- 前端多个标签页怎样同步登录状态变化
- 下一篇
- 前端怎样观察动态节点并及时断开监听
-
- 文章 · 前端 | 54分钟前 |
- TypeScript catch 变量为 unknown 时怎样安全处理
- 478浏览 收藏
-
- 文章 · 前端 | 56分钟前 | css · CSS 滚动条 Overflow scrollbar-gutter 布局跳动
- CSS scrollbar-gutter 怎样避免滚动条出现时布局横向跳动
- 417浏览 收藏
-
- 文章 · 前端 | 56分钟前 |
- TypeScript satisfies 怎样校验对象又保留字面量类型
- 162浏览 收藏
-
- 文章 · 前端 | 1小时前 | css · CSS 锚点定位 anchor-name position-area position-anchor 浮层定位
- CSS 锚点定位怎样让浮层跟随触发元素
- 262浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- 前端怎样把非关键任务放到空闲时间
- 458浏览 收藏
-
- 文章 · 前端 | 1小时前 | 前端 · 性能监控 · javascript · 浏览器API · 静态资源 前端性能 PerformanceObserver PerformanceResourceTiming 资源等待时间
- 前端怎样统计静态资源等待时间
- 215浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- 前端组件尺寸变化怎样避免循环更新
- 389浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- 前端怎样观察动态节点并及时断开监听
- 479浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- 前端多个标签页怎样同步登录状态变化
- 243浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- 前端 Request 读取一次后怎样再次发送
- 301浏览 收藏
-
- 文章 · 前端 | 1小时前 |
- 前端怎样合并多个取消信号
- 490浏览 收藏
-
- 文章 · 前端 | 18小时前 | 前端 · typescript ·
- TypeScript 自定义类型守卫收窄 API 响应
- 493浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 511次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 571次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 597次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 549次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 365次使用
-
- JavaScript函数定义及示例详解
- 2025-05-11 502浏览
-
- 智能体安全引领产业升级——国内AI安全产品市场深度分析
- 2026-08-21 501浏览
-
- CSS变量简化按钮悬停效果技巧
- 2026-05-31 501浏览
-
- JavaScript符号类型详解与应用
- 2026-05-31 501浏览
-
- HTML剪贴板复制粘贴怎么用
- 2026-05-26 501浏览
