/** * wait.js — 唯一的等待原语 * * 核心主张:**脚本里不允许出现裸 sleep 当作"等待"**。 * * tick(轮询节拍) = "多久检查一次" → 允许存在,默认 150ms * sleep(盲等) = "赌它 X 秒后会好" → 禁止用作等待手段 * * 为什么这是稳定性的关键:网络抖动时页面可能 300ms 才渲染,也可能 8s 才渲染。 * 写 `sleep(2000)` 的脚本在快的时候白等 1.7s,在慢的时候直接失败; * 而 `waitFor(条件, 超时)` 在快的时候立刻返回,慢的时候一直等到条件成立。 * 这把"网络有多慢"从"脚本能不能跑通"里彻底解耦。 * * 依赖:TikTokAuto.logger(调用时取,避免 concat 顺序耦合) */ TikTokAuto.wait = (function () { 'use strict'; var DEFAULT_TICK_MS = 150; function now() { return Date.now(); } /** * 轮询节拍。 * * ★ 这里是**唯一**的等待出口,所以「用户点了停止」的检查放在这一处就够了 —— * 全流程(等待应用启动、等待播放、等待结果卡片、等待评论区……共几十处等待) * 都会经过 tick,于是都能被打断,业务代码一行都不用改。 * bridge 未启用时是空操作(独立运行时零开销)。 */ function tick(ms) { if (TikTokAuto.bridge) { TikTokAuto.bridge.checkStop(); } sleep(ms === undefined ? DEFAULT_TICK_MS : ms); } /** * 反复求值 predicate 直到它返回"真值"或超时。 * * predicate 的返回值语义: * - null / false → 条件未满足,继续轮询 * - 其它任何值(含 0!) → 条件已满足,原样返回该值 * 之所以不用 `if (value)` 判断,是为了允许 predicate 合法地返回 0 / '' 这类"假值结果"。 * * @returns 满足时的返回值;超时返回 null */ function poll(predicate, timeoutMs, tickMs) { var deadline = now() + timeoutMs; var step = tickMs === undefined ? DEFAULT_TICK_MS : tickMs; var last = null; for (;;) { try { var value = predicate(); if (value !== null && value !== false && value !== undefined) { return value; } last = value; } catch (error) { last = error; } if (now() >= deadline) { return null; } tick(Math.min(step, Math.max(1, deadline - now()))); } } /** * 带日志与语义名字的 poll —— 业务代码应该用这个,而不是裸 poll。 * 超时会记 WARN(而不是静默返回 null),这样"卡在哪一步"永远有据可查。 */ function waitFor(name, predicate, timeoutMs, tickMs) { var began = now(); var value = poll(predicate, timeoutMs, tickMs); var cost = now() - began; if (value === null) { TikTokAuto.logger.warn('waitFor', name + ' 超时(等了 ' + cost + 'ms / 上限 ' + timeoutMs + 'ms)'); } else { TikTokAuto.logger.info('waitFor', name + ' 就绪(' + cost + 'ms)'); } return value; } /** 等待条件成立,并把成立时的值原样返回;语义等价于 waitFor */ function until(condition, timeoutMs, tickMs) { return poll(condition, timeoutMs, tickMs); } /** 等待某个东西消失(例如弹窗、加载圈) */ function untilGone(isPresent, timeoutMs, tickMs) { return poll(function () { return isPresent() ? null : true; }, timeoutMs, tickMs); } /** * 等待页面"安静"下来。 * * 判据不是时间,而是**可见文案的拼接签名连续 N 次不变**。 * 这比 sleep 可靠:加载中会不断有新节点出现,签名就会变; * 真正稳定后才返回,因此天然适配"网络慢 → 渲染慢"。 * * @param sample () => string,页面签名 * @param options { maxMs, tickMs, stableTimes } */ function waitQuiet(sample, options) { var settings = options || {}; var maxMs = settings.maxMs === undefined ? 8000 : settings.maxMs; var tickMs = settings.tickMs === undefined ? 250 : settings.tickMs; var stableTimes = settings.stableTimes === undefined ? 3 : settings.stableTimes; var deadline = now() + maxMs; var previous = null; var sameCount = 0; while (now() < deadline) { var current = sample(); if (current === previous) { sameCount += 1; if (sameCount >= stableTimes) { return true; } } else { sameCount = 0; previous = current; } tick(tickMs); } return false; } return { DEFAULT_TICK_MS: DEFAULT_TICK_MS, tick: tick, poll: poll, waitFor: waitFor, until: until, untilGone: untilGone, waitQuiet: waitQuiet }; }());