| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- /**
- * 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
- };
- }());
|