1 // ========================================================================== 2 // Project: SproutCore - JavaScript Application Framework 3 // Copyright: ©2006-2011 Strobe Inc. and contributors. 4 // Portions ©2008-2011 Apple Inc. All rights reserved. 5 // License: Licensed under MIT license (see license.js) 6 // ========================================================================== 7 8 /** 9 @class 10 11 A Timer executes a method after a defined period of time. Timers are 12 significantly more efficient than using setTimeout() or setInterval() 13 because they are cooperatively scheduled using the run loop. Timers are 14 also gauranteed to fire at the same time, making it far easier to keep 15 multiple timers in sync. 16 17 ## Overview 18 19 Timers were created for SproutCore as a way to efficiently defer execution 20 of code fragments for use in Animations, event handling, and other tasks. 21 22 Browsers are typically fairly inconsistent about when they will fire a 23 timeout or interval based on what the browser is currently doing. Timeouts 24 and intervals are also fairly expensive for a browser to execute, which 25 means if you schedule a large number of them it can quickly slow down the 26 browser considerably. 27 28 Timers, on the other handle, are scheduled cooperatively using the 29 SC.RunLoop, which uses exactly one timeout to fire itself when needed and 30 then executes by timers that need to fire on its own. This approach can 31 be many times faster than using timers and guarantees that timers scheduled 32 to execute at the same time generally will do so, keeping animations and 33 other operations in sync. 34 35 ## Scheduling a Timer 36 37 To schedule a basic timer, you can simply call SC.Timer.schedule() with 38 a target and action you wish to have invoked: 39 40 var timer = SC.Timer.schedule({ 41 target: myObject, action: 'timerFired', interval: 100 42 }); 43 44 When this timer fires, it will call the timerFired() method on myObject. 45 46 In addition to calling a method on a particular object, you can also use 47 a timer to execute a variety of other types of code: 48 49 - If you include an action name, but not a target object, then the action will be passed down the responder chain. 50 - If you include a property path for the action property (e.g. 'MyApp.someController.someMethod'), then the method you name will be executed. 51 - If you include a function in the action property, then the function will be executed. If you also include a target object, the function will be called with this set to the target object. 52 53 In general these properties are read-only. Changing an interval, target, 54 or action after creating a timer will have an unknown effect. 55 56 ## Scheduling Repeating Timers 57 58 In addition to scheduling one time timers, you can also schedule timers to 59 execute periodically until some termination date. You make a timer 60 repeating by adding the repeats: YES property: 61 62 var timer = SC.Timer.schedule({ 63 target: myObject, 64 action: 'updateAnimation', 65 interval: 100, 66 repeats: YES, 67 until: Time.now() + 1000 68 }) ; 69 70 The above example will execute the myObject.updateAnimation() every 100msec 71 for 1 second from the current time. 72 73 If you want a timer to repeat without expiration, you can simply omit the 74 until: property. The timer will then repeat until you invalidate it. 75 76 ## Pausing and Invalidating Timers 77 78 If you have created a timer but you no longer want it to execute, you can 79 call the invalidate() method on it. This will remove the timer from the 80 run loop and clear certain properties so that it will not run again. 81 82 You can use the invalidate() method on both repeating and one-time timers. 83 84 If you do not want to invalidate a timer completely but you just want to 85 stop the timer from execution temporarily, you can alternatively set the 86 isPaused property to YES: 87 88 timer.set('isPaused', YES) ; 89 // Perform some critical function; timer will not execute 90 timer.set('isPaused', NO) ; 91 92 When a timer is paused, it will be scheduled and will fire like normal, 93 but it will not actually execute the action method when it fires. For a 94 one time timer, this means that if you have the timer paused when it fires, 95 it may never actually execute the action method. For repeating timers, 96 this means the timer will remain scheduled but simply will not execute its 97 action while the timer is paused. 98 99 ## Firing Timers 100 101 If you need a timer to execute immediately, you can always call the fire() 102 method yourself. This will execute the timer action, if the timer is not 103 paused. For a one time timer, it will also invalidate the timer and remove 104 it from the run loop. Repeating timers can be fired anytime and it will 105 not interrupt their regular scheduled times. 106 107 108 @extends SC.Object 109 @author Charles Jolley 110 @version 1.0 111 @since version 1.0 112 */ 113 SC.Timer = SC.Object.extend( 114 /** @scope SC.Timer.prototype */ { 115 116 /** 117 The target object whose method will be invoked when the time fires. 118 119 You can set either a target/action property or you can pass a specific 120 method. 121 122 @type {Object} 123 @field 124 */ 125 target: null, 126 127 /** 128 The action to execute. 129 130 The action can be a method name, a property path, or a function. If you 131 pass a method name, it will be invoked on the target object or it will 132 be called up the responder chain if target is null. If you pass a 133 property path and it resolves to a function then the function will be 134 called. If you pass a function instead, then the function will be 135 called in the context of the target object. 136 137 @type {String, Function} 138 */ 139 action: null, 140 141 /** 142 Set if the timer should be created from a memory pool. Normally you will 143 want to leave this set, but if you plan to use bindings or observers with 144 this timer, then you must set isPooled to NO to avoid reusing your timer. 145 146 @type Boolean 147 */ 148 isPooled: NO, 149 150 /** 151 The time interval in milliseconds. 152 153 You generally set this when you create the timer. If you do not set it 154 then the timer will fire as soon as possible in the next run loop. 155 156 @type {Number} 157 */ 158 interval: 0, 159 160 /** 161 Timer start date offset. 162 163 The start date determines when the timer will be scheduled. The first 164 time the timer fires will be interval milliseconds after the start 165 date. 166 167 Generally you will not set this property yourself. Instead it will be 168 set automatically to the current run loop start date when you schedule 169 the timer. This ensures that all timers scheduled in the same run loop 170 cycle will execute in the sync with one another. 171 172 The value of this property is an offset like what you get if you call 173 Date.now(). 174 175 @type {Number} 176 */ 177 startTime: null, 178 179 /** 180 YES if you want the timer to execute repeatedly. 181 182 @type {Boolean} 183 */ 184 repeats: NO, 185 186 /** 187 Last date when the timer will execute. 188 189 If you have set repeats to YES, then you can also set this property to 190 have the timer automatically stop executing past a certain date. 191 192 This property should contain an offset value like startOffset. However if 193 you set it to a Date object on create, it will be converted to an offset 194 for you. 195 196 If this property is null, then the timer will continue to repeat until you 197 call invalidate(). 198 199 @type {Date, Number} 200 */ 201 until: null, 202 203 /** 204 Set to YES to pause the timer. 205 206 Pausing a timer does not remove it from the run loop, but it will 207 temporarily suspend it from firing. You should use this property if 208 you will want the timer to fire again the future, but you want to prevent 209 it from firing temporarily. 210 211 If you are done with a timer, you should call invalidate() instead of 212 setting this property. 213 214 @type {Boolean} 215 */ 216 isPaused: NO, 217 218 /** 219 YES onces the timer has been scheduled for the first time. 220 */ 221 isScheduled: NO, 222 223 /** 224 YES if the timer can still execute. 225 226 This read only property will return YES as long as the timer may possibly 227 fire again in the future. Once a timer has become invalid, it cannot 228 become valid again. 229 230 @field 231 @type {Boolean} 232 */ 233 isValid: YES, 234 235 /** 236 Set to the current time when the timer last fired. Used to find the 237 next 'frame' to execute. 238 */ 239 lastFireTime: 0, 240 241 /** 242 Computed property returns the next time the timer should fire. This 243 property resets each time the timer fires. Returns -1 if the timer 244 cannot fire again. 245 246 @type Time 247 */ 248 fireTime: function() { 249 if (!this.get('isValid')) { return -1 ; } // not valid - can't fire 250 251 // can't fire w/o startTime (set when schedule() is called). 252 var start = this.get('startTime'); 253 if (!start || start === 0) { return -1; } 254 255 // fire interval after start. 256 var interval = this.get('interval'), last = this.get('lastFireTime'); 257 if (last < start) { last = start; } // first time to fire 258 259 // find the next time to fire 260 var next ; 261 if (this.get('repeats')) { 262 if (interval === 0) { // 0 means fire as fast as possible. 263 next = last ; // time to fire immediately! 264 265 // find the next full interval after start from last fire time. 266 } else { 267 next = start + (Math.floor((last - start) / interval)+1)*interval; 268 } 269 270 // otherwise, fire only once interval after start 271 } else { 272 next = start + interval ; 273 } 274 275 // can never have a fireTime after until 276 var until = this.get('until'); 277 if (until && until>0 && next>until) next = until; 278 279 return next ; 280 }.property('interval', 'startTime', 'repeats', 'until', 'isValid', 'lastFireTime').cacheable(), 281 282 /** 283 Schedules the timer to execute in the runloop. 284 285 This method is called automatically if you create the timer using the 286 schedule() class method. If you create the timer manually, you will 287 need to call this method yourself for the timer to execute. 288 289 @returns {SC.Timer} The receiver 290 */ 291 schedule: function() { 292 if (!this.get('isValid')) return this; // nothing to do 293 294 this.beginPropertyChanges(); 295 296 // if start time was not set explicitly when the timer was created, 297 // get it from the run loop. This way timer scheduling will always 298 // occur in sync. 299 if (!this.startTime) this.set('startTime', SC.RunLoop.currentRunLoop.get('startTime')) ; 300 301 // now schedule the timer if the last fire time was < the next valid 302 // fire time. The first time lastFireTime is 0, so this will always go. 303 var next = this.get('fireTime'), last = this.get('lastFireTime'); 304 if (next >= last) { 305 this.set('isScheduled', YES); 306 SC.RunLoop.currentRunLoop.scheduleTimer(this, next); 307 } 308 309 this.endPropertyChanges() ; 310 311 return this ; 312 }, 313 /** 314 Invalidates the timer so that it will not execute again. If a timer has 315 been scheduled, it will be removed from the run loop immediately. 316 317 @returns {SC.Timer} The receiver 318 */ 319 invalidate: function() { 320 this.beginPropertyChanges(); 321 this.set('isValid', NO); 322 323 var runLoop = SC.RunLoop.currentRunLoop; 324 if(runLoop) runLoop.cancelTimer(this); 325 326 this.action = this.target = null ; // avoid memory leaks 327 this.endPropertyChanges(); 328 329 // return to pool... 330 if (this.get('isPooled')) SC.Timer.returnTimerToPool(this); 331 return this ; 332 }, 333 334 /** 335 Immediately fires the timer. 336 337 If the timer is not-repeating, it will be invalidated. If it is repeating 338 you can call this method without interrupting its normal schedule. 339 340 @returns {void} 341 */ 342 fire: function() { 343 344 // this will cause the fireTime to recompute 345 var last = Date.now(); 346 this.set('lastFireTime', last); 347 348 var next = this.get('fireTime'); 349 350 // now perform the fire action unless paused. 351 if (!this.get('isPaused')) this.performAction() ; 352 353 // reschedule the timer if needed... 354 if (next > last) { 355 this.schedule(); 356 } else { 357 this.invalidate(); 358 } 359 }, 360 361 /** 362 Actually fires the action. You can override this method if you need 363 to change how the timer fires its action. 364 */ 365 performAction: function() { 366 var typeOfAction = SC.typeOf(this.action); 367 368 // if the action is a function, just try to call it. 369 if (typeOfAction == SC.T_FUNCTION) { 370 this.action.call((this.target || this), this) ; 371 372 // otherwise, action should be a string. If it has a period, treat it 373 // like a property path. 374 } else if (typeOfAction === SC.T_STRING) { 375 if (this.action.indexOf('.') >= 0) { 376 var path = this.action.split('.') ; 377 var property = path.pop() ; 378 379 var target = SC.objectForPropertyPath(path, window) ; 380 var action = target.get ? target.get(property) : target[property]; 381 if (action && SC.typeOf(action) == SC.T_FUNCTION) { 382 action.call(target, this) ; 383 } else { 384 throw new Error('%@: Timer could not find a function at %@'.fmt(this, this.action)); 385 } 386 387 // otherwise, try to execute action direction on target or send down 388 // responder chain. 389 } else { 390 SC.RootResponder.responder.sendAction(this.action, this.target, this); 391 } 392 } 393 }, 394 395 init: function() { 396 sc_super(); 397 398 // convert startTime and until to times if they are dates. 399 if (this.startTime instanceof Date) { 400 this.startTime = this.startTime.getTime() ; 401 } 402 403 if (this.until instanceof Date) { 404 this.until = this.until.getTime() ; 405 } 406 }, 407 408 /** @private - Default values to reset reused timers to. */ 409 RESET_DEFAULTS: { 410 target: null, action: null, 411 isPooled: NO, isPaused: NO, isScheduled: NO, isValid: YES, 412 interval: 0, repeats: NO, until: null, 413 startTime: null, lastFireTime: 0 414 }, 415 416 /** 417 Resets the timer settings with the new settings. This is the method 418 called by the Timer pool when a timer is reused. You will not normally 419 call this method yourself, though you could override it if you need to 420 reset additional properties when a timer is reused. 421 422 @params {Hash} props properties to copy over 423 @returns {SC.Timer} receiver 424 */ 425 reset: function(props) { 426 if (!props) props = SC.EMPTY_HASH; 427 428 // note: we copy these properties manually just to make them fast. we 429 // don't expect you to use observers on a timer object if you are using 430 // pooling anyway so this won't matter. Still notify of property change 431 // on fireTime to clear its cache. 432 this.propertyWillChange('fireTime'); 433 var defaults = this.RESET_DEFAULTS ; 434 for(var key in defaults) { 435 if (!defaults.hasOwnProperty(key)) continue ; 436 this[key] = SC.none(props[key]) ? defaults[key] : props[key]; 437 } 438 this.propertyDidChange('fireTime'); 439 return this ; 440 }, 441 442 // .......................................................... 443 // TIMER QUEUE SUPPORT 444 // 445 446 /** @private - removes the timer from its current timerQueue if needed. 447 return value is the new "root" timer. 448 */ 449 removeFromTimerQueue: function(timerQueueRoot) { 450 var prev = this._timerQueuePrevious, next = this._timerQueueNext ; 451 452 if (!prev && !next && timerQueueRoot !== this) return timerQueueRoot ; // not in a queue... 453 454 // else, patch up to remove... 455 if (prev) prev._timerQueueNext = next ; 456 if (next) next._timerQueuePrevious = prev ; 457 this._timerQueuePrevious = this._timerQueueNext = null ; 458 return (timerQueueRoot === this) ? next : timerQueueRoot ; 459 }, 460 461 /** @private - schedules the timer in the queue based on the runtime. */ 462 scheduleInTimerQueue: function(timerQueueRoot, runTime) { 463 this._timerQueueRunTime = runTime ; 464 465 // find the place to begin 466 var beforeNode = timerQueueRoot; 467 var afterNode = null ; 468 while(beforeNode && beforeNode._timerQueueRunTime < runTime) { 469 afterNode = beforeNode ; 470 beforeNode = beforeNode._timerQueueNext; 471 } 472 473 if (afterNode) { 474 afterNode._timerQueueNext = this ; 475 this._timerQueuePrevious = afterNode ; 476 } 477 478 if (beforeNode) { 479 beforeNode._timerQueuePrevious = this ; 480 this._timerQueueNext = beforeNode ; 481 } 482 483 // I am the new root if beforeNode === root 484 return (beforeNode === timerQueueRoot) ? this : timerQueueRoot ; 485 }, 486 487 /** @private 488 adds the receiver to the passed array of expired timers based on the 489 current time and then recursively calls the next timer. Returns the 490 first timer that is not expired. This is faster than iterating through 491 the timers because it does some faster cleanup of the nodes. 492 */ 493 collectExpiredTimers: function(timers, now) { 494 if (this._timerQueueRunTime > now) return this ; // not expired! 495 timers.push(this); // add to queue.. fixup next. assume we are root. 496 var next = this._timerQueueNext ; 497 this._timerQueueNext = null; 498 if (next) next._timerQueuePrevious = null; 499 return next ? next.collectExpiredTimers(timers, now) : null; 500 } 501 502 }) ; 503 504 /** @scope SC.Timer */ 505 506 /* 507 Created a new timer with the passed properties and schedules it to 508 execute. This is the same as calling SC.Time.create({ props }).schedule(). 509 510 Note that unless you explicitly set isPooled to NO, this timer will be 511 pulled from a shared memory pool of timers. You cannot using bindings or 512 observers on these timers as they may be reused for future timers at any 513 time. 514 515 @params {Hash} props Any properties you want to set on the timer. 516 @returns {SC.Timer} new timer instance. 517 */ 518 SC.Timer.schedule = function(props) { 519 // get the timer. 520 var timer ; 521 if (!props || SC.none(props.isPooled) || props.isPooled) { 522 timer = this.timerFromPool(props); 523 } else timer = this.create(props); 524 return timer.schedule(); 525 } ; 526 527 /** 528 Returns a new timer from the timer pool, copying the passed properties onto 529 the timer instance. If the timer pool is currently empty, this will return 530 a new instance. 531 */ 532 SC.Timer.timerFromPool = function(props) { 533 var timers = this._timerPool; 534 if (!timers) timers = this._timerPool = [] ; 535 var timer = timers.pop(); 536 if (!timer) timer = this.create(); 537 return timer.reset(props) ; 538 }; 539 540 /** 541 Returns a timer instance to the timer pool for later use. This is done 542 automatically when a timer is invalidated if isPooled is YES. 543 */ 544 SC.Timer.returnTimerToPool = function(timer) { 545 if (!this._timerPool) this._timerPool = []; 546 547 this._timerPool.push(timer); 548 return this ; 549 }; 550 551 552