wait.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. /**
  2. * wait.js — 唯一的等待原语
  3. *
  4. * 核心主张:**脚本里不允许出现裸 sleep 当作"等待"**。
  5. *
  6. * tick(轮询节拍) = "多久检查一次" → 允许存在,默认 150ms
  7. * sleep(盲等) = "赌它 X 秒后会好" → 禁止用作等待手段
  8. *
  9. * 为什么这是稳定性的关键:网络抖动时页面可能 300ms 才渲染,也可能 8s 才渲染。
  10. * 写 `sleep(2000)` 的脚本在快的时候白等 1.7s,在慢的时候直接失败;
  11. * 而 `waitFor(条件, 超时)` 在快的时候立刻返回,慢的时候一直等到条件成立。
  12. * 这把"网络有多慢"从"脚本能不能跑通"里彻底解耦。
  13. *
  14. * 依赖:TikTokAuto.logger(调用时取,避免 concat 顺序耦合)
  15. */
  16. TikTokAuto.wait = (function () {
  17. 'use strict';
  18. var DEFAULT_TICK_MS = 150;
  19. function now() {
  20. return Date.now();
  21. }
  22. /**
  23. * 轮询节拍。
  24. *
  25. * ★ 这里是**唯一**的等待出口,所以「用户点了停止」的检查放在这一处就够了 ——
  26. * 全流程(等待应用启动、等待播放、等待结果卡片、等待评论区……共几十处等待)
  27. * 都会经过 tick,于是都能被打断,业务代码一行都不用改。
  28. * bridge 未启用时是空操作(独立运行时零开销)。
  29. */
  30. function tick(ms) {
  31. if (TikTokAuto.bridge) {
  32. TikTokAuto.bridge.checkStop();
  33. }
  34. sleep(ms === undefined ? DEFAULT_TICK_MS : ms);
  35. }
  36. /**
  37. * 反复求值 predicate 直到它返回"真值"或超时。
  38. *
  39. * predicate 的返回值语义:
  40. * - null / false → 条件未满足,继续轮询
  41. * - 其它任何值(含 0!) → 条件已满足,原样返回该值
  42. * 之所以不用 `if (value)` 判断,是为了允许 predicate 合法地返回 0 / '' 这类"假值结果"。
  43. *
  44. * @returns 满足时的返回值;超时返回 null
  45. */
  46. function poll(predicate, timeoutMs, tickMs) {
  47. var deadline = now() + timeoutMs;
  48. var step = tickMs === undefined ? DEFAULT_TICK_MS : tickMs;
  49. var last = null;
  50. for (;;) {
  51. try {
  52. var value = predicate();
  53. if (value !== null && value !== false && value !== undefined) {
  54. return value;
  55. }
  56. last = value;
  57. } catch (error) {
  58. last = error;
  59. }
  60. if (now() >= deadline) {
  61. return null;
  62. }
  63. tick(Math.min(step, Math.max(1, deadline - now())));
  64. }
  65. }
  66. /**
  67. * 带日志与语义名字的 poll —— 业务代码应该用这个,而不是裸 poll。
  68. * 超时会记 WARN(而不是静默返回 null),这样"卡在哪一步"永远有据可查。
  69. */
  70. function waitFor(name, predicate, timeoutMs, tickMs) {
  71. var began = now();
  72. var value = poll(predicate, timeoutMs, tickMs);
  73. var cost = now() - began;
  74. if (value === null) {
  75. TikTokAuto.logger.warn('waitFor', name + ' 超时(等了 ' + cost + 'ms / 上限 ' + timeoutMs + 'ms)');
  76. } else {
  77. TikTokAuto.logger.info('waitFor', name + ' 就绪(' + cost + 'ms)');
  78. }
  79. return value;
  80. }
  81. /** 等待条件成立,并把成立时的值原样返回;语义等价于 waitFor */
  82. function until(condition, timeoutMs, tickMs) {
  83. return poll(condition, timeoutMs, tickMs);
  84. }
  85. /** 等待某个东西消失(例如弹窗、加载圈) */
  86. function untilGone(isPresent, timeoutMs, tickMs) {
  87. return poll(function () {
  88. return isPresent() ? null : true;
  89. }, timeoutMs, tickMs);
  90. }
  91. /**
  92. * 等待页面"安静"下来。
  93. *
  94. * 判据不是时间,而是**可见文案的拼接签名连续 N 次不变**。
  95. * 这比 sleep 可靠:加载中会不断有新节点出现,签名就会变;
  96. * 真正稳定后才返回,因此天然适配"网络慢 → 渲染慢"。
  97. *
  98. * @param sample () => string,页面签名
  99. * @param options { maxMs, tickMs, stableTimes }
  100. */
  101. function waitQuiet(sample, options) {
  102. var settings = options || {};
  103. var maxMs = settings.maxMs === undefined ? 8000 : settings.maxMs;
  104. var tickMs = settings.tickMs === undefined ? 250 : settings.tickMs;
  105. var stableTimes = settings.stableTimes === undefined ? 3 : settings.stableTimes;
  106. var deadline = now() + maxMs;
  107. var previous = null;
  108. var sameCount = 0;
  109. while (now() < deadline) {
  110. var current = sample();
  111. if (current === previous) {
  112. sameCount += 1;
  113. if (sameCount >= stableTimes) {
  114. return true;
  115. }
  116. } else {
  117. sameCount = 0;
  118. previous = current;
  119. }
  120. tick(tickMs);
  121. }
  122. return false;
  123. }
  124. return {
  125. DEFAULT_TICK_MS: DEFAULT_TICK_MS,
  126. tick: tick,
  127. poll: poll,
  128. waitFor: waitFor,
  129. until: until,
  130. untilGone: untilGone,
  131. waitQuiet: waitQuiet
  132. };
  133. }());