Posted by Juha Lindstedt on January 4th, 2016
I'm going to show you step by step how I built a fully capable and extremely performant view library, weighting just couple of kilobytes. I will also prove that DOM is actually quite fast, if used properly.
View library we're going to create doesn't care if the data is mutable or immutable - both will work. You can also choose to reorder DOM elements by key or just replace the contents. But the main idea is to understand 100 % how everything work under the hood.
These techniques are based on my FRZR view library (simplified a bit). Check it out if you want to get started immediately, but I still encourage you to follow through these posts – I promise there's something new to learn.
We will begin by focusing on creating, reordering and removing DOM elements in this first post.
Let's start!
Before we're going to build anything, let's study how HTML elements are created in plain JavaScript.
Creating HTML elements is quite easy:
// create elements
var h1 = document.createElement('h1');
var p = document.createElement('p');
// add text
h1.textContent = 'Hello world';
p.textContent = 'Vanilla JavaScript rocks!';
// add to DOM
document.body.appendChild(h1);
document.body.appendChild(p);
// result
// <body><h1>Hello world</h1><p>Vanilla JavaScript rocks!</p></body>
We'll make it even easier with a little helper function:
function el (tagName, attributes) {
var element = document.createElement(tagName);
// go through attributes and set them
for (var attributeName in attributes) {
element[attributeName] = attributes[attributeName];
}
return element;
}
Couldn't be smoother to use:
// create elements
var h1 = el('h1', { textContent: 'Hello WRLD!' });
var p = el('p', { innerHTML: "Works like the train toilet.<br><i>(Finnish proverb)</i>" });
// add to DOM
document.body.appendChild(h1);
document.body.appendChild(p);
Next we will create a View. It will be just a wrapper for HTML elements, with some extra features (we will get back to those in the next part). Think Views as components.
Let's get to business and define a View constructor:
function View (options, data) {
for (var key in options) {
if (key === 'el') {
// little trick here to pass the parameters to the el helper
if (typeof options.el === 'string') {
this.el = el(options.el);
} else if (options.el instanceof Array) {
this.el = el(options.el[0], options.el[1]);
} else {
this.el = options.el;
}
} else {
this[key] = options[key];
}
}
// let's get back to this line later
if (this.init) this.init(data);
}
Then add some child mounting methods:
View.prototype.addChild = function (childView) {
this.el.appendChild(childView.el);
childView.parent = this;
};
View.prototype.addBefore = function (childView, before) {
this.el.insertBefore(childView.el, before.el || before);
childView.parent = this;
};
Let's try it out!
var body = new View({ el: document.body });
var h1 = new View({
el: ['h1', { textContent: 'Hello View!' }]
});
var p = new View({
el: ['p', { textContent: 'Powered by: ' }]
});
// shameless advertisement
var a = new View({
el: ['a', { href: 'https://frzr.js.org', target: '_blank', textContent: 'FRZR' }]
});
p.addChild(a);
body.addChild(h1);
body.addChild(p);
Time for some magic. Here's one simple method to add/reorder/remove child views:
View.prototype.setChildren = function (views) {
// traverse the DOM starting from the first child element (if present)
var traverse = this.el.firstChild;
// go through given views (if any)
if (views) {
for (var i = 0; i < views.length; i++) {
if (views[i].el === traverse) {
// element already in place, continue to next sibling
traverse = traverse.nextSibling;
continue;
}
// insert/reorder element to the dom
if (traverse) {
this.addBefore(views[i], traverse);
} else {
this.addChild(views[i]);
}
}
}
// remove any DOM nodes left out
while (traverse) {
var next = traverse.nextSibling;
this.el.removeChild(traverse);
traverse = next;
}
}
Let's create a list of Views and start to shuffle them:
var body = new View({ el: document.body });
var ul = new View({ el: 'ul' });
var views = new Array(25);
for (var i = 0; i < views.length; i++) {
views[i] = new View({
el: ['li', { textContent: 'Item ' + i }]
});
}
ul.setChildren(views);
body.addChild(ul);
setInterval(function () {
views.sort(function () { return Math.random() * 2 - 1; });
ul.setChildren(views);
}, 250);
This is just for convenience, an inheritance helper function:
View.extend = function (options) {
function ExtendedView (data) {
View.call(this, options, data);
}
ExtendedView.prototype = Object.create(View.prototype);
ExtendedView.prototype.constructor = ExtendedView;
return ExtendedView;
}
Now we can start building components. Let's also measure how performant our tiny view library is by reordering 1000 DOM elements one by one!
// our list element component
var Li = View.extend({
el: 'li',
init: function (data) {
// this gets executed when the View is created
this.el.textContent = data;
}
})
var body = new View({ el: document.body });
var ul = new View({ el: 'ul' });
// let's create list of views
var views = new Array(1000);
for (var i = 0; i < views.length; i++) {
views[i] = new Li('Item ' + i);
}
// add to DOM
ul.setChildren(views);
// let's see how fast we're running
var fps = new View({ el : 'p' });
body.addChild(fps);
body.addChild(ul);
// fps calculation..
var lastFrame = Date.now();
var frameTimeTotal = 0;
var frameTimeCount = 0;
// start running
tick();
function tick () {
// tick on every animationFrame
requestAnimationFrame(tick);
// save frame start time
var now = Date.now();
// take last element and move to first
views.unshift(views.pop());
// update views
ul.setChildren(views);
// fps stuff
frameTimeTotal += (now - lastFrame);
frameTimeCount++;
lastFrame = now;
};
setInterval(function () {
// print fps
fps.el.textContent = 'Running at ' + (1000 / (frameTimeTotal / frameTimeCount)).toFixed(2) + ' fps';
}, 1000);
In the next episode we'll learn how to update the DOM elements efficiently. While I write the next post, you can check out some more advanced examples made with FRZR.
Click here to subscribe. I will send you a maximum of one email per blog post and promise not to share your email to anyone.