summaryrefslogtreecommitdiffstats
path: root/fastdom.js
diff options
context:
space:
mode:
Diffstat (limited to 'fastdom.js')
-rw-r--r--fastdom.js232
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);