summaryrefslogtreecommitdiffstats
path: root/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'index.js')
-rw-r--r--index.js245
1 files changed, 153 insertions, 92 deletions
diff --git a/index.js b/index.js
index 0f657b5..7efb9d5 100644
--- a/index.js
+++ b/index.js
@@ -1,6 +1,6 @@
/**
- * DOM-Batch
+ * FastDom
*
* Eliminates layout thrashing
* by batching DOM read/write
@@ -38,11 +38,11 @@
* @constructor
*/
function FastDom() {
+ this.frames = [];
this.lastId = 0;
- this.jobs = {};
this.mode = null;
- this.pending = false;
this.queue = {
+ hash: {},
read: [],
write: []
};
@@ -57,8 +57,16 @@
*/
FastDom.prototype.read = function(fn, ctx) {
var job = this.add('read', fn, ctx);
+
this.queue.read.push(job.id);
- this.request('read');
+
+ // If we're writing and a 'read' job
+ // comes in, we do have to schedule a new frame
+ var needsFrame = !this.batchPending || this.mode === 'writing';
+
+ // Schedule a new frame if need be
+ if (needsFrame) this.scheduleBatch();
+
return job.id;
};
@@ -71,74 +79,106 @@
*/
FastDom.prototype.write = function(fn, ctx) {
var job = this.add('write', fn, ctx);
+
this.queue.write.push(job.id);
- this.request('write');
+
+ // If we're emptying the read
+ // queue and a write comes in,
+ // we don't need to schedule a
+ // new frame. If we're writing
+ // and write comes in we don't
+ // need to schedule a new frame
+ var needsFrame = !this.batchPending;
+
+ // Schedule a new frame if need be
+ if (needsFrame) this.scheduleBatch();
+
return job.id;
};
/**
- * Removes a job from
- * the 'reads' queue.
+ * Defers the given job
+ * by the number of frames
+ * specified.
+ *
+ * @param {Number} frame
+ * @param {Function} fn
+ * @api public
+ */
+ FastDom.prototype.defer = function(frame, fn, ctx) {
+
+ // Accepts two arguments
+ if (typeof frame === 'function') {
+ ctx = fn;
+ fn = frame;
+ frame = 1;
+ }
+
+ var self = this;
+ var index = frame - 1;
+
+ return this.schedule(index, function() {
+ self.run({
+ fn: fn,
+ ctx: ctx
+ });
+ });
+ };
+
+ /**
+ * Clears a scheduled 'read',
+ * 'write' or 'defer' job.
*
* @param {Number} id
* @api public
*/
FastDom.prototype.clear = function(id) {
- var job = this.jobs[id];
- if (!job) return;
-
- // Clear reference
- delete this.jobs[id];
// Defer jobs are cleared differently
- if (job.type === 'defer') {
- caf(job.timer);
- return;
+ if (typeof id === 'function') {
+ return this.clearFrame(id);
}
+ var job = this.queue.hash[id];
+ if (!job) return;
+
var list = this.queue[job.type];
var index = list.indexOf(id);
+
+ // Clear references
+ delete this.queue.hash[id];
if (~index) list.splice(index, 1);
};
/**
- * Makes the decision as to
- * whether a the frame needs
- * to be scheduled.
+ * Clears a scheduled frame.
*
- * @param {String} type
+ * @param {Function} frame
* @api private
*/
- FastDom.prototype.request = function(type) {
- var mode = this.mode;
- var self = this;
-
- // If we are currently writing, we don't
- // need to schedule a new frame as this
- // job will be emptied from the write queue
- if (mode === 'writing' && type === 'write') return;
-
- // If we are reading we don't need to schedule
- // a new frame as this read will be emptied
- // in the currently active read queue
- if (mode === 'reading' && type === 'read') return;
-
- // If we are reading we don't need to schedule
- // a new frame and this write job will be run
- // after the read queue has been emptied in the
- // currently active frame.
- if (mode === 'reading' && type === 'write') return;
+ FastDom.prototype.clearFrame = function(frame) {
+ var index = this.frames.indexOf(frame);
+ if (~index) this.frames.splice(index, 1);
+ };
- // If there is already a frame
- // scheduled, don't schedule another one
- if (this.pending) return;
+ /**
+ * Schedules a new read/write
+ * batch if one isn't pending.
+ *
+ * @api private
+ */
+ FastDom.prototype.scheduleBatch = function() {
+ var self = this;
- // Schedule frame (preserving context)
- raf(function() { self.frame(); });
+ // Schedule batch for next frame
+ this.schedule(0, function() {
+ self.runBatch();
+ self.batchPending = false;
+ });
// Set flag to indicate
// a frame has been scheduled
- this.pending = true;
+ this.batchPending = true;
};
/**
@@ -167,7 +207,7 @@
FastDom.prototype.flush = function(list) {
var id;
while (id = list.shift()) {
- this.run(this.jobs[id]);
+ this.run(this.queue.hash[id]);
}
};
@@ -177,12 +217,7 @@
*
* @api private
*/
- FastDom.prototype.frame = function() {
-
- // Set the pending flag to
- // false so that any new requests
- // that come in will schedule a new frame
- this.pending = false;
+ FastDom.prototype.runBatch = function() {
// Set the mode to 'reading',
// then empty all read jobs
@@ -198,32 +233,6 @@
};
/**
- * Defers the given job
- * by the number of frames
- * specified.
- *
- * @param {Number} frames
- * @param {Function} fn
- * @api public
- */
- FastDom.prototype.defer = function(frames, fn, ctx) {
- if (frames < 0) return;
- var job = this.add('defer', fn, ctx);
- var self = this;
-
- (function wrapped() {
- if (!(frames--)) {
- self.run(job);
- return;
- }
-
- job.timer = raf(wrapped);
- })();
-
- return job.id;
- };
-
- /**
* Adds a new job to
* the given queue.
*
@@ -235,7 +244,7 @@
*/
FastDom.prototype.add = function(type, fn, ctx) {
var id = this.uniqueId();
- return this.jobs[id] = {
+ return this.queue.hash[id] = {
id: id,
fn: fn,
ctx: ctx,
@@ -244,17 +253,8 @@
};
/**
- * Called when a callback errors.
- * Overwrite this if you don't
- * want errors inside your jobs
- * to fail silently.
- *
- * @param {Error}
- */
- FastDom.prototype.onError = function(){};
-
- /**
* Runs a given job.
+ *
* @param {Object} job
* @api private
*/
@@ -262,14 +262,75 @@
var ctx = job.ctx || this;
// Clear reference to the job
- delete this.jobs[job.id];
+ delete this.queue.hash[job.id];
- // Call the job in
- try { job.fn.call(ctx); } catch(e) {
- this.onError(e);
+ if (this.quiet) {
+ try { job.fn.call(ctx); } catch (e) {}
+ } else {
+ job.fn.call(ctx);
}
};
+ /**
+ * Starts of a rAF loop
+ * to empty the frame queue.
+ *
+ * @api private
+ */
+ FastDom.prototype.loop = function() {
+ var self = this;
+
+ // Don't start more than one loop
+ if (this.looping) return;
+
+ raf(function frame() {
+ var fn = self.frames.shift();
+
+ // Run the frame
+ if (fn) fn();
+
+ // If no more frames,
+ // stop looping
+ if (!self.frames.length) {
+ self.looping = false;
+ return;
+ }
+
+ raf(frame);
+ });
+
+ this.looping = true;
+ };
+
+ /**
+ * Adds a function to
+ * a specified index
+ * of the frame queue.
+ *
+ * @param {Number} index
+ * @param {Function} fn
+ * @return {Function}
+ */
+ FastDom.prototype.schedule = function(index, fn) {
+
+ // Make sure this slot
+ // hasn't already been
+ // taken. If it has, try
+ // re-scheduling for the next slot
+ if (this.frames[index]) {
+ return this.schedule(index + 1, fn);
+ }
+
+ // Start the rAF
+ // loop to empty
+ // the frame queue
+ this.loop();
+
+ // Insert this function into
+ // the frames queue and return
+ return this.frames[index] = fn;
+ };
+
// We only ever want there to be
// one instance of FastDom in an app
fastdom = fastdom || new FastDom();