当前位置:首页 > 文章列表 > 文章 > 前端 > 前端怎样用 Web Locks 协调多标签页任务

前端怎样用 Web Locks 协调多标签页任务

来源:17golang原创 2026-10-18 02:42:28 0浏览 收藏

多个同源标签页同时刷新资料、同步草稿或更新缓存时,真正需要协调的不是“哪个标签页更重要”,而是“同一份共享资源在同一时刻只能由谁处理”。Web Locks API 用一个由业务自己命名的锁,把这类互斥范围交给浏览器管理:拿到锁的异步回调继续工作,其他同名请求排队,回调返回后锁自动释放。

官方资料地址:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Locks_API。下面只讨论锁名、等待和释放,不把 Web Locks 当成服务端分布式锁,也不涉及浏览器截图或真实运行结果。

先把共享资源映射成稳定锁名

锁名是协调协议的核心。假设多个标签页都可能刷新当前用户资料,那么它们应该共同使用 refresh-profile,而不是把标签页编号拼进锁名。前者表达“同一份资料刷新任务互斥”,后者会让每个标签页拿到不同的锁,失去协调作用。

锁的范围还受源约束:同一个源下使用相同名字的请求才会进入同一套锁管理关系。锁名要从资源本身来,而不是从调用者来;如果任务是按项目区分的,可以使用 refresh-project-42 这类稳定命名,但不要把随机数作为互斥键。

标签页 A 和标签页 B 通过同一个 refresh-profile 锁名进入 navigator.locks 和刷新任务
图1:标签页 A、标签页 B 指向同一个稳定锁名,再由 navigator.locks 把刷新任务放入 request 回调;这是结构说明图,不是浏览器截图。

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 回调里,成功和异常都能走到同一个生命周期出口。

排队请求、held lock、异步回调与 Promise 在正常完成和异常出口后汇入锁释放
图2:回调返回的 Promise 代表持锁范围,正常完成和异常出口都汇入锁释放;连线是静态关系,不是运行证据。
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 结束,浏览器据此结束持有范围。

取消排队后,已经持锁的标签页会被中断吗?

不会。取消某个等待请求只影响这个请求;已经取得锁的回调是否停止,要由它自己的取消信号和业务逻辑决定。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
前端多个标签页怎样同步登录状态变化前端多个标签页怎样同步登录状态变化
上一篇
前端多个标签页怎样同步登录状态变化
前端怎样观察动态节点并及时断开监听
下一篇
前端怎样观察动态节点并及时断开监听
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    511次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    571次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    597次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    549次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    365次使用