Uncovering the Native DOM API

By Nicolas Bevacqua

JavaScript libraries such as jQuery serve a great purpose in normalizing cross-browser behaviors in the DOM in such a way that it’s possible to use the same interface to interact with many different browsers. But they do so at a price. Part of that price, in the case of some developers, is having no idea what the heck the library is actually doing when we use it.

Heck, it works! Right? Well, no. You should know what’s happening behind the scenes, in order to better understand what you are doing. Otherwise, you would be just programming by coincidence.

In this article, I’ll explore some of the parts of the DOM API that are usually abstracted away behind a little neat interface in your library of choice, including Ajax, events and DOM querying.

Meet: XMLHttpRequest

Surely you know how to write AJAX requests, right? Probably something like…

$.ajax({
    url: '/endpoint'
}).done(function(data){
    // do something awesome
}).fail(function(xhr){
    // sad little dance
});

But, how do we write that with native browser-level JavaScript?

The name XMLHttpRequest is right on one count in that it performs requests. But the name is misleading in that it can handle any data, not just XML. It also is’t limited to just the HTTP protocol. For full details, check out MDN.

XMLHttpRequest is what drives the AJAX “magic” all over rich internet applications nowadays. Writing these requests can be, admittedly, kind of hard to get right without looking it up, or having prepared to use them for an interview.

Lets give it a first try:

var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function(){
    var completed = 4;
    if(xhr.readyState === completed){
        if(xhr.status === 200){
            // do something with xhr.responseText
        }else{
            // handle the error
        }
    }
};
xhr.open('GET', '/endpoint', true);
xhr.send(null);

You can also test this in a pen I made here. Let’s go over the snippet I wrote here, making sure we don’t miss anything.

The .onreadystatechange handler will fire every time xhr.readyState changes, but the only state that’s really relevant here is 4, which denotes an XHR request is complete, regardless of the outcome.

Once the request is complete, the XHR object will have it’s status filled. If you try to access status before completion, you might get an exception. Lastly, once you know the status of your XHR request, you can do something about it, you should use xhr.responseText to figure out how to react to the response, probably passing that to a callback.

The request is prepared using xhr.open, passing the HTTP method in the first parameter, the resource to query in the second parameter, and a third parameter to decide whether the request should be asynchronous (true) or block the UI thread and make everyone cry (false).

If you also want to send some data, you should pass that to xhr.send. This function actually sends the request and it supports all the signatures below.

void send();
void send(ArrayBuffer data);
void send(Blob data);
void send(Document data);
void send(DOMString? data);
void send(FormData data);

I won’t go into detail, but you’d use those signatures to send data to the server.

A sensible way to wrap our native XHR call in a reusable function might be the following:

function ajax(url, opts){
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function(){
        var completed = 4;
        if(xhr.readyState === completed){
            if(xhr.status === 200){
                opts.success(xhr.responseText, xhr);
            }else{
                opts.error(xhr.responseText, xhr);
            }
        }
    };
    xhr.open(opts.method, url, true);
    xhr.send(opts.data);
}
ajax('/foo', { // usage
    method: 'GET',
    success: function(response){
        console.log(response);
    },
    error: function(response){
        console.log(response);
    }
});

You might want to add default values to the method, success and error options, maybe even use promises, but this should be enough to get you going.

Event Listeners

Lets say you now want to attach that awesome AJAX call to one your DOM elements, that’s ridiculously easy!

$('button').on('click', function(){
    ajax( ... );
});

Sure, you could use jQuery like your life depended on it, but this one is pretty simple to do with “pure” JavaScript. Lets try a reusable function from the get-go.

function add(element, type, handler){
    if (element.addEventListener){
        element.addEventListener(type, handler, false);
    }else if (element.attachEvent){
        element.attachEvent('on' + type, handler); 
    }else{
        // more on this later
    }
}
function remove(element, type, handler){
    if (element.removeEventListener){
        element.removeEventListener(type, handler);
    }else if (element.detachEvent){
        element.detachEvent(type, handler);
    }else{
        // more on this later
    }
}

This one is pretty straightforward, you just add events with either the W3C event model or the IE event model.

The last resort would be to use an element[‘on’ + type] = handler, but this would be very bad because we wouldn’t be able to attach more than one event to each DOM element.

If necessary, we could use a dictionary to keep the handlers in a way that they are easy to add and remove. Then it would be just a matter of calling all of these handlers when an event is fired. This brings a whole host of complications, though:

function(window){
    var events = {}, map = [];
    function add(element, type, handler){
        var key = 'on' + type,
            id = uid(element),
            e = events[id];
        element[key] = eventStorm(element, type);
        if(!e){
            e = events[id] = { handlers: {} };
        }
        if(!e.handlers[type]){
            e.handlers[type] = [];
            e.handlers[type].active = 0;
        }
        e.handlers[type].push(handler);
        e.handlers[type].active++;
    }
    function remove(element, type, handler){
        var key = 'on' + type,
            e = events[uid(element)];
        if(!e || !e.handlers[type]){
            return;
        }
        var handlers = e.handlers[type],
            index = handlers.indexOf(handler);
        // delete it in place to avoid ordering issues
        delete handlers[index];
        handlers.active--;
        if (handlers.active === 0){
            if (element[key]){
                element[key] = null;
                e.handlers[type] = [];
            }
        }
    }
    function eventStorm(element, type){
        return function(){
            var e = events[uid(element)];
            if(!e || !e.handlers[type]){
                return;
            }
            var handlers = e.handlers[type],
                len = handlers.length,
                i;
            for(i = 0; i < len; i++){
                // check the handler wasn't removed
                if (handlers[i]){
                    handlers[i].apply(this, arguments);
                }
            }
        };
    }
    // this is a fast way to identify our elements
    // .. at the expense of our memory, though.
    function uid(element){
        var index = map.indexOf(element);
        if (index === -1){
            map.push(element);
            index = map.length - 1;
        }
        return index;
    }
    window.events = {
        add: add,
        remove: remove
    };
}(window);

You can glance at how this can very quickly get out of hand. Remember this was just in the case of no W3C event model, and no IE event model. Fortunately, this is largely unnecessary nowadays.

You can imagine how hacks of this kind are all over your favorite libraries. They have to be if they want to support the old, decrepit and outdated browsers. Some have been taking steps back from the support every single browser philosophy.

I encourage you to read your favorite library’s code, and learn how they resolve these situations, or how they are written in general.

Event Delegation

What the heck is event delegation?

This is an ever ubiquitous interview question. Yet, every single time I’m asked this question during the course of an interview, the interviewers look surprised that I actually know what event delegation is. Other common interview questions include event bubbling, event capturing, event propagation.

Save the interview questions link in your pocket and read it later to treat yourself to a little evaluation of your front-end development skills. It’s good to know where you’re standing.

Event delegation is what you have to do when you have many elements which need the same event handler. It doesn’t matter if the handler depends on the actual element, because event delegation accomodates that.

Let’s look at a use case. I’ll use the Jade syntax.

body
    ul.foo
        li.bar
        li.bar
        li.bar
        li.bar
    ul.foo
        li.bar
        li.bar
        li.bar
        li.bar

We want, for whatever reason, to attach an event handler to each .foo element. The problem is that event listening is resource consuming. It’s lighter to attach a single event than thousands. Yet, it’s surprisingly common to work in codebases with little to no event delegation.

A better performing approach is to add a super event handler on a node which is a parent to every node that wants to listen to that event using this handler. And then:

  • When the event is raised on one of the children, it bubbles up the DOM chain
  • It reaches the parent node which has our super handler.
  • That special handler will check whether the event target is one of the intended targets
  • Finally the actual handler will be invoked, passing it the appropriate event context.

This is what happens when you bind events using jQuery code such as:

$('body').on('click', '.bar', function(){
    console.log('clicked bar!', $(this));
});

As opposed to more unfortunate code:

$('.bar').on('click', function(){
    console.log('clicked bar!', $(this));
});

Which would work pretty much the same way, except it will create one event handler for each .bar element, hindering performance.

There is one crucial difference, however. Event handling done directly on a node works for just that node, forever. Event delegation works on any children that meet the criteria provided, .bar in this case. If you were to add more .bar elements to your DOM, those would also match the criteria, and therefore be attached to the super handler we created in the past.

I won’t be providing an example on raw JavaScript event delegation, but at least you now understand how it works and what it is, and hopefully, you understood why you need to use it.

We’ve been mentioning selectors such as .bar this whole time, but how does that work?

Querying the DOM

You might have heard of Sizzle, the internal library jQuery uses as a selector engine. I’m not very knowledgeable on all of the internals of Sizzle, but if you are curious, take a look around their codebase For the most part, it uses c.querySelector and c.querySelectorAll. These methods enjoy very good support accross browsers.

Sizzle performs optimizations such as picking whether to use c.getElementByIdc.getElementsByTagNamec.getElementsByClassName or one of the querySelector functions. It also fixes inconsistencies in IE8 as well as some other cross-browser fixes.

Other than that, querying the DOM is pretty much done natively.

Lets turn to manipulation.

DOM Manipulation

Manipulating the DOM is one of those things that is remarkably important to get right, and strikingly easy to get wrong.

I think most people know how to add nodes to the DOM, so I won’t waste my time on that. Instead, I’d like to talk about createDocumentFragment.

document.createDocumentFragment allows us to create a DOM structure that’s not attached to the main DOM tree. This allows us to create nodes that only exist in memory and helps us to avoid DOM reflowing.

Once our tree fragment is ready, we can attach it to the DOM. When we do, all the child nodes in the fragment are attached to the specified node.

var somewhere = document.getElementById('here'),
    fragment = document.createDocumentFragment(),
    i, foo;
for(i = 0, i < 1000; i++){
    foo = document.createElement('div');
    foo.innerText = i;
    fragment.appendChild(foo);
}
somewhere.appendChild(fragment);

Pen here

There’s a cute post on DocumentFragments, written by John Resig, you might want to check out.

Conclusion

Hopefully this post has given you some understanding of what is going on under the covers in some of the core aspects of popular libraries, like jQuery. This knowledge can be very useful not just when building your web application, but especially when debugging issues.

This article was originally published at https://blog.ponyfoo.com/2013/06/10/uncovering-the-native-dom-api

Previous

Drawing and Animating with Two.js and Illustrator

The Future of JavaScript…Now!

Next

6 thoughts on “Uncovering the Native DOM API”

  1. I get completely vexed when I meet developers who only know jQuery. jQuery (and other libraries) should be something you learn once you already know Javascript. Adding events, AJAX requests, selecting elements who have IDs are all very easily done with vanilla JS and save the browser a lot of overhead.

    Articles like this are good, as hopefully some will be able to take a step back to learn what they should’ve already known when discovering jQuery.

  2. This is a great article! Its one of the ways I’ve learned native JavaScript without depending on jQuery for everything. When I started, I started by using jQuery. But as soon as my curiosity kicked in, I started looking into jQuery’s source and learning how they did what they did – helping me truly learn the language.

    The problem that I usually see is balancing this out with real-world demands. As developers, engineers and programmer we should definitely strive to learn the language and become experts. But we also need to balance this with the needs of the company and/or business. And usually you need to use the tools that currently exist in order to deliver.

    Most of the time I struggle with this myself; should I code my own ajax calls or depend on jQuery? This all depends on the project, team and needs of the company/business. I find that I can do this in my own personal projects, but not really in the day-to-day grind. But I do agree we should aim to push this kind of knowledge and effort in our community.

  3. “Otherwise, you would be just programming by coincidence.” – I think that’s going a bit too far. If I want to fade out some element when it’s clicked and I code $(“#someElement”).on(“click”…) with a call to .fadeOut() it’s hardly a coincident when it works. I don’t need to know all of the internals of jQuery to make good use of the library.

    I use jQuery because it makes my job easier, not because I couldn’t possibly get the same job done without it. For years I did get the job done without it because it didn’t exist yet… For me as a programmer I’m interested in the internals and even before browsing through some of the jQuery source I could make educated guesses at what it was doing, but I do understand that there are, e.g., graphic designers who’ve been told by their boss to “fade out some element” and they’re just doing the best they can.

    Having said that, I do believe that it’s it’s ridiculous to try to use any JS library without first understanding the basics of JavaScript – at minimum basic syntax, operators, common functions, etc. I often see questions on programming forums complaining that “[library] method xyz is broken” when really the poster has made no effort to read the library’s documentation and has also made some basic syntax errors that even a mediocre JS programmer should be able to identify.

    And I do agree that it is useful to understand how a chosen library works in a general sense, so that (to give a random jQuery example) a programmer might know that a performance problem with a particular piece of code can be alleviated by replacing an .each() loop with a standard for loop.

  4. You can also use `addEventListener` with XHR thanks to XHR2 (the one supporting CORS).

    It simplifies a lot the code because you can write the following:

    “`javascript
    xhr.addEventListener(“load”, opts.success);
    xhr.addEventListener(“error”, opts.error);
    “`

    The first argument will be a progress event.

Comments are closed.