diff options
Diffstat (limited to 'index.js')
-rw-r--r-- | index.js | 245 |
1 files changed, 153 insertions, 92 deletions
@@ -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(); |