breakup.js
v0.1.0
Copyright 2013 Nic Jansma
http://nicj.net
Licensed under the MIT license
Introduction
Serially enumerating over a collection (such as using async.forEachSeries()
in Node.js or jQuery.each()
in the browser) can lead to performance and
responsiveness issues if processing or looping through the collection takes
too long. In some browsers, enumerating over a large number of elements (or
doing a lot of work on each element) may cause the browser to become unresponsive,
and possibly prompt the user to stop running the script.
breakup.js
helps solve this problem by breaking up the enumeration into
time-based chunks, and yielding to the environment if a threshold of time
has passed before continuing. This will help avoid a Long Running Script
dialog in browsers as they are given a chance to update their UI. It is meant
to be a simple, drop-in replacement for async.forEachSeries()
. It also provides
breakup.each()
as a replacement for jQuery.each()
(though the developer may
have to modify code-flow to deal with the asynchronous nature of breakup.js).
breakup.js
does this by keeping track of how much time the enumeration has taken
after processing each item. If the enumeration time has passed a threshold (the
default is 50ms, but this can be customized), the enumeration will yield before
resuming. Yielding can be done immediately in environments that support it (such
as process.nextTick()
in Node.js and setImmediate()
in modern browsers), and
will fallback to a setTimeout(..., 4)
in older browsers. This yield will allow
the environment to do any UI and other processing work it wants to do. In browsers,
this will help reduce the chance of a Long Running Script dialog.
Changing async.forEachSeries()
to breakup.forEachSeries()
is as simple as
changing the module name. You may add two additional parameters to fine-tune
the wait time and yield time if you prefer (see Documentation for details).
Changing jQuery.each()
to breakup.each()
requires a bit more work as you
will need to change from waiting for the function to return to waiting for a callback
to fire. See the breakup.each()
for details.
Download
Releases are available for download from GitHub.
Alternatively, you can install using Node Package Manager (npm):
npm install breakup
Development: breakup.js - 8.1kb
Production: breakup.min.js - 1.5kb (minified)
Node.js / async.js
breakup.js can be used as a drop-in replacement for the async.forEachSeries()
function in the
async.js Node.js module (which can also be used in browsers).
For example, instead of using async.forEachSeries()
:
var async = require('async');
async.forEachSeries(objs, function(i, item, callback) {}, function(err) {});
You can use breakup.js's version:
var breakup = require('breakup');
breakup.forEachSeries(objs, function(i, item, callback) {}, function(err) {});
Additional parameters are available for breakup.forEachSeries()
to control
the yielding behavior, see the Documentation for details.
In The Browser
To use any of the breakup.js functions, simply add a <script>
tag:
<script type="text/javascript" src="breakup.js"></script>
<script type="text/javascript">
breakup.forEachSeries(data, iterateFn, function(err){
});
</script>
jQuery
breakup.js can be used as a replacement for jQuery's jQuery.each()
as breakup.each()
,
or as a replacement for the jQuery selector jQuery(selector).each()
as jQuery(selector).breakup()
.
However, breakup.js may require some changes to existing jQuery code so it will
know how to handle the asynchronous nature of breakup.js. For example, when
you're using jQuery.each()
, the operation will block until the enumeration
is complete. Since breakup.js relies on callback events so it can yield to the browser,
existing jQuery code will need to pass in a callback-complete function parameter so it knows
when the enumeration has completed. If you don't do this, the code that follows may
break on the assumption that all of the enumeration in jQuery.each()
has
completed. Essentially, you will need to change your code to handle callback-driven
flow control.
In addition, this type of change may require you to change any code calling
the function that the original jQuery.each()
call was in if it returned a value
that depended on that work, as the new breakup.each()
's callback-complete function is
what will drive the new code flow. If you need the return value of a function called
jQuery.each()
you will have to have breakup.each()
's callback-complete fire a
new callback with the return values instead of simply returning it in the original function call.
For example, you may be using jQuery.each()
like this:
function doIteration() {
var a = [];
$.each(
objs,
function(i, item) { a.push(item.something()); });
return a.length;
}
var b = doIteration();
Here's how you should adjust the above code for breakup.each()
:
- Change
jQuery.each()
(or $.each()
) to breakup.each()
- Add a third parameter to
breakup.each()
, which is the callback-complete function - Wrap any subsequent code that depended on work done in
$.each()
into
your new callback-complete function (eg return a.length
above) - Change any callers of this code to take a new completion callback instead of a return value
Sample:
function doIteration(callback) {
var a = [];
breakup.each(
objs,
function(i, item) { a.push(item.something()); },
function(err) {
callback(a.length);
});
}
doIteration(function(b) {
});
Documentation
### forEachSeries(arr, iterator, callback, workTime, yieldTime)
This function should be a drop-in replacement for async.forEachSeries()
.
Applies an iterator function to each item in an array, in series.
The iterator is called with an item from the list and a callback for when it
has finished. If the iterator passes an error to this callback, the main
callback for the forEachSeries function is immediately called with the error.
Arguments
Example
var breakup = require('breakup');
var arr = [];
breakup.forEachSeries(
[1,2,3],
function(item, callback) {
arr.push(item);
callback();
},
function(err) {
}
);
### each(arr, iterator, callback, workTime, yieldTime)
This function is meant to be a replacement for jQuery.each()
or jQuery(selector).each()
.
jQuery.each()
can be replaced by breakup.each()
(per NOTE below).
If jQuery is defined before breakup.js is included, jQuery will also be extended by
adding the jQuery(selector).breakup()
function.
each()
applies an iterator function to each item in an array, in series.
The iterator is called with the item's index, the item, and a callback for when it
has finished. If the iterator passes an error to this callback, the main
callback for the each function is immediately called with the error.
Arguments
- arr - An array to iterate over.
- iterator(index, item, callback) - A function to apply to each item in the array.
The iterator is passed a callback(err) which must be called once it has completed.
If no error has occured, the callback should be run without arguments or
with an explicit null argument.
- callback(err) - A callback which is called after all the iterator functions
have finished, or an error has occurred.
Example
var arr = [];
breakup.each(
[1,2,3],
function(index, item, callback) {
arr.push(item);
callback();
},
function(err) {
}
);
$([1,2,3]).breakup(
function(index, item, callback) {
arr.push(item);
callback();
},
function(err) {
}
);
**NOTE**: The difference between `breakup.forEachSeries()` and `breakup.each()`
is the iterator signature: `forEachSeries()` iterates with `function(item, callback)` and
requires the callback to indicate work is done. This matches the `async.forEachSeries()`
signature. On the other hand, `each()` matches the jQuery signature by using the iterator
`function(index, item)`, and waiting on the return of the function to move to the next
item. If you need to switch from `jQuery.each()` to `breakup.forEachSeries()`, you will need
to change the signature, and thus your code flow, to handle the callback instead of the function return.
DEFAULT_WORK_TIME
Default for how many milliseconds to enumerate for prior to yielding.
By default, this value is set to 50
(ms).
If not specified as the fourth parameter of forEachSeries()
or each()
, this value will be used.
You may overwrite this value to change the global default.
This will be a best-case scenario. If a single item takes longer than the DEFAULT_WORK_TIME
to process, the yield won't occur until after that item fires its callback. In other words,
enumeration won't yield mid-item: the time-check is performed only at each item's callback.
DEFAULT_YIELD_TIME
How much time to yield for if process.nextTick()
or setImmediate()
is not available.
By default, this value is set to 4
(ms).
If not specified as the fifth parameter of forEachSeries()
or each()
, this value will be used.
You may overwrite this value to change the global default.
### noConflict()
Changes the value of breakup back to its original value, returning a reference to the
breakup object.
Version History
- v0.1.0 - 2013-02-11 Initial version
Thanks
This module (and documentation, tests, etc) were inspired by
caolan's async.js module.