导读:本期聚焦于樱由罗创作的《JS如何操作Web Locks锁?独占锁、共享锁与非阻塞获取三种机制解决资源竞争》,敬请观看详情。多个浏览器标签页同时修改同一份本地数据时,后写入的总是覆盖先写入的结果,这种资源竞争问题在纯前端环境中一直缺乏优雅的解决方案。Web Locks API的出现改变了这一局面,它允许脚本通过navigator.locks.request方法申请全局锁,将异步操作串行化或按规则并发执行。该机制包含三种常见使用方式:独占锁适合写入操作,同一时刻只允许一个任务进入临界区;共享锁允许任意数量的读操作同时持有,但与独占锁互斥;通过ifAvailable选项还能实现非阻塞获取,拿不到锁时立即退出而非排队等待。本文从基础用法、锁模式差异、中断与超时等维度展开,结合具体代码演示这三种锁机制如何解决多标签页下的资源竞争问题。阅读后你可以直接将这些模式应用到本地存储、请求去重、状态同步等场景中。

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

JS如何操作Web 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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0929/63234.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。