前端里的资源竞争通常不像服务端那样被频繁讨论,但多标签页环境下的冲突却实实在在地存在。例如两个标签页同时写localStorage的某个键,最后写入的页面会覆盖前一个页面的结果,用户可能看到不一致的状态。Web Locks API 提供了一套基于Promise的全局锁机制,让同一源下的不同执行上下文可以协调对共享资源的访问。它的核心方法挂在navigator.locks上,用法远比想象中简单,但锁模式的选择会直接影响并发行为。

独占锁:保护写入临界区
Web Locks API 最基础的调用方式是使用navigator.locks.request()。第一个参数是锁名字符串,第二个参数是一个回调函数,回调内可以执行异步任务。当请求一个独占锁时,只要没有其他任务持有同名锁,回调就会立即执行;如果有,则排队等待。锁会在回调返回的Promise resolve后自动释放,不需要手动调用类似release()的方法。
独占锁可以理解为互斥锁,它是默认模式。下面这段代码演示了两个写操作如何通过同一个锁名串行执行,避免同时修改本地存储导致的数据覆盖。
async function saveUserConfig(config) {
await navigator.locks.request('user-config-write', async () => {
const current = JSON.parse(localStorage.getItem('userConfig') || '{}');
const merged = { ...current, ...config };
localStorage.setItem('userConfig', JSON.stringify(merged));
// 模拟耗时操作
await new Promise(resolve => setTimeout(resolve, 200));
});
}
// 两个标签页同时调用时,会依次执行
saveUserConfig({ theme: 'dark' });
saveUserConfig({ fontSize: 16 });
这个例子里,两个调用即使来自不同标签页,也会因为锁名同为user-config-write而排队。第二个调用必须等第一个回调执行完毕、锁被释放后才能进入。注意锁名的设计通常需要带上业务含义,不同的资源应当使用不同的锁名,避免无关任务互相阻塞。
独占锁适合写入或更新场景,但它也有明显的性能代价。如果多个标签页只是读取数据,完全没有必要串行化。此时就可以引入共享锁,让读操作并发执行。
共享锁:多读者与写者互斥
Web Locks API 不只支持互斥的独占锁,还可以通过第三个参数的mode字段申请共享锁。共享锁允许任意数量的任务同时持有同一个锁,只要当前没有独占锁被持有。反过来,当有共享锁存在时,独占锁必须等待所有共享锁释放后才能获取。这种读写互斥、读读并发的语义和操作系统里的读写锁一致。
共享锁的使用方式几乎相同,只需要把mode设置为'shared'。下面这段代码展示了三个读操作和一个写操作的竞争关系。
async function readConfig(tabId) {
return navigator.locks.request('user-config', { mode: 'shared' }, async () => {
console.log(`标签页 ${tabId} 开始读取`);
await new Promise(resolve => setTimeout(resolve, 500));
const data = JSON.parse(localStorage.getItem('userConfig') || '{}');
console.log(`标签页 ${tabId} 读取完成`);
return data;
});
}
async function writeConfig(newData, tabId) {
return navigator.locks.request('user-config', { mode: 'exclusive' }, async () => {
console.log(`标签页 ${tabId} 开始写入`);
await new Promise(resolve => setTimeout(resolve, 300));
localStorage.setItem('userConfig', JSON.stringify(newData));
console.log(`标签页 ${tabId} 写入完成`);
});
}
// 三个读操作可以同时进入
readConfig('A');
readConfig('B');
readConfig('C');
// 写操作需要等所有共享锁释放后才能执行
writeConfig({ theme: 'light' }, 'D');
共享锁最大的价值在于提高了读密集场景的吞吐量。多个标签页可以同时读取配置而互不干扰,一旦有一个写操作请求独占锁,后续的读操作也会被阻塞,直到写操作完成。这样就保证了读到的数据始终是一致的,不会出现读到写了一半的脏数据。
值得注意的是,共享锁并不是所有浏览器都实现了与独占锁完全一致的排队语义,使用前需要检查目标浏览器对mode: 'shared'的支持情况。不过现代Chrome和Edge已经完整支持,Firefox也从较新版本开始跟上。
非阻塞获取与中断等待
有些场景下,等待锁并不是最好的选择。例如一个低优先级的后台同步任务,如果拿不到锁,不如直接跳过,等下一次机会再来。Web Locks API 提供了ifAvailable选项,把它设置为true后,请求锁时会立即尝试获取;如果锁已经被占用,回调不会进入,整个请求直接返回null。
async function tryRunBackgroundSync() {
const result = await navigator.locks.request('background-sync', { ifAvailable: true }, async () => {
console.log('获得锁,开始后台同步');
await doSyncWork();
return true;
});
if (result === null) {
console.log('锁被占用,本次跳过');
}
}
非阻塞获取非常适合心跳上报、缓存预热、日志刷新这类允许延迟的任务。它的好处是避免了排队造成的资源闲置,也避免因为长时间等待锁而影响页面响应。你可以把三种锁机制对应到不同的资源竞争强度:独占锁用于强一致写入,共享锁用于高并发读取,非阻塞获取用于可放弃的辅助任务。
除了ifAvailable,request方法还接受signal参数,可以传入一个AbortSignal。当等待时间超过预期或者用户主动取消时,可以中止锁请求,不用一直挂在队列里。结合AbortController可以实现锁等待超时,代码大致如下。
async function requestWithTimeout(lockName, timeoutMs) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
await navigator.locks.request(lockName, { signal: controller.signal }, async () => {
console.log('拿到锁');
});
} catch (err) {
if (err.name === 'AbortError') {
console.log('等待超时,已放弃');
}
} finally {
clearTimeout(timer);
}
}
代码中err.name会得到AbortError,这是中止请求后抛出的标准错误名称。使用signal不会影响已经进入回调的任务,只会在等待阶段生效,因此不需要担心持有锁的任务被强制中断。
落地实践:多标签页防重复操作与注意事项
一个典型应用是防止用户在不同标签页重复提交同一个操作。比如一个后台管理页面,用户可能打开多个标签页同时点击同步按钮,如果没有协调,服务器会收到多份重复请求。借助Web Locks API,可以在发起请求前申请一个独占锁,锁内先检查本地标记,再决定是否执行同步。
async function syncFromMultipleTabs() {
return navigator.locks.request('manual-sync', async () => {
const flag = sessionStorage.getItem('sync-in-progress');
if (flag) {
return { skipped: true };
}
sessionStorage.setItem('sync-in-progress', '1');
try {
const res = await fetch('/api/sync', { method: 'POST' });
return await res.json();
} finally {
sessionStorage.removeItem('sync-in-progress');
}
});
}
这段代码里锁保证了检查和设置标记是原子的,不会出现两个标签页都检查到flag为空、同时发起同步的情况。当然,如果你希望同步操作只在一个标签页执行,其他标签页直接跳过,可以把ifAvailable: true加进去,这样第二个标签页不会排队等待。
使用Web Locks API时还有几个值得注意的点。锁的作用域是源级别的,也就是协议、域名、端口都相同的页面才能竞争同一个锁,跨源页面不会互相影响。锁本身不存储任何数据,它只是一个协调信号,业务状态仍然需要自己维护。长时间持有锁会阻塞所有等待者,所以回调里应当只包含必要的临界区代码,耗时的IO操作尽量在锁外完成,或者使用signal设置超时。最后,不要在持有一个锁时再去请求另一个锁,否则容易产生死锁;如果必须嵌套,务必保证所有标签页申请锁的顺序一致。
Web Locks API 并不是为了替代Worker或SharedWorker通信,而是为跨执行上下文提供一种轻量级的互斥原语。对于多标签页共享数据、防重复请求、协调后台任务等场景,它比基于localStorage轮询或BroadcastChannel协商的方案更可靠,代码也更直观。掌握独占锁、共享锁、非阻塞获取这三种机制,基本能覆盖大部分前端资源竞争问题。
Web Locks API独占锁共享锁修改时间:2026-09-29 02:38:04