GM_lock — 一个用于用户脚本的轻量级跨标签页异步锁
一个轻量、无依赖的用户脚本互斥锁(mutex),通过 GM.setValue + GM_addValueChangeListener 进行协调,因此在任意时刻 只有一个标签页/上下文 会执行临界区代码。适用于共享同一用户脚本存储的多个标签页和 iframe。
安装 / @require
这是一个库,而不是独立脚本。请在你的用户脚本头部加入:// @require https://scriptcat.org/lib/4709/0.1.0/GM_lock.js?sha384-dbz4ENOCrR50Xa7gxF6jsZLb8ExaYEMuOpTpDgw6Xt4yavf5ZgoPnbaoN4M3MIMQ
API
// JavaScript
var GM_lock: (tag: string, func: () => (void | Promise<void>)) => Promise<void>;
tag:用于标识锁作用域的字符串。相同 tag ⇒ 相同锁。
func:你的临界区(可同步或异步)。
返回:当
func执行完成后 resolve 的Promise<void>。func中的错误会被捕获并打印到控制台;它们不会导致 promise reject。
注意: 当前实现不会返回
func的返回值(始终以void解析)。如果需要返回结果,请在func内写入外部变量或存储。
使用示例
// 所有标签页中同一时刻只有一个实例会进入此代码块
await GM_lock('sync-cache', async () => {
const res = await fetch('https://api.example.com/data');
const data = await res.json();
// 处理数据;按需持久化
await GM.setValue('cache:data', data);
});
按功能使用简短、稳定的 tag,例如:'queue:upload'、'cache:refresh'。
管理器兼容性与所需授权
需要支持以下 API 的用户脚本管理器:
GM.setValueGM_addValueChangeListenerGM_removeValueChangeListener
可选/条件需求(仅在启用 cleanup 时需要;见 Options):
GM.deleteValues(或旧版别名GM_deleteValues)
已在 ScriptCat、Tampermonkey、Violentmonkey 测试;其他支持上述 API 的环境理论上也可使用。
工作原理(高层概述)
每个竞争者会构造一个 锁 id:
id = <大时间戳>_<随机 base36>,并使用带有以下前缀的 按 tag 分组 键:ack键:GM_lock::<tag>::acknxt键:GM_lock::<tag>::nxt
竞争者通过写入
ack来进行“宣布”,然后通过nxt中转。同时竞争的伙伴会收集在同一 ack 时间点 写入的竞争者,经过一个小的 冲突间隔(默认约 500 ms)后,通过对收集的 id 排序并选择最小者,确定胜者。胜者进入
func。执行完毕后,胜者再次更新
ack以释放锁,若需要则允许下一轮选举。
这种事件驱动的设计能最小化轮询并在多个标签页/框架间进行良好协调。
配置项(源代码中的内部常量)
COLLISION_INTERVAL = 500—— 在选举胜者前的等待,用于减少竞争窗口。CLEANUP_VALUES = false—— 若设为true,使用后会移除辅助键(需要GM.deleteValues)。SHOW_LOG = false—— 若设为true,通过GM.setValue输出时间标记(调试用)。
说明与注意事项
- 不要在
func中长时间阻塞事件循环;优先使用await异步工作。(其他竞争者依赖事件和短计时器进行协调。) - 如果某标签页在持锁时崩溃,下一轮选举会因 ack/nxt 刷新和间隔机制而继续推进。
- 当前实现 仅记录错误而不 reject;如需错误上报,请在
func内自行处理。
许可协议
公共领域 — The Unlicense。
本库在源码头部附带完整 Unlicense 声明,简要概括:
- 你可以自由复制、修改、发布、使用、编译、销售、再分发本软件,
用于任何目的(商业或非商业),以任何方式。 - 作者放弃在版权法下的全部权利,不对软件做任何形式的担保。
- 使用造成的任何损失,作者概不负责。
详情可参考:https://unlicense.org