master
Raw Download raw file
  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