master
1
2 'use strict';
3
4 /***
5 * @package Function
6 * @dependency core
7 * @description Lazy, throttled, and memoized functions, delayed functions and handling of timers, argument currying.
8 *
9 ***/
10
11 function setDelay(fn, ms, after, scope, args) {
12 // Delay of infinity is never called of course...
13 if(ms === Infinity) return;
14 if(!fn.timers) fn.timers = [];
15 if(!isNumber(ms)) ms = 1;
16 // This is a workaround for <= IE8, which apparently has the
17 // ability to call timeouts in the queue on the same tick (ms?)
18 // even if functionally they have already been cleared.
19 fn._canceled = false;
20 fn.timers.push(setTimeout(function(){
21 if(!fn._canceled) {
22 after.apply(scope, args || []);
23 }
24 }, ms));
25 }
26
27 extend(Function, true, true, {
28
29 /***
30 * @method lazy([ms] = 1, [immediate] = false, [limit] = Infinity)
31 * @returns Function
32 * @short Creates a lazy function that, when called repeatedly, will queue execution and wait [ms] milliseconds to execute.
33 * @extra If [immediate] is %true%, first execution will happen immediately, then lock. If [limit] is a fininte number, calls past [limit] will be ignored while execution is locked. Compare this to %throttle%, which will execute only once per [ms] milliseconds. Note that [ms] can also be a fraction. Calling %cancel% on a lazy function will clear the entire queue. For more see @functions.
34 * @example
35 *
36 * (function() {
37 * // Executes immediately.
38 * }).lazy()();
39 * (3).times(function() {
40 * // Executes 3 times, with each execution 20ms later than the last.
41 * }.lazy(20));
42 * (100).times(function() {
43 * // Executes 50 times, with each execution 20ms later than the last.
44 * }.lazy(20, false, 50));
45 *
46 ***/
47 'lazy': function(ms, immediate, limit) {
48 var fn = this, queue = [], locked = false, execute, rounded, perExecution, result;
49 ms = ms || 1;
50 limit = limit || Infinity;
51 rounded = ceil(ms);
52 perExecution = round(rounded / ms) || 1;
53 execute = function() {
54 var queueLength = queue.length, maxPerRound;
55 if(queueLength == 0) return;
56 // Allow fractions of a millisecond by calling
57 // multiple times per actual timeout execution
58 maxPerRound = max(queueLength - perExecution, 0);
59 while(queueLength > maxPerRound) {
60 // Getting uber-meta here...
61 result = Function.prototype.apply.apply(fn, queue.shift());
62 queueLength--;
63 }
64 setDelay(lazy, rounded, function() {
65 locked = false;
66 execute();
67 });
68 }
69 function lazy() {
70 // If the execution has locked and it's immediate, then
71 // allow 1 less in the queue as 1 call has already taken place.
72 if(queue.length < limit - (locked && immediate ? 1 : 0)) {
73 queue.push([this, arguments]);
74 }
75 if(!locked) {
76 locked = true;
77 if(immediate) {
78 execute();
79 } else {
80 setDelay(lazy, rounded, execute);
81 }
82 }
83 // Return the memoized result
84 return result;
85 }
86 return lazy;
87 },
88
89 /***
90 * @method throttle([ms] = 1)
91 * @returns Function
92 * @short Creates a "throttled" version of the function that will only be executed once per <ms> milliseconds.
93 * @extra This is functionally equivalent to calling %lazy% with a [limit] of %1% and [immediate] as %true%. %throttle% is appropriate when you want to make sure a function is only executed at most once for a given duration. For more see @functions.
94 * @example
95 *
96 * (3).times(function() {
97 * // called only once. will wait 50ms until it responds again
98 * }.throttle(50));
99 *
100 ***/
101 'throttle': function(ms) {
102 return this.lazy(ms, true, 1);
103 },
104
105 /***
106 * @method debounce([ms] = 1)
107 * @returns Function
108 * @short Creates a "debounced" function that postpones its execution until after <ms> milliseconds have passed.
109 * @extra This method is useful to execute a function after things have "settled down". A good example of this is when a user tabs quickly through form fields, execution of a heavy operation should happen after a few milliseconds when they have "settled" on a field. For more see @functions.
110 * @example
111 *
112 * var fn = (function(arg1) {
113 * // called once 50ms later
114 * }).debounce(50); fn() fn() fn();
115 *
116 ***/
117 'debounce': function(ms) {
118 var fn = this;
119 function debounced() {
120 debounced.cancel();
121 setDelay(debounced, ms, fn, this, arguments);
122 };
123 return debounced;
124 },
125
126 /***
127 * @method delay([ms] = 1, [arg1], ...)
128 * @returns Function
129 * @short Executes the function after <ms> milliseconds.
130 * @extra Returns a reference to itself. %delay% is also a way to execute non-blocking operations that will wait until the CPU is free. Delayed functions can be canceled using the %cancel% method. Can also curry arguments passed in after <ms>.
131 * @example
132 *
133 * (function(arg1) {
134 * // called 1s later
135 * }).delay(1000, 'arg1');
136 *
137 ***/
138 'delay': function(ms) {
139 var fn = this;
140 var args = multiArgs(arguments, null, 1);
141 setDelay(fn, ms, fn, fn, args);
142 return fn;
143 },
144
145 /***
146 * @method every([ms] = 1, [arg1], ...)
147 * @returns Function
148 * @short Executes the function every <ms> milliseconds.
149 * @extra Returns a reference to itself. Repeating functions with %every% can be canceled using the %cancel% method. Can also curry arguments passed in after <ms>.
150 * @example
151 *
152 * (function(arg1) {
153 * // called every 1s
154 * }).every(1000, 'arg1');
155 *
156 ***/
157 'every': function(ms) {
158 var fn = this, args = arguments;
159 args = args.length > 1 ? multiArgs(args, null, 1) : [];
160 function execute () {
161 fn.apply(fn, args);
162 setDelay(fn, ms, execute);
163 }
164 setDelay(fn, ms, execute);
165 return fn;
166 },
167
168 /***
169 * @method cancel()
170 * @returns Function
171 * @short Cancels a delayed function scheduled to be run.
172 * @extra %delay%, %lazy%, %throttle%, and %debounce% can all set delays.
173 * @example
174 *
175 * (function() {
176 * alert('hay'); // Never called
177 * }).delay(500).cancel();
178 *
179 ***/
180 'cancel': function() {
181 var timers = this.timers, timer;
182 if(isArray(timers)) {
183 while(timer = timers.shift()) {
184 clearTimeout(timer);
185 }
186 }
187 this._canceled = true;
188 return this;
189 },
190
191 /***
192 * @method after([num] = 1)
193 * @returns Function
194 * @short Creates a function that will execute after [num] calls.
195 * @extra %after% is useful for running a final callback after a series of asynchronous operations, when the order in which the operations will complete is unknown.
196 * @example
197 *
198 * var fn = (function() {
199 * // Will be executed once only
200 * }).after(3); fn(); fn(); fn();
201 *
202 ***/
203 'after': function(num) {
204 var fn = this, counter = 0, storedArguments = [];
205 if(!isNumber(num)) {
206 num = 1;
207 } else if(num === 0) {
208 fn.call();
209 return fn;
210 }
211 return function() {
212 var ret;
213 storedArguments.push(multiArgs(arguments));
214 counter++;
215 if(counter == num) {
216 ret = fn.call(this, storedArguments);
217 counter = 0;
218 storedArguments = [];
219 return ret;
220 }
221 }
222 },
223
224 /***
225 * @method once()
226 * @returns Function
227 * @short Creates a function that will execute only once and store the result.
228 * @extra %once% is useful for creating functions that will cache the result of an expensive operation and use it on subsequent calls. Also it can be useful for creating initialization functions that only need to be run once.
229 * @example
230 *
231 * var fn = (function() {
232 * // Will be executed once only
233 * }).once(); fn(); fn(); fn();
234 *
235 ***/
236 'once': function() {
237 return this.throttle(Infinity, true);
238 },
239
240 /***
241 * @method fill(<arg1>, <arg2>, ...)
242 * @returns Function
243 * @short Returns a new version of the function which when called will have some of its arguments pre-emptively filled in, also known as "currying".
244 * @extra Arguments passed to a "filled" function are generally appended to the curried arguments. However, if %undefined% is passed as any of the arguments to %fill%, it will be replaced, when the "filled" function is executed. This allows currying of arguments even when they occur toward the end of an argument list (the example demonstrates this much more clearly).
245 * @example
246 *
247 * var delayOneSecond = setTimeout.fill(undefined, 1000);
248 * delayOneSecond(function() {
249 * // Will be executed 1s later
250 * });
251 *
252 ***/
253 'fill': function() {
254 var fn = this, curried = multiArgs(arguments);
255 return function() {
256 var args = multiArgs(arguments);
257 curried.forEach(function(arg, index) {
258 if(arg != null || index >= args.length) args.splice(index, 0, arg);
259 });
260 return fn.apply(this, args);
261 }
262 }
263
264
265 });
266