diff options
Diffstat (limited to 'fastdom.js')
-rw-r--r-- | fastdom.js | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/fastdom.js b/fastdom.js new file mode 100644 index 0000000..919ac4f --- /dev/null +++ b/fastdom.js @@ -0,0 +1,232 @@ +!(function(win) { + +/** + * FastDom + * + * Eliminates layout thrashing + * by batching DOM read/write + * interactions. + * + * @author Wilson Page <wilsonpage@me.com> + * @author Kornel Lesinski <kornel.lesinski@ft.com> + */ + +'use strict'; + +/** + * Mini logger + * + * @return {Function} + */ +var debug = 0 ? console.log.bind(console, '[fastdom]') : function() {}; + +/** + * Normalized rAF + * + * @type {Function} + */ +var raf = win.requestAnimationFrame + || win.webkitRequestAnimationFrame + || win.mozRequestAnimationFrame + || win.msRequestAnimationFrame + || function(cb) { return setTimeout(cb, 16); }; + +/** + * Initialize a `FastDom`. + * + * @constructor + */ +function FastDom() { + var self = this; + self.reads = []; + self.writes = []; + self.raf = raf.bind(win); // test hook + debug('initialized', self); +} + +FastDom.prototype = { + constructor: FastDom, + + /** + * Adds a job to the read batch and + * schedules a new frame if need be. + * + * @param {Function} fn + * @public + */ + measure: function(fn, ctx) { + debug('measure'); + var task = { fn: fn, ctx: ctx }; + this.reads.push(task); + scheduleFlush(this); + return task; + }, + + /** + * Adds a job to the + * write batch and schedules + * a new frame if need be. + * + * @param {Function} fn + * @public + */ + mutate: function(fn, ctx) { + debug('mutate'); + var task = { fn: fn, ctx: ctx }; + this.writes.push(task); + scheduleFlush(this); + return task; + }, + + /** + * Clears a scheduled 'read' or 'write' task. + * + * @param {Object} task + * @return {Boolean} success + * @public + */ + clear: function(task) { + debug('clear', task); + return remove(this.reads, task) || remove(this.writes, task); + }, + + /** + * Extend this FastDom with some + * custom functionality. + * + * Because fastdom must *always* be a + * singleton, we're actually extending + * the fastdom instance. This means tasks + * scheduled by an extension still enter + * fastdom's global task queue. + * + * The 'super' instance can be accessed + * from `this.fastdom`. + * + * @example + * + * var myFastdom = fastdom.extend({ + * // called on creation + * initialize: function() { + * + * }, + * + * // override a method + * measure: function(fn) { + * // do extra stuff ... + * + * // then call the original + * return this.fastdom.measure(fn); + * }, + * + * ... + * }); + * + * @param {Object} props properties to mixin + * @return {FastDom} + */ + extend: function(props) { + debug('extend', props); + if (typeof props != 'object') throw new Error('expected object'); + + var child = Object.create(this); + Object.assign(child, props); + child.fastdom = this; + + // run optional creation hook + if (child.initialize) child.initialize(); + + return child; + }, + + // override this with a function + // to prevent Errors in console + // when tasks throw + catch: null +}; + +/** + * Schedules a new read/write + * batch if one isn't pending. + * + * @private + */ + +function scheduleFlush(fastdom) { + if (!fastdom.scheduled) { + fastdom.scheduled = true; + fastdom.raf(flush.bind(null, fastdom)); + debug('flush scheduled'); + } +} + +/** + * Runs queued `read` and `write` tasks. + * + * Errors are caught and thrown by default. + * If a `.catch` function has been defined + * it is called instead. + * + * @private + */ +function flush(fastdom) { + debug('flush'); + + var writes = fastdom.writes; + var reads = fastdom.reads; + var error; + + try { + debug('flushing reads', reads.length); + runTasks(reads); + debug('flushing writes', writes.length); + runTasks(writes); + } catch (e) { error = e; } + + fastdom.scheduled = false; + + // If the batch errored we may still have tasks queued + if (reads.length || writes.length) scheduleFlush(fastdom); + + if (error) { + debug('task errored', error.message); + if (fastdom.catch) fastdom.catch(error); + else throw error; + } +} + +/** + * We run this inside a try catch + * so that if any jobs error, we + * are able to recover and continue + * to flush the batch until it's empty. + * + * @private + */ + +function runTasks(tasks) { + debug('run tasks'); + var task; while (task = tasks.shift()) task.fn.call(task.ctx); +} + +/** + * Remove an item from an Array. + * + * @param {Array} array + * @param {*} item + * @return {Boolean} + */ +function remove(array, item) { + var index = array.indexOf(item); + return !!~index && !!array.splice(index, 1); +} + +// There should never be more than +// one instance of `FastDom` in an app +var exports = win.fastdom = (win.fastdom || new FastDom()); // jshint ignore:line + +// Expose to CJS & AMD +if ((typeof define)[0] == 'f') define(function() { return exports; }); +else if ((typeof module)[0] == 'o') module.exports = exports; + +})(window); |