timer.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. // Copyright 2006 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview A timer class to which other classes and objects can listen on.
  16. * This is only an abstraction above {@code setInterval}.
  17. *
  18. * @see ../demos/timers.html
  19. */
  20. goog.provide('goog.Timer');
  21. goog.require('goog.Promise');
  22. goog.require('goog.events.EventTarget');
  23. /**
  24. * Class for handling timing events.
  25. *
  26. * @param {number=} opt_interval Number of ms between ticks (default: 1ms).
  27. * @param {Object=} opt_timerObject An object that has {@code setTimeout},
  28. * {@code setInterval}, {@code clearTimeout} and {@code clearInterval}
  29. * (e.g., {@code window}).
  30. * @constructor
  31. * @extends {goog.events.EventTarget}
  32. */
  33. goog.Timer = function(opt_interval, opt_timerObject) {
  34. goog.events.EventTarget.call(this);
  35. /**
  36. * Number of ms between ticks
  37. * @private {number}
  38. */
  39. this.interval_ = opt_interval || 1;
  40. /**
  41. * An object that implements {@code setTimeout}, {@code setInterval},
  42. * {@code clearTimeout} and {@code clearInterval}. We default to the window
  43. * object. Changing this on {@link goog.Timer.prototype} changes the object
  44. * for all timer instances which can be useful if your environment has some
  45. * other implementation of timers than the {@code window} object.
  46. * @private {{setTimeout:!Function, clearTimeout:!Function}}
  47. */
  48. this.timerObject_ = /** @type {{setTimeout, clearTimeout}} */ (
  49. opt_timerObject || goog.Timer.defaultTimerObject);
  50. /**
  51. * Cached {@code tick_} bound to the object for later use in the timer.
  52. * @private {Function}
  53. * @const
  54. */
  55. this.boundTick_ = goog.bind(this.tick_, this);
  56. /**
  57. * Firefox browser often fires the timer event sooner (sometimes MUCH sooner)
  58. * than the requested timeout. So we compare the time to when the event was
  59. * last fired, and reschedule if appropriate. See also
  60. * {@link goog.Timer.intervalScale}.
  61. * @private {number}
  62. */
  63. this.last_ = goog.now();
  64. };
  65. goog.inherits(goog.Timer, goog.events.EventTarget);
  66. /**
  67. * Maximum timeout value.
  68. *
  69. * Timeout values too big to fit into a signed 32-bit integer may cause overflow
  70. * in FF, Safari, and Chrome, resulting in the timeout being scheduled
  71. * immediately. It makes more sense simply not to schedule these timeouts, since
  72. * 24.8 days is beyond a reasonable expectation for the browser to stay open.
  73. *
  74. * @private {number}
  75. * @const
  76. */
  77. goog.Timer.MAX_TIMEOUT_ = 2147483647;
  78. /**
  79. * A timer ID that cannot be returned by any known implementation of
  80. * {@code window.setTimeout}. Passing this value to {@code window.clearTimeout}
  81. * should therefore be a no-op.
  82. *
  83. * @private {number}
  84. * @const
  85. */
  86. goog.Timer.INVALID_TIMEOUT_ID_ = -1;
  87. /**
  88. * Whether this timer is enabled
  89. * @type {boolean}
  90. */
  91. goog.Timer.prototype.enabled = false;
  92. /**
  93. * An object that implements {@code setTimeout}, {@code setInterval},
  94. * {@code clearTimeout} and {@code clearInterval}. We default to the global
  95. * object. Changing {@code goog.Timer.defaultTimerObject} changes the object for
  96. * all timer instances which can be useful if your environment has some other
  97. * implementation of timers you'd like to use.
  98. * @type {{setTimeout, clearTimeout}}
  99. */
  100. goog.Timer.defaultTimerObject = goog.global;
  101. /**
  102. * Variable that controls the timer error correction. If the timer is called
  103. * before the requested interval times {@code intervalScale}, which often
  104. * happens on Mozilla, the timer is rescheduled.
  105. * @see {@link #last_}
  106. * @type {number}
  107. */
  108. goog.Timer.intervalScale = 0.8;
  109. /**
  110. * Variable for storing the result of {@code setInterval}.
  111. * @private {?number}
  112. */
  113. goog.Timer.prototype.timer_ = null;
  114. /**
  115. * Gets the interval of the timer.
  116. * @return {number} interval Number of ms between ticks.
  117. */
  118. goog.Timer.prototype.getInterval = function() {
  119. return this.interval_;
  120. };
  121. /**
  122. * Sets the interval of the timer.
  123. * @param {number} interval Number of ms between ticks.
  124. */
  125. goog.Timer.prototype.setInterval = function(interval) {
  126. this.interval_ = interval;
  127. if (this.timer_ && this.enabled) {
  128. // Stop and then start the timer to reset the interval.
  129. this.stop();
  130. this.start();
  131. } else if (this.timer_) {
  132. this.stop();
  133. }
  134. };
  135. /**
  136. * Callback for the {@code setTimeout} used by the timer.
  137. * @private
  138. */
  139. goog.Timer.prototype.tick_ = function() {
  140. if (this.enabled) {
  141. var elapsed = goog.now() - this.last_;
  142. if (elapsed > 0 && elapsed < this.interval_ * goog.Timer.intervalScale) {
  143. this.timer_ = this.timerObject_.setTimeout(
  144. this.boundTick_, this.interval_ - elapsed);
  145. return;
  146. }
  147. // Prevents setInterval from registering a duplicate timeout when called
  148. // in the timer event handler.
  149. if (this.timer_) {
  150. this.timerObject_.clearTimeout(this.timer_);
  151. this.timer_ = null;
  152. }
  153. this.dispatchTick();
  154. // The timer could be stopped in the timer event handler.
  155. if (this.enabled) {
  156. this.timer_ =
  157. this.timerObject_.setTimeout(this.boundTick_, this.interval_);
  158. this.last_ = goog.now();
  159. }
  160. }
  161. };
  162. /**
  163. * Dispatches the TICK event. This is its own method so subclasses can override.
  164. */
  165. goog.Timer.prototype.dispatchTick = function() {
  166. this.dispatchEvent(goog.Timer.TICK);
  167. };
  168. /**
  169. * Starts the timer.
  170. */
  171. goog.Timer.prototype.start = function() {
  172. this.enabled = true;
  173. // If there is no interval already registered, start it now
  174. if (!this.timer_) {
  175. // IMPORTANT!
  176. // window.setInterval in FireFox has a bug - it fires based on
  177. // absolute time, rather than on relative time. What this means
  178. // is that if a computer is sleeping/hibernating for 24 hours
  179. // and the timer interval was configured to fire every 1000ms,
  180. // then after the PC wakes up the timer will fire, in rapid
  181. // succession, 3600*24 times.
  182. // This bug is described here and is already fixed, but it will
  183. // take time to propagate, so for now I am switching this over
  184. // to setTimeout logic.
  185. // https://bugzilla.mozilla.org/show_bug.cgi?id=376643
  186. //
  187. this.timer_ = this.timerObject_.setTimeout(this.boundTick_, this.interval_);
  188. this.last_ = goog.now();
  189. }
  190. };
  191. /**
  192. * Stops the timer.
  193. */
  194. goog.Timer.prototype.stop = function() {
  195. this.enabled = false;
  196. if (this.timer_) {
  197. this.timerObject_.clearTimeout(this.timer_);
  198. this.timer_ = null;
  199. }
  200. };
  201. /** @override */
  202. goog.Timer.prototype.disposeInternal = function() {
  203. goog.Timer.superClass_.disposeInternal.call(this);
  204. this.stop();
  205. delete this.timerObject_;
  206. };
  207. /**
  208. * Constant for the timer's event type.
  209. * @const
  210. */
  211. goog.Timer.TICK = 'tick';
  212. /**
  213. * Calls the given function once, after the optional pause.
  214. * <p>
  215. * The function is always called asynchronously, even if the delay is 0. This
  216. * is a common trick to schedule a function to run after a batch of browser
  217. * event processing.
  218. *
  219. * @param {function(this:SCOPE)|{handleEvent:function()}|null} listener Function
  220. * or object that has a handleEvent method.
  221. * @param {number=} opt_delay Milliseconds to wait; default is 0.
  222. * @param {SCOPE=} opt_handler Object in whose scope to call the listener.
  223. * @return {number} A handle to the timer ID.
  224. * @template SCOPE
  225. */
  226. goog.Timer.callOnce = function(listener, opt_delay, opt_handler) {
  227. if (goog.isFunction(listener)) {
  228. if (opt_handler) {
  229. listener = goog.bind(listener, opt_handler);
  230. }
  231. } else if (listener && typeof listener.handleEvent == 'function') {
  232. // using typeof to prevent strict js warning
  233. listener = goog.bind(listener.handleEvent, listener);
  234. } else {
  235. throw Error('Invalid listener argument');
  236. }
  237. if (Number(opt_delay) > goog.Timer.MAX_TIMEOUT_) {
  238. // Timeouts greater than MAX_INT return immediately due to integer
  239. // overflow in many browsers. Since MAX_INT is 24.8 days, just don't
  240. // schedule anything at all.
  241. return goog.Timer.INVALID_TIMEOUT_ID_;
  242. } else {
  243. return goog.Timer.defaultTimerObject.setTimeout(listener, opt_delay || 0);
  244. }
  245. };
  246. /**
  247. * Clears a timeout initiated by {@link #callOnce}.
  248. * @param {?number} timerId A timer ID.
  249. */
  250. goog.Timer.clear = function(timerId) {
  251. goog.Timer.defaultTimerObject.clearTimeout(timerId);
  252. };
  253. /**
  254. * @param {number} delay Milliseconds to wait.
  255. * @param {(RESULT|goog.Thenable<RESULT>|Thenable)=} opt_result The value
  256. * with which the promise will be resolved.
  257. * @return {!goog.Promise<RESULT>} A promise that will be resolved after
  258. * the specified delay, unless it is canceled first.
  259. * @template RESULT
  260. */
  261. goog.Timer.promise = function(delay, opt_result) {
  262. var timerKey = null;
  263. return new goog
  264. .Promise(function(resolve, reject) {
  265. timerKey =
  266. goog.Timer.callOnce(function() { resolve(opt_result); }, delay);
  267. if (timerKey == goog.Timer.INVALID_TIMEOUT_ID_) {
  268. reject(new Error('Failed to schedule timer.'));
  269. }
  270. })
  271. .thenCatch(function(error) {
  272. // Clear the timer. The most likely reason is "cancel" signal.
  273. goog.Timer.clear(timerKey);
  274. throw error;
  275. });
  276. };