1. 简介
本节为非规范性内容。
脚本可针对特定的资源名称和模式发起锁请求。调度算法会根据当前和先前的请求状态,最终授予锁请求。锁即为已被授予的请求;它具有资源名称和模式,并以对象的形式返回给脚本。只要锁被持有,可能会阻止其他锁请求被授予(取决于名称和模式)。脚本可释放锁,此时可能允许其他锁请求被授予。
该 API 提供了可选功能,按需使用,包括:
-
异步任务返回值,
-
共享与独占锁模式,
-
条件获取,
-
用于查询锁状态的诊断,
-
用于防止死锁的应急机制。
协作协调在共享agent的存储桶范围内进行;这可能跨越多个agent 集群。
注意: Agent大致对应于窗口(标签页)、iframe 和 worker。Agent 集群在某些用户代理实现中对应于独立进程。
1.1. 用法概述
该 API 的用法如下:
-
请求锁。
-
在异步任务中持有锁进行操作。
-
任务完成后锁自动释放。
API 的基本用法如下:
navigator. locks. request( 'my_resource' , async lock=> { // 已获取锁。 await do_something(); await do_something_else(); // 现在锁将被释放。 });
在异步函数中,可以使用 await 等待锁请求:
// 请求锁之前。 await navigator. locks. request( 'my_resource' , async lock=> { // 已获取锁。 await do_something(); // 现在锁将被释放。 }); // 锁释放之后
1.2. 应用动机与场景
一个基于 Web 的文档编辑器将状态存储在内存中以加快访问,并将更改(作为一系列记录)持久化到诸如Indexed Database API之类的存储 API,以增强可靠性和离线使用,同时同步到服务器以支持跨设备使用。当同一文档在两个标签页中被打开编辑时,必须在标签之间协调操作,例如只允许一个标签进行更改或同步文档。需要协调哪个标签处于活动状态(并将内存状态与存储 API 同步),并在活动标签离开(导航、关闭、崩溃)时让另一个标签成为活动标签。
在数据同步服务中,会指定一个“主标签页”。只有该标签页应执行某些操作(如网络同步、清理队列数据等)。它持有锁且从不释放。其他标签页可以尝试获取锁,这些尝试将被排队。如果“主标签页”崩溃或关闭,则其他标签页之一将获得锁并成为新的主标签页。
Indexed Database API定义了一个事务模型,允许在同一源下的多个命名存储分区间进行共享读和独占写访问。将这一概念作为原语暴露出来,允许基于资源可用性调度任何 Web 平台活动,例如允许为其他存储类型(如 Caches [Service-Workers])组合事务,跨存储类型,甚至跨非存储 API(如网络请求)。
2. 基本概念
本规范中:
-
浏览器中的独立用户配置文件被视为独立的用户代理。
-
每个隐私模式浏览会话都视为一个独立的用户代理。
以下步骤入队所用的任务来源为Web Locks 任务来源。
2.1. 资源名称
资源名称是由 Web 应用选择的JavaScript 字符串,用于表示抽象资源。
资源名称除了用于调度算法外没有外部意义,但在共享同一agent的存储桶之间是全局的。Web 应用可自由选择资源命名方案。
encodeURIComponent( db_name) + '/' + encodeURIComponent( store_name)
以 U+002D 连字符(-)开头的资源名称为保留项;请求此类名称将导致抛出异常。
2.2. 锁管理器
锁管理器封装锁和锁请求的状态。每个存储桶通过与 Web Locks API 相关联的存储瓶包含一个锁管理器。
注意: 共享同一存储桶的页面和 worker(agent)在同一用户代理下即使处于不同的浏览上下文,也共享同一个锁管理器。
此处需完善与[Storage]的集成,包括如何从给定环境正确获取锁管理器。
2.3. 锁模式与调度
模式为"exclusive"或"shared"。模式可用于模拟常见的读者-写者锁模式。如果持有"exclusive"锁,则不能授予同名的其他锁。如果持有"shared"锁,则可以授予其他同名的"shared"锁,但不能授予"exclusive"锁。API
默认模式为"exclusive"。
其他属性如超时、公平性等也可能影响调度。
2.4. 锁
锁表示对共享资源的独占访问。
锁具有clientId,为不透明字符串。
锁具有mode,为"exclusive"或"shared"。
锁具有waiting promise,即一个 Promise。
锁具有released promise,即一个 Promise。
-
在授予锁时回调隐式或显式提供的 Promise,决定锁持有时长。该 Promise 结算时,锁被释放。称为锁的waiting promise。
-
LockManager的request()方法返回的 Promise,在锁释放或请求中止时结算。称为锁的released promise。
const p1= navigator. locks. request( 'resource' , lock=> { const p2= new Promise( r=> { // 使用锁并处理 promise... }); return p2; });
上述示例中,p1为released promise,p2为waiting promise。
注意,大多数代码会将回调实现为async函数,返回的 Promise 为隐式,如下例所示:
const p1= navigator. locks. request( 'resource' , async lock=> { // 使用锁的逻辑... });
上述代码中,waiting
promise未命名,但作为匿名async回调的返回值依然存在。
进一步说明:如果回调非async且返回非 Promise,则返回值会被立即解析为 Promise;锁将在下一个微任务中释放,released
promise也将在后续微任务中解析。
当锁 lock的等待 promise被解决(fulfilled 或 rejected)时,将以下步骤加入锁任务队列:
-
释放锁lock。
-
解析lock的released promise,其值为lock的waiting promise。
2.5. 锁请求
锁请求表示对锁的待处理请求。
每个锁管理器都有一个锁请求队列映射,它是一个映射,将资源名映射到锁请求队列。
当以下步骤返回 true 时,锁请求request被称为可授予:
2.6. 锁的终止
每当卸载文档清理步骤针对某个document运行时,使用其agent执行终止剩余锁和请求。
当某个agent终止时,使用该 agent 执行终止剩余锁和请求。
当前仅适用于 worker,且定义较为模糊,因为没有规范的方法在 worker 终止时运行这些步骤。
3. API
3.1. Navigator 混入
[SecureContext ]interface mixin {NavigatorLocks readonly attribute LockManager locks ; };Navigator includes NavigatorLocks ;WorkerNavigator includes NavigatorLocks ;
每个环境设置对象都有一个LockManager对象。
locks getter 的步骤是返回this的相关设置对象的LockManager对象。
3.2. LockManager
类
[SecureContext ,Exposed =(Window ,Worker )]interface {LockManager Promise <any >request (DOMString ,name LockGrantedCallback );callback Promise <any >request (DOMString ,name LockOptions ,options LockGrantedCallback );callback Promise <LockManagerSnapshot >query (); };callback =LockGrantedCallback Promise <any > (Lock ?);lock enum {LockMode ,"shared" };"exclusive" dictionary {LockOptions LockMode = "exclusive";mode boolean =ifAvailable false ;boolean =steal false ;AbortSignal ; };signal dictionary {LockManagerSnapshot sequence <LockInfo >;held sequence <LockInfo >; };pending dictionary {LockInfo DOMString ;name LockMode ;mode DOMString ; };clientId
LockManager实例允许脚本发起锁请求并查询锁管理器的状态。
3.2.1.
request()
方法
- promise = navigator . locks .
request(name, callback)- promise = navigator . locks .
request(name, options, callback) - promise = navigator . locks .
-
request()方法用于请求锁。name(第一个参数)是资源名字符串。
callback(最后一个参数)是一个回调函数,在锁被授予时调用,参数为
Lock。该回调由脚本指定,通常为async函数。锁会一直持有直到回调函数完成。如果传入的是非 async 回调,则会自动包装为立即 resolve 的 promise,因此锁只会在同步回调期间持有。
返回的promise会在锁释放后以回调的结果 resolve(或 reject),如果请求被中止则 reject。
示例:
try { const result= await navigator. locks. request( 'resource' , async lock=> { // 此处持有锁。 await do_something(); await do_something_else(); return "ok" ; // 锁将在此处释放。 }); // |result| 为回调的返回值。 } catch ( ex) { // 如果回调抛出异常,会在此处捕获。 }
无论回调是正常返回还是抛出异常,锁都会被释放。
可以通过第二个参数指定options字典,callback始终为最后一个参数。
- options . mode
-
mode选项可以为"exclusive"(默认)或"shared"。多个标签页/worker可以以"shared"模式持有同一资源的锁,但只有一个标签页/worker可以以"exclusive"模式持有该资源的锁。最常见的用途是允许多个读取者同时访问资源,但防止修改。所有读取锁释放后,单个独占写入者可以获取锁进行修改,之后可以再次由独占写入者或更多共享读取者持有。
await navigator. locks. request( 'resource' , { mode: 'shared' }, async lock=> { // 此处持有锁。其他上下文也可能以共享模式持有锁, // 但不会有其他上下文以独占模式持有锁。 });
- options . ifAvailable
-
如果
ifAvailable选项为true,则仅在无需等待的情况下授予锁。注意这仍然不是同步的;在许多用户代理中,这需要跨进程通信以判断锁是否可授予。如果无法授予锁,则回调以null调用。(这是预期行为,请求不会被拒绝。)
await navigator. locks. request( 'resource' , { ifAvailable: true }, async lock=> { if ( ! lock) { // 未获取锁。可在此处做相应处理。 return ; } // 此处持有锁。 });
- options . signal
-
signal选项可设置为AbortSignal。这允许中止锁请求,例如在未及时授予锁时:
const controller= new AbortController(); setTimeout(() => controller. abort(), 200 ); // 最多等待 200ms。 try { await navigator. locks. request( 'resource' , { signal: controller. signal}, async lock=> { // 此处持有锁。 }); // 此处已完成锁相关操作。 } catch ( ex) { // 如果定时器触发,|ex| 会是 error name 为 "AbortError" 的 DOMException。 }
如果在锁授予前收到中止信号,则请求 promise 会以AbortError拒绝。一旦锁被授予,signal
将被忽略。
- options . steal
-
如果
steal选项为true,则会释放该资源的所有已持有锁(这些锁的released promise会以AbortErrorresolve),并授予该请求,抢占所有排队请求。如果 web 应用检测到不可恢复的状态——例如某个协调点(如 Service Worker)发现持有锁的标签页不再响应——则可使用此选项“抢占”锁。
steal选项。
使用后,之前持有锁的代码将无法保证自己是唯一访问该资源的上下文。
同样,使用该选项的代码也无法保证其他上下文不会继续以拥有资源的方式执行。
该选项主要用于 web 应用在应用或用户代理出现异常时尝试恢复,此时行为本就不可预测。
request(name, callback)和
request(name, options, callback)
方法的步骤如下:
-
如果未传入options,则令options为一个新的
LockOptions字典,成员为默认值。 -
如果environment的相关全局对象的关联 Document不是完全激活,则返回一个以"
InvalidStateError"DOMException拒绝的 promise。 -
令manager为获取锁管理器(参数为environment)的结果。如果返回失败,则返回一个以"
SecurityError"DOMException拒绝的 promise。 -
如果name以 U+002D HYPHEN-MINUS (-) 开头,则返回一个以"
NotSupportedError"DOMException拒绝的 promise。 -
如果options["
steal"]和options["ifAvailable"]均为 true,则返回一个以"NotSupportedError"DOMException拒绝的 promise。 -
如果options["
steal"]为 true 且options["mode"]不是"exclusive",则返回一个以"NotSupportedError"DOMException拒绝的 promise。 -
如果options["
signal"]存在,且options["steal"]或options["ifAvailable"]为 true,则返回一个以"NotSupportedError"DOMException拒绝的 promise。 -
如果options["
signal"]存在且已中止,则返回一个以options["signal"]的中止原因拒绝的 promise。 -
令promise为新建 promise。
-
使用promise、当前agent、environment的id、manager、callback、name、options["
mode"]、options["ifAvailable"]、options["steal"]和options["signal"]请求锁。 -
返回promise。
3.2.2.
query()
方法
- state = await navigator . locks .
query() -
query()方法可用于生成某个源的锁管理器状态快照,允许 web 应用检查其锁使用情况,用于日志或调试。返回的 promise resolve 为state,一个普通数据结构(即 JSON 格式),结构如下:
{ held: [ { name: "resource1" , mode: "exclusive" , clientId: "8b1e730c-7405-47db-9265-6ee7c73ac153" }, { name: "resource2" , mode: "shared" , clientId: "8b1e730c-7405-47db-9265-6ee7c73ac153" }, { name: "resource2" , mode: "shared" , clientId: "fad203a5-1f31-472b-a7f7-a3236a1f6d3b" }, ], pending: [ { name: "resource1" , mode: "exclusive" , clientId: "fad203a5-1f31-472b-a7f7-a3236a1f6d3b" }, { name: "resource1" , mode: "exclusive" , clientId: "d341a5d0-1d8d-4224-be10-704d1ef92a15" }, ] }
query()方法的步骤如下:
-
如果environment的相关全局对象的关联 Document不是完全激活,则返回一个以"
InvalidStateError"DOMException拒绝的 promise。 -
令manager为获取锁管理器(参数为environment)的结果。如果返回失败,则返回一个以"
SecurityError"DOMException拒绝的 promise。 -
令promise为新建 promise。
-
返回promise。
3.3. Lock类
[SecureContext ,Exposed =(Window ,Worker )]interface {Lock readonly attribute DOMString name ;readonly attribute LockMode mode ; };
4. 算法
4.1. 请求锁
-
令request为新建的锁请求(agent、clientId、manager、name、mode、callback、promise、signal)。
-
如果signal存在,则添加算法signal to abort the request request(参数signal)到signal。
-
-
令queueMap为manager的锁请求队列映射。
-
令queue为从queueMap获取name对应的锁请求队列的结果。
-
令held为manager的已持有锁集合。
-
如果steal为 true,则执行以下步骤:
-
遍历held中的每个lock:
-
如果lock的name为name,则执行:
-
拒绝lock的released promise,错误为"
AbortError"DOMException。
-
-
前置request到queue。
-
-
否则,执行以下步骤:
-
处理锁请求队列queue。
-
-
返回request。
4.2. 释放锁
4.3. 中止请求
4.4. 处理指定资源名的锁请求队列
-
遍历queue中的每个request:
-
如果request不是grantable,则返回。
注意:队列中只有第一个成员是 grantable。如果某项不可授予,则后续所有项也自动不可授予。
-
移除request从queue。
-
令agent为request的agent。
-
令manager为request的manager。
-
令clientId为request的clientId。
-
令name为request的name。
-
令mode为request的mode。
-
令callback为request的callback。
-
令p为request的promise。
-
令signal为request的signal。
-
令waiting为新建 promise。
-
令lock为新建的锁,参数为agentagent、clientIdclientId、managermanager、modemode、namename、released promisep、waiting promisewaiting。
-
4.5. 锁状态快照
已持有锁状态的快照不保证顺序。
5. 使用注意事项
本节为非规范性内容。
5.1. 死锁
死锁是并发计算中的一个概念,针对特定锁管理器的死锁可以通过本 API 引入。
脚本 1:
navigator. locks. request( 'A' , async a=> { await navigator. locks. request( 'B' , async b=> { // do stuff with A and B }); });
脚本 2:
navigator. locks. request( 'B' , async b=> { await navigator. locks. request( 'A' , async a=> { // do stuff with A and B }); });
如果脚本 1 和脚本 2几乎同时运行,则可能出现脚本 1持有锁 A、脚本 2持有锁 B,且双方都无法继续——即死锁。这不会影响整个用户代理,不会暂停标签页,也不会影响同源的其他脚本,但此功能会被阻塞。
防止死锁需要谨慎。一个方法是始终按严格顺序获取多个锁。
如下辅助函数可用于按一致顺序请求多个锁:
async function requestMultiple( resources, callback) { const sortedResources= [... resources]; sortedResources. sort(); // 始终按相同顺序请求。 async function requestNext( locks) { return await navigator. locks. request( sortedResources. shift(), async lock=> { // 当前持有此锁及之前所有已请求的锁。 locks. push( lock); // 如有需要,递归请求下一个锁。 if ( sortedResources. length> 0 ) return await requestNext( locks); // 否则,执行回调。 return await callback( locks); // 回调返回(或抛出)后,所有锁将被释放。 }); } return await requestNext([]); }
实际使用中,多锁场景往往并不如此直接——库和工具常常会无意中掩盖锁的使用。
6. 安全与隐私注意事项
6.1. 锁作用域
锁管理器的作用域定义很重要,因为它决定了隐私边界。锁可作为临时状态保留机制,也可像存储 API 一样作为通信机制,其权限不得高于存储机制。用户代理如果对某项服务施加更细粒度的隔离,也必须对其他服务施加同样的隔离;例如,用户代理因隐私原因对同一源的顶级页面(第一方)和跨域 iframe(第三方)分别分区存储,则也必须对锁进行类似分区。
这也为 Web 应用作者提供了合理预期:如果对某存储资源获取了锁,则所有同源浏览上下文都必须观察到相同状态。
6.2. 隐私浏览
每个隐私模式浏览会话在本 API 中视为独立的用户代理。即,隐私模式外请求/持有的锁不会影响隐私模式内的锁,反之亦然。这既防止网站判断会话是否为“隐身”,也不允许不同会话间通信。
6.3. 实现风险
实现必须确保锁不会跨源。否则会为不同源脚本间通信提供侧信道,或允许一个源的脚本干扰另一个源的行为(如拒绝服务)。
6.4. 安全与隐私清单
W3C TAG 制定了安全与隐私自查问卷,供规范编辑者参考。相关问题如下:
-
本规范不涉及个人身份信息或高价值数据。
-
未为某个源引入可跨会话持久化的新状态。
-
未向 Web 暴露新的持久化跨源状态。
-
未向某个源暴露其当前无法访问的新数据(如通过轮询[IndexedDB-2])。
-
未启用新的脚本执行/加载机制。
-
本规范不允许某个源访问以下内容:
-
用户位置。
-
用户设备上的传感器。
-
用户本地计算环境的相关信息。
-
访问其他设备。
-
对用户代理原生 UI 的任何控制。
-
-
未向 Web 暴露临时标识符。所有资源名均由 Web 应用自身提供。
-
如用户代理区分第一方与第三方存储,则锁行为也区分。见§ 6.1 锁作用域。
-
用户代理“隐身”模式下的行为见§ 6.2 隐私浏览。
-
本 API 不会将数据持久化到用户本地设备。
-
本 API 不允许降低默认安全特性。
7. 致谢
特别感谢 Alex Russell, Andreas Butler, Anne van Kesteren, Boris Zbarsky, Chris Messina, Darin Fisher, Domenic Denicola, Gus Caplan, Harald Alvestrand, Jake Archibald, Kagami Sascha Rosylight, L. David Baron, Luciano Pacheco, Marcos Caceres, Ralph Chelala, Raymond Toy, Ryan Fioravanti, 以及 Victor Costan 对本提案的贡献。
特别感谢 Tab Atkins, Jr. 创建和维护 Bikeshed,本规范所用的文档工具,以及他在文档编写方面的建议。