Aug 14, 2014 in jQuery, Javascript, Interview
Sometimes during Javascript interview I ask candidates about jQuery internals. Nothing really crazy or too complicated, but I believe that a programmer with sufficient knowledge of the language will not have problems understanding the jQuery "magic".
In the next few sections I will cover main concepts you can use to build own simple lightweight "jQuery". Of course it's going to be very basic and simple library, but it will perfectly illustrate the idea. To avoid confusion let's call it ion - something small, tiny and not associated with jQuery and $ sign.
The very first thing to understand is that jQuery's $
is nothing but a function. It has a fancy name, but still. We start implementation with a usual IIFE:
(function(window, undefined) {
function ion(selector) {
if (!(this instanceof ion)) {
return new ion(selector);
}
};
ion.fn = ion.prototype;
window.ion = ion;
})(window);
This $
function does a lot of things depending on passed parameters, but we can narrow them down to several main cases:
Single function parameter. If the function is passed into $
function it will be executed on DOM ready (DOMContentLoaded) event, or immediately if this event has already occurred.
String parameter. String argument is considered either a CSS selector or HTML string to construct a new DOM object.
HTMLElement. If HTMLElement is passed, it's wrapped into jQuery instance object, resulting in jQuery collection of one element.
NodeList. Node list is treated the same as single Node element above. The only difference is that returned jQuery collection includes multiple nodes.
The function also ensures that it is invoked as a constructor. This is important because we need to work with its prototype.
Now when we have outlined main behavior ion
must have, let's see how it can be implemented.
In order to support function as argument we will store it in the stack of callbacks to be invoked on DOM ready event. It can be written like this:
(function(window, undefined) {
// Handle DOMContentLoaded event
var domReadyStack = [];
function handleDOMReady(fn) {
return document.readyState === 'complete' ? fn.call(document) : domReadyStack.push(fn);
}
document.addEventListener('DOMContentLoaded', function onDOMReady() {
document.removeEventListener('DOMContentLoaded', onDOMReady);
while (domReadyStack.length) {
domReadyStack.shift().call(document);
}
});
// ion constructor
function ion(selector) {
if (!(this instanceof ion)) {
return new ion(selector);
}
if (typeof selector === 'function') {
return handleDOMReady(selector);
}
}
ion.fn = ion.prototype;
window.ion = ion;
})(window);
Similarly to jQuery we can use it this way:
ion(function() {
console.log('DOM is ready!');
});
Now we come to the essence of the library: DOM elements selection and creation. Retrieving DOM elements is pretty easy if we don't have to support older browsers (we don't :), so we can simply use querySelectorAll
method for this. If selector
is already a DOM element or NodeList then we only need to set them to nodes
array for internal use.
(function(window, undefined) {
// Handle DOMContentLoaded event
// ...
// ion constructor
function ion(selector) {
// ...
// Number of elements in collection
this.length = 0;
// Nodes collection array
this.nodes = [];
// HTMLElements and NodeLists are wrapped in nodes array
if (selector instanceof HTMLElement || selector instanceof NodeList) {
this.nodes = selector.length > 1 ? [].slice.call(selector) : [selector];
}
else if (typeof selector === 'string') {
if (selector[0] === '<' && selector[selector.length - 1] === ">") {
// Create DOM elements
this.nodes = [createNode(selector)];
}
else {
// Query DOM
this.nodes = [].slice.call(document.querySelectorAll(selector));
}
}
if (this.nodes.length) {
this.length = this.nodes.length;
for (var i = 0; i < this.nodes.length; i++) {
this[i] = this.nodes[i];
}
}
}
function createNode(html) {
var div = document.createElement('div');
div.innerHTML = html;
return div.firstChild;
}
ion.fn = ion.prototype;
window.ion = ion;
})(window);
Note how HTMLElement
's and NodeList
's are converted to plain arrays and assigned to own property nodes
of the current ion
instance as well as individual properties of it. It allows to access specific DOM elements in familiar way, like: ion(".post")[0]
.
If selector is a string with the first character <
and the last >
, then such string is considered an HTML and is used to create a new DOM element. Note that for this purposed I used pretty naive approach with innerHTML
. It's not ideal because of several reasons, one of them being is that browser will start downloading media content such as images or scripts immediately. But for the sake of this simple library it is not a problem.
Take a look at some demonstration: Demo 1.
Now let's see what ion
can do so far:
// 1. Run code on DOM ready
ion(function() {
console.log('DOM is ready');
});
// 2. Retrieve elements by CSS selector
var $oddLi = ion('.item-list > li:nth-child(odd)');
// 3. Create new DOM elements
var $newDiv = ion('<div class="profile"></div>');
All right, now when we have this basics, let's create actual methods we know from jQuery.
This is fun part. The reason that jQuery is so cool is that it offers tons of useful methods, which are also very convenient to use due to method chaining. I'm going to show how easy it is to extend our simple library with this functionality. Of course it doesn't make sense to reimplement all the methods jQuery has, just few of them are going to be a good example.
Let's start with each
method to iterate all the elements in the set. All the instance methods will be defined on ion
prototype:
ion.fn.each = function(callback) {
for (var i = 0; i < this.length; i++) {
callback.call(this[i], this, i);
}
return this;
};
Pretty easy, right? Now we can use each
method for other methods. Here is how addClass
and text
methods could look like:
ion.fn.addClass = function(classes) {
return this.each(function() {
this.className += ' ' + classes;
});
};
ion.fn.text = function(str) {
if (str) {
return this.each(function() {
this.innerText = str;
});
}
return this.length && this[0].innerText;
};
Note how these methods return this
instance. This is needed for method chaining, so you can write concise expressions like you are used to doing with jQuery:
ion('.time').addClass('now').text(Date.now());
Take a look at this Demo 2.
Similarly let's also implement one of the most important methods on
. Our simplified version will allow as to bind DOM events, and is going to be built on top of addEventListener
method. It will not support event delegation, but it's really not hard to add. So the implementation:
ion.fn.on = function(name, handler) {
return this.each(function() {
this.addEventListener(name, handler, false);
});
};
Yes, that simple. Of course this is a very primitive implementation, lacking many features, but we are not going to reinvent jQuery.
Now following patterns above we can create any method our library may need. Finally, complete code for this mini-library:
(function(window, undefined) {
// Handle DOMContentLoaded event
var domReadyStack = [];
function handleDOMReady(fn) {
return document.readyState === 'complete' ? fn.call(document) : domReadyStack.push(fn);
}
document.addEventListener('DOMContentLoaded', function onDOMReady() {
document.removeEventListener('DOMContentLoaded', onDOMReady);
while (domReadyStack.length) {
domReadyStack.shift().call(document);
}
});
// ion constructor
function ion(selector) {
if (!(this instanceof ion)) {
return new ion(selector);
}
if (typeof selector === 'function') {
return handleDOMReady(selector);
}
// Number of elements in collection
this.length = 0;
// Nodes collection array
this.nodes = [];
// HTMLElements and NodeLists are wrapped in nodes array
if (selector instanceof HTMLElement || selector instanceof NodeList) {
this.nodes = selector.length > 1 ? [].slice.call(selector) : [selector];
} else if (typeof selector === 'string') {
if (selector[0] === '<' && selector[selector.length - 1] === ">") {
// Create DOM elements
this.nodes = [createNode(selector)];
} else {
// Query DOM
this.nodes = [].slice.call(document.querySelectorAll(selector));
}
}
if (this.nodes.length) {
this.length = this.nodes.length;
for (var i = 0; i < this.nodes.length; i++) {
this[i] = this.nodes[i];
}
}
}
function createNode(html) {
var div = document.createElement('div');
div.innerHTML = html;
return div.firstChild;
}
// Methods
ion.fn = ion.prototype;
ion.fn.each = function(callback) {
for (var i = 0; i < this.length; i++) {
callback.call(this[i], this, i);
}
return this;
};
ion.fn.addClass = function(classes) {
return this.each(function() {
this.className += ' ' + classes;
});
};
ion.fn.removeClass = function(className) {
return this.each(function() {
this.className = this.className.replace(new RegExp('\\b' + className + '\\b', 'g'), '');
});
};
ion.fn.text = function(str) {
if (str) {
return this.each(function() {
this.innerText = str;
});
}
return this.length && this[0].innerText;
};
ion.fn.on = function(name, handler) {
return this.each(function() {
this.addEventListener(name, handler, false);
});
};
window.ion = ion;
})(window);
Take a look at this demonstration of the ion.
As you can see writing a little jQuery-like something can be fun. Of course I don't mean, anyone should try to replace jQuery with such a primitive hack like described in this article, after all jQuery is high quality tool, optimized and is carefully tested to work in many browsers. But I think that it can be a nice workout and also an interesting problem for the interview, to see if candidate actually understands how ultimately libraries like jQuery operate internally.
I will be very thankful for comments and corrections. Stay tuned!
comments powered by Disqus