blossom
Version:
Modern, Cross-Platform Application Framework
557 lines (436 loc) • 18.9 kB
JavaScript
// ========================================================================
// SproutCore -- JavaScript Application Framework
// Copyright ©2006-2011, Strobe Inc. and contributors.
// Portions copyright ©2008 Apple Inc. All rights reserved.
// ========================================================================
sc_require('system/object');
var SC = global.SC; // Required to allow foundation to be re-namespaced as BT
// when loaded by the buildtools.
/**
@class
A Timer executes a method after a defined period of time. Timers are
significantly more efficient than using setTimeout() or setInterval()
because they are cooperatively scheduled using the run loop. Timers are
also gauranteed to fire at the same time, making it far easier to keep
multiple timers in sync.
h2. Overview
Timers were created for SproutCore as a way to efficiently defer execution
of code fragments for use in Animations, event handling, and other tasks.
Browsers are typically fairly inconsistant about when they will fire a
timeout or interval based on what the browser is currently doing. Timeouts
and intervals are also fairly expensive for a browser to execute, which
means if you schedule a large number of them it can quickly slow down the
browser considerably.
Timers, on the other handle, are scheduled cooperatively using the
SC.RunLoop, which uses exactly one timeout to fire itself when needed and
then executes by timers that need to fire on its own. This approach can
be many timers faster than using timers and gaurantees that timers scheduled
to execute at the same time generally will do so, keeping animations and
other operations in sync.
h2. Scheduling a Timer
To schedule a basic timer, you can simply call SC.Timer.schedule() with
a target and action you wish to have invoked:
{{{
var timer = SC.Timer.schedule({
target: myObject, action: 'timerFired', interval: 100
});
}}}
When this timer fires, it will call the timerFired() method on myObject.
In addition to calling a method on a particular object, you can also use
a timer to execute a variety of other types of code:
- If you include an action name, but not a target object, then the action will be passed down the responder chain.
- If you include a property path for the action property (e.g. 'MyApp.someController.someMethod'), then the method you name will be executed.
- 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.
In general these properties are read-only. Changing an interval, target,
or action after creating a timer will have an unknown effect.
h2. Scheduling Repeating Timers
In addition to scheduling one time timers, you can also schedule timers to
execute periodically until some termination date. You make a timer
repeating by adding the repeats: true property:
{{{
var timer = SC.Timer.schedule({
target: myObject,
action: 'updateAnimation',
interval: 100,
repeats: true,
until: Time.now() + 1000
}) ;
}}}
The above example will execute the myObject.updateAnimation() every 100msec
for 1 second from the current time.
If you want a timer to repeat without expiration, you can simply omit the
until: property. The timer will then repeat until you invalidate it.
h2. Pausing and Invalidating Timers
If you have created a timer but you no longer want it to execute, you can
call the invalidate() method on it. This will remove the timer from the
run loop and clear certain properties so that it will not run again.
You can use the invalidate() method on both repeating and one-time timers.
If you do not want to invalidate a timer completely but you just want to
stop the timer from execution temporarily, you can alternatively set the
isPaused property to true:
{{{
timer.set('isPaused', true) ;
// Perform some critical function; timer will not execute
timer.set('isPaused', false) ;
}}}
When a timer is paused, it will be scheduled and will fire like normal,
but it will not actually execute the action method when it fires. For a
one time timer, this means that if you have the timer paused when it fires,
it may never actually execute the action method. For repeating timers,
this means the timer will remain scheduled but simply will not execute its
action while the timer is paused.
h2. Firing Timers
If you need a timer to execute immediately, you can always call the fire()
method yourself. This will execute the timer action, if the timer is not
paused. For a one time timer, it will also invalidate the timer and remove
it from the run loop. Repeating timers can be fired anytime and it will
not interrupt their regular scheduled times.
@extends SC.Object
@author Charles Jolley
@version 1.0
@since version 1.0
*/
SC.Timer = SC.Object.extend(
/** @scope SC.Timer.prototype */ {
/**
The target object whose method will be invoked when the time fires.
You can set either a target/action property or you can pass a specific
method.
@type {Object}
@field
*/
target: null,
/**
The action to execute.
The action can be a method name, a property path, or a function. If you
pass a method name, it will be invoked on the target object or it will
be called up the responder chain if target is null. If you pass a
property path and it resolves to a function then the function will be
called. If you pass a function instead, then the function will be
called in the context of the target object.
@type {String, Function}
*/
action: null,
/**
Set if the timer should be created from a memory pool. Normally you will
want to leave this set, but if you plan to use bindings or observers with
this timer, then you must set isPooled to false to avoid reusing your timer.
@property {Boolean}
*/
isPooled: false,
/**
The time interval in milliseconds.
You generally set this when you create the timer. If you do not set it
then the timer will fire as soon as possible in the next run loop.
@type {Number}
*/
interval: 0,
/**
Timer start date offset.
The start date determines when the timer will be scheduled. The first
time the timer fires will be interval milliseconds after the start
date.
Generally you will not set this property yourself. Instead it will be
set automatically to the current run loop start date when you schedule
the timer. This ensures that all timers scheduled in the same run loop
cycle will execute in the sync with one another.
The value of this property is an offset like what you get if you call
Date.now().
@type {Number}
*/
startTime: null,
/**
true if you want the timer to execute repeatedly.
@type {Boolean}
*/
repeats: false,
/**
Last date when the timer will execute.
If you have set repeats to true, then you can also set this property to
have the timer automatically stop executing past a certain date.
This property should contain an offset value like startOffset. However if
you set it to a Date object on create, it will be converted to an offset
for you.
If this property is null, then the timer will continue to repeat until you
call invalidate().
@type {Date, Number}
*/
until: null,
/**
Set to true to pause the timer.
Pausing a timer does not remove it from the run loop, but it will
temporarily suspend it from firing. You should use this property if
you will want the timer to fire again the future, but you want to prevent
it from firing temporarily.
If you are done with a timer, you should call invalidate() instead of
setting this property.
@type {Boolean}
*/
isPaused: false,
/**
true onces the timer has been scheduled for the first time.
*/
isScheduled: false,
/**
true if the timer can still execute.
This read only property will return true as long as the timer may possibly
fire again in the future. Once a timer has become invalid, it cannot
become valid again.
@field
@type {Boolean}
*/
isValid: true,
/**
Set to the current time when the timer last fired. Used to find the
next 'frame' to execute.
*/
lastFireTime: 0,
/**
Computed property returns the next time the timer should fire. This
property resets each time the timer fires. Returns -1 if the timer
cannot fire again.
@property {Time}
*/
fireTime: function() {
if (!this.get('isValid')) return -1 ; // not valid - can't fire
// can't fire w/o startTime (set when schedule() is called).
var start = this.get('startTime');
if (!start || start === 0) return -1;
// fire interval after start.
var interval = this.get('interval'), last = this.get('lastFireTime');
if (last < start) last = start; // first time to fire
// find the next time to fire
var next ;
if (this.get('repeats')) {
if (interval === 0) { // 0 means fire as fast as possible.
next = last ; // time to fire immediately!
// find the next full interval after start from last fire time.
} else {
next = start + (Math.floor((last - start) / interval)+1)*interval;
}
// otherwise, fire only once interval after start
} else next = start + interval ;
// can never have a fireTime after until
var until = this.get('until');
if (until && until>0 && next>until) next = until;
return next ;
}.property('interval', 'startTime', 'repeats', 'until', 'isValid', 'lastFireTime').cacheable(),
/**
Schedules the timer to execute in the runloop.
This method is called automatically if you create the timer using the
schedule() class method. If you create the timer manually, you will
need to call this method yourself for the timer to execute.
@returns {SC.Timer} The receiver
*/
schedule: function() {
if (!this.get('isValid')) return this; // nothing to do
this.beginPropertyChanges();
// if start time was not set explicitly when the timer was created,
// get it from the run loop. This way timer scheduling will always
// occur in sync.
if (!this.startTime) this.set('startTime', SC.RunLoop.currentRunLoop.get('startTime')) ;
// now schedule the timer if the last fire time was < the next valid
// fire time. The first time lastFireTime is 0, so this will always go.
var next = this.get('fireTime'), last = this.get('lastFireTime');
if (next >= last) {
this.set('isScheduled', true);
SC.RunLoop.currentRunLoop.scheduleTimer(this, next);
}
this.endPropertyChanges() ;
return this ;
},
/**
Invalidates the timer so that it will not execute again. If a timer has
been scheduled, it will be removed from the run loop immediately.
@returns {SC.Timer} The receiver
*/
invalidate: function() {
this.beginPropertyChanges();
this.set('isValid', false);
SC.RunLoop.currentRunLoop.cancelTimer(this);
this.action = this.target = null ; // avoid memory leaks
this.endPropertyChanges();
// return to pool...
if (this.get('isPooled')) SC.Timer.returnTimerToPool(this);
return this ;
},
/**
Immediately fires the timer.
If the timer is not-repeating, it will be invalidated. If it is repeating
you can call this method without interrupting its normal schedule.
@returns {void}
*/
fire: function() {
// this will cause the fireTime to recompute
var last = Date.now();
this.set('lastFireTime', last);
var next = this.get('fireTime');
// now perform the fire action unless paused.
if (!this.get('isPaused')) this.performAction() ;
// reschedule the timer if needed...
if (next > last) {
this.schedule();
} else {
this.invalidate();
}
},
/**
Actually fires the action. You can override this method if you need
to change how the timer fires its action.
*/
performAction: function() {
var typeOfAction = SC.typeOf(this.action);
// if the action is a function, just try to call it.
if (typeOfAction == SC.T_FUNCTION) {
this.action.call((this.target || this), this) ;
// otherwise, action should be a string. If it has a period, treat it
// like a property path.
} else if (typeOfAction === SC.T_STRING) {
if (this.action.indexOf('.') >= 0) {
var path = this.action.split('.') ;
var property = path.pop() ;
var target = SC.objectForPropertyPath(path, window) ;
var action = target.get ? target.get(property) : target[property];
if (action && SC.typeOf(action) == SC.T_FUNCTION) {
action.call(target, this) ;
} else {
throw '%@: Timer could not find a function at %@'.fmt(this, this.action) ;
}
// otherwise, try to execute action direction on target or send down
// responder chain.
} else {
SC.app.sendAction(this.action, this.target, this);
}
}
},
init: function() {
arguments.callee.base.apply(this, arguments);
// convert startTime and until to times if they are dates.
if (this.startTime instanceof Date) {
this.startTime = this.startTime.getTime() ;
}
if (this.until instanceof Date) {
this.until = this.until.getTime() ;
}
},
/** @private - Default values to reset reused timers to. */
RESET_DEFAULTS: {
target: null, action: null,
isPooled: false, isPaused: false, isScheduled: false, isValid: true,
interval: 0, repeats: false, until: null,
startTime: null, lastFireTime: 0
},
/**
Resets the timer settings with the new settings. This is the method
called by the Timer pool when a timer is reused. You will not normally
call this method yourself, though you could override it if you need to
reset additonal properties when a timer is reused.
@params {Hash} props properties to copy over
@returns {SC.Timer} receiver
*/
reset: function(props) {
if (!props) props = SC.EMPTY_HASH;
// note: we copy these properties manually just to make them fast. we
// don't expect you to use observers on a timer object if you are using
// pooling anyway so this won't matter. Still notify of property change
// on fireTime to clear its cache.
this.propertyWillChange('fireTime');
var defaults = this.RESET_DEFAULTS ;
for(var key in defaults) {
if (!defaults.hasOwnProperty(key)) continue ;
this[key] = SC.none(props[key]) ? defaults[key] : props[key];
}
this.propertyDidChange('fireTime');
return this ;
},
// ..........................................................
// TIMER QUEUE SUPPORT
//
/** @private - removes the timer from its current timerQueue if needed.
return value is the new "root" timer.
*/
removeFromTimerQueue: function(timerQueueRoot) {
var prev = this._timerQueuePrevious, next = this._timerQueueNext ;
if (!prev && !next && timerQueueRoot !== this) return timerQueueRoot ; // not in a queue...
// else, patch up to remove...
if (prev) prev._timerQueueNext = next ;
if (next) next._timerQueuePrevious = prev ;
this._timerQueuePrevious = this._timerQueueNext = null ;
return (timerQueueRoot === this) ? next : timerQueueRoot ;
},
/** @private - schedules the timer in the queue based on the runtime. */
scheduleInTimerQueue: function(timerQueueRoot, runTime) {
this._timerQueueRunTime = runTime ;
// find the place to begin
var beforeNode = timerQueueRoot;
var afterNode = null ;
while(beforeNode && beforeNode._timerQueueRunTime < runTime) {
afterNode = beforeNode ;
beforeNode = beforeNode._timerQueueNext;
}
if (afterNode) {
afterNode._timerQueueNext = this ;
this._timerQueuePrevious = afterNode ;
}
if (beforeNode) {
beforeNode._timerQueuePrevious = this ;
this._timerQueueNext = beforeNode ;
}
// I am the new root if beforeNode === root
return (beforeNode === timerQueueRoot) ? this : timerQueueRoot ;
},
/** @private
adds the receiver to the passed array of expired timers based on the
current time and then recursively calls the next timer. Returns the
first timer that is not expired. This is faster than iterating through
the timers because it does some faster cleanup of the nodes.
*/
collectExpiredTimers: function(timers, now) {
if (this._timerQueueRunTime > now) return this ; // not expired!
timers.push(this); // add to queue.. fixup next. assume we are root.
var next = this._timerQueueNext ;
this._timerQueueNext = null;
if (next) next._timerQueuePrevious = null;
return next ? next.collectExpiredTimers(timers, now) : null;
}
}) ;
/** @scope SC.Timer */
/*
Created a new timer with the passed properties and schedules it to
execute. This is the same as calling SC.Time.create({ props }).schedule().
Note that unless you explicitly set isPooled to false, this timer will be
pulled from a shared memory pool of timers. You cannot using bindings or
observers on these timers as they may be reused for future timers at any
time.
@params {Hash} props Any properties you want to set on the timer.
@returns {SC.Timer} new timer instance.
*/
SC.Timer.schedule = function(props) {
// get the timer.
var timer ;
if (!props || SC.none(props.isPooled) || props.isPooled) {
timer = this.timerFromPool(props);
} else timer = this.create(props);
return timer.schedule();
} ;
/**
Returns a new timer from the timer pool, copying the passed properties onto
the timer instance. If the timer pool is currently empty, this will return
a new instance.
*/
SC.Timer.timerFromPool = function(props) {
var timers = this._timerPool;
if (!timers) timers = this._timerPool = [] ;
var timer = timers.pop();
if (!timer) timer = this.create();
return timer.reset(props) ;
};
/**
Returns a timer instance to the timer pool for later use. This is done
automatically when a timer is invalidated if isPooled is true.
*/
SC.Timer.returnTimerToPool = function(timer) {
if (!this._timerPool) this._timerPool = [];
this._timerPool.push(timer);
return this ;
};