summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.npmignore3
-rw-r--r--.travis.yml7
-rw-r--r--GruntFile.js14
-rw-r--r--README.md56
-rw-r--r--component.json4
-rw-r--r--examples/index.html53
-rw-r--r--lib/dom-batch.js169
-rw-r--r--package.json19
-rw-r--r--readme.md1
-rw-r--r--test/buster.js13
-rw-r--r--test/setup.js6
-rw-r--r--test/tests.js105
13 files changed, 364 insertions, 88 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6344cc2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+examples \ No newline at end of file
diff --git a/.npmignore b/.npmignore
new file mode 100644
index 0000000..a806a83
--- /dev/null
+++ b/.npmignore
@@ -0,0 +1,3 @@
+/node_modules/
+/examples/
+/test/ \ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..372f875
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,7 @@
+script:
+ - "npm test"
+
+language: node_js
+
+node_js:
+ - 0.10.2 \ No newline at end of file
diff --git a/GruntFile.js b/GruntFile.js
new file mode 100644
index 0000000..1c279aa
--- /dev/null
+++ b/GruntFile.js
@@ -0,0 +1,14 @@
+module.exports = function(grunt) {
+
+ // Project configuration.
+ grunt.initConfig({
+ buster: {
+ foo: {}
+ },
+ });
+
+ grunt.loadNpmTasks('grunt-buster');
+
+ // Default task.
+ grunt.registerTask('default', ['buster']);
+};
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c05376b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,56 @@
+# DOM-Batch
+
+Eliminates layout thrashing by batching DOM read/write interactions.
+
+```js
+var dom = new DomBatch();
+
+dom.read(function() {
+ console.log('<DOM Read>');
+});
+
+dom.write(function() {
+ console.log('<DOM Write>');
+});
+
+dom.read(function() {
+ console.log('<DOM Read>');
+});
+
+dom.write(function() {
+ console.log('<DOM Write>');
+});
+
+// Output:
+
+<DOM Read>
+<DOM Read>
+<DOM Write>
+<DOM Write>
+```
+
+## Tests
+
+#### With PhantomJS
+
+```
+$ npm install
+$ npm test
+```
+
+#### Without PhantomJS
+
+```
+$ node_modules/.bin/buster-static
+```
+
+...then visit http://localhost:8282/ in browser
+
+## Author
+
+- **Wilson Page** - [@wilsonpage](http://github.com/wilsonpage)
+
+## Contributors
+
+- **Wilson Page** - [@wilsonpage](http://github.com/wilsonpage)
+- **George Crawford** - [@georgecrawford](http://github.com/georgecrawford) \ No newline at end of file
diff --git a/component.json b/component.json
index a714a34..94d0242 100644
--- a/component.json
+++ b/component.json
@@ -1,7 +1,7 @@
{
"name": "dom-batch",
- "description": "Batches functions into a single async batch",
- "version": "0.0.1",
+ "description": "Eliminates layout thrashing by batching DOM read/write interactions",
+ "version": "0.3.0",
"main": "lib/dom-batch.js",
"scripts": [
"lib/dom-batch.js"
diff --git a/examples/index.html b/examples/index.html
deleted file mode 100644
index 99531ac..0000000
--- a/examples/index.html
+++ /dev/null
@@ -1,53 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-<meta charset="UTF-8">
-<title>title</title>
-<style></style>
-</head>
-<body>
- <div class='div1'></div>
- <div class='div2'></div>
- <div class='div3'></div>
- <script type="text/javascript" src="../lib/dom-batch.js"></script>
- <script>
-
-
- domBatch.write(function() {
- console.log('write1');
- });
-
- domBatch.read(function() {
- console.log('read1');
- });
-
- domBatch.read(function() {
- console.log('read1');
- });
-
- domBatch.write(function() {
- console.log('write1');
- });
-
-
- setTimeout(function() {
-
- domBatch.write(function() {
- console.log('write2');
- });
-
- domBatch.read(function() {
- console.log('read2');
- });
-
- domBatch.read(function() {
- console.log('read2');
- });
-
- domBatch.write(function() {
- console.log('write2');
- });
- }, 400);
- </script>
-</body>
-</html> \ No newline at end of file
diff --git a/lib/dom-batch.js b/lib/dom-batch.js
index 903033e..4671b80 100644
--- a/lib/dom-batch.js
+++ b/lib/dom-batch.js
@@ -1,38 +1,153 @@
-(function() {
- var domBatch = {};
- var reads = [];
- var writes = [];
- var batch;
-
- function call(fns) {
- var fn;
- while (fn = fns.shift()) fn();
+
+/**
+ * DOM-Batch
+ *
+ * Eliminates layout thrashing
+ * by batching DOM read/write
+ * interactions.
+ *
+ * @author Wilson Page <wilsonpage@me.com>
+ */
+
+;(function(){
+
+ 'use strict';
+
+ // RequestAnimationFrame Polyfill
+ var raf = window.requestAnimationFrame
+ || window.webkitRequestAnimationFrame
+ || window.mozRequestAnimationFrame
+ || function(cb) { window.setTimeout(cb, 1000 / 60); };
+
+ /**
+ * Creates a new
+ * DomBatch instance.
+ *
+ * (you should only have one
+ * instance per application).
+ *
+ * @constructor
+ */
+ function DomBatch() {
+ this.reads = [];
+ this.writes = [];
+ this.mode = null;
+ this.pending = false;
}
- domBatch.read = function(fn) {
- batch = batch || setBatch();
- reads.push(fn);
+ /**
+ * Adds a job to
+ * the read queue.
+ *
+ * @param {Function} fn
+ * @api public
+ */
+ DomBatch.prototype.read = function(fn) {
+ this.reads.push(fn);
+ this.request('read');
};
- domBatch.write = function(fn) {
- batch = batch || setBatch();
- writes.push(fn);
+ /**
+ * Adds a job to
+ * the write queue.
+ *
+ * @param {Function} fn
+ * @api public
+ */
+ DomBatch.prototype.write = function(fn) {
+ this.writes.push(fn);
+ this.request('write');
};
- function setBatch() {
- return setTimeout(function() {
- call(reads);
- call(writes);
- batch = null;
- }, 0);
- }
+ /**
+ * Makes the decision as to
+ * whether a the frame needs
+ * to be scheduled.
+ *
+ * @param {String} type
+ * @api private
+ */
+ DomBatch.prototype.request = function(type) {
+ var self = this;
+
+ // If we are currently writing, we don't
+ // need to scedule a new frame as this
+ // job will be emptied from the write queue
+ if (this.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 (this.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 (this.mode === 'reading' && type === 'write') return;
+
+ // If there is already a frame
+ // scheduled, don't schedule another one
+ if (this.pending) return;
+
+ // Schedule frame (preserving context)
+ raf(function() { self.frame(); });
+
+ // Set flag to indicate
+ // a frame has been scheduled
+ this.pending = true;
+ };
+
+ /**
+ * Calls each job in
+ * the list passed.
+ *
+ * @param {Array} list
+ * @api private
+ */
+ DomBatch.prototype.run = function(list) {
+ while (list.length) {
+ list.shift().call(this);
+ }
+ };
+
+ /**
+ * Runs any read jobs followed
+ * by any write jobs.
+ *
+ * @api private
+ */
+ DomBatch.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;
+
+ // Set the mode to 'reading' so
+ // that we know we can add more
+ // reads to the queue instead of
+ // scheduling a new frame.
+ this.mode = 'reading';
+ this.run(this.reads);
+
+ // Set
+ this.mode = 'writing';
+ this.run(this.writes);
+
+ this.mode = null;
+ };
+
+ /**
+ * Expose 'DomBatch'
+ */
- // Expose the library
if (typeof exports === "object") {
- module.exports = domBatch;
+ module.exports = DomBatch;
} else if (typeof define === "function" && define.amd) {
- define(domBatch);
+ define(function(){ return DomBatch; });
} else {
- window['domBatch'] = domBatch;
+ window['DomBatch'] = DomBatch;
}
-}()); \ No newline at end of file
+
+})();
diff --git a/package.json b/package.json
index 407e35d..74c91c8 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "dom-batch",
- "description": "Batches functions into a single async batch",
- "version": "0.0.1",
+ "description": "Eliminates layout thrashing by batching DOM read/write interactions",
+ "version": "0.3.0",
"main": "lib/dom-batch.js",
"scripts": [
"lib/dom-batch.js"
@@ -13,7 +13,16 @@
},
"repository": {
"type": "git",
- "url": "git://github.com/wilsonpage/fruitmachine.git"
+ "url": "git://github.com/wilsonpage/dom-batch.git"
},
- "license": "MIT"
-} \ No newline at end of file
+ "license": "MIT",
+ "devDependencies": {
+ "grunt-buster": "~0.2.1",
+ "grunt-cli": "~0.1.9",
+ "grunt": "~0.4.1",
+ "buster": "~0.6.12"
+ },
+ "scripts": {
+ "test": "./node_modules/.bin/grunt buster"
+ }
+}
diff --git a/readme.md b/readme.md
deleted file mode 100644
index 81ae3d3..0000000
--- a/readme.md
+++ /dev/null
@@ -1 +0,0 @@
-DOM batcher provides a way for you to add DOM read functions and DOM writes functions to a batch. When the end of the synchonous flow is reached, all the DOM reads are executed, then all the DOM writes. This is useful as DOM reads after DOM writes require the DOM to recompute layout which is expensive. Doing all your DOM reads, then all your DOM writes is the most efficient way of dealing with the DOM. \ No newline at end of file
diff --git a/test/buster.js b/test/buster.js
new file mode 100644
index 0000000..33839c3
--- /dev/null
+++ b/test/buster.js
@@ -0,0 +1,13 @@
+var config = module.exports;
+
+config["dom-batch"] = {
+ rootPath: '../',
+ environment: "browser",
+ sources: [
+ 'test/setup.js',
+ 'lib/dom-batch.js'
+ ],
+ tests: [
+ 'test/tests.js'
+ ]
+};
diff --git a/test/setup.js b/test/setup.js
new file mode 100644
index 0000000..c556d1b
--- /dev/null
+++ b/test/setup.js
@@ -0,0 +1,6 @@
+
+// RequestAnimationFrame Polyfill
+var raf = window.requestAnimationFrame
+ || window.webkitRequestAnimationFrame
+ || window.mozRequestAnimationFrame
+ || function(cb) { window.setTimeout(cb, 1000 / 60); }; \ No newline at end of file
diff --git a/test/tests.js b/test/tests.js
new file mode 100644
index 0000000..bc708b4
--- /dev/null
+++ b/test/tests.js
@@ -0,0 +1,105 @@
+
+buster.testCase('DomBatch', {
+
+ "Should run reads before writes": function(done) {
+ var dom = new DomBatch();
+
+ var read = this.spy(function() {
+ refute(write.called);
+ });
+
+ var write = this.spy(function() {
+ assert(read.called);
+ done();
+ });
+
+ dom.read(read);
+ dom.write(write);
+ },
+
+ "Should call all reads together, followed by all writes": function(done) {
+ var dom = new DomBatch();
+ var read1 = this.spy();
+ var read2 = this.spy();
+ var write1 = this.spy();
+ var write2 = this.spy();
+
+ // Assign unsorted
+ dom.read(read1);
+ dom.write(write1);
+ dom.read(read2);
+ dom.write(write2);
+
+ // After the queue has been emptied
+ // check the callbacks were called
+ // in the correct order.
+ raf(function() {
+ assert(read1.calledBefore(read2));
+ assert(read2.calledBefore(write1));
+ assert(write1.calledBefore(write2));
+ done();
+ });
+ },
+
+ "Should call a read in the same frame if scheduled inside a read callback": function(done) {
+ var dom = new DomBatch();
+ var cb = this.spy();
+
+ dom.read(function() {
+
+ // Schedule a callback for *next* frame
+ raf(cb);
+
+ // Schedule a read callback
+ // that should be run in the
+ // current frame checking that
+ // the RAF callback has not
+ // yet been fired.
+ dom.read(function() {
+ refute(cb.called);
+ done();
+ });
+ });
+ },
+
+ "Should call a write in the same frame if scheduled inside a read callback": function(done) {
+ var dom = new DomBatch();
+ var cb = this.spy();
+
+ dom.read(function() {
+
+ // Schedule a callback for *next* frame
+ raf(cb);
+
+ // Schedule a read callback
+ // that should be run in the
+ // current frame checking that
+ // the RAF callback has not
+ // yet been fired.
+ dom.write(function() {
+ refute(cb.called);
+ done();
+ });
+ });
+ },
+
+ "Should call a read in the *next* frame if scheduled inside a write callback": function(done) {
+ var dom = new DomBatch();
+ var cb = this.spy();
+
+ dom.write(function() {
+
+ // Schedule a callback for *next* frame
+ raf(cb);
+
+ // Schedule a write that should be
+ // called in the next frame, meaning
+ // the test callback should have already
+ // been called.
+ dom.read(function() {
+ assert(cb.called);
+ done();
+ });
+ });
+ }
+}); \ No newline at end of file