Search

Pony Foo

Ramblings of a degenerate coder

Uncovering the Native DOM API

(2 comments)reading time: , published

JavaScript libraries such as jQuery serve a great purpose in enabling and normalizing cross-browser behaviors of 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. And 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 happens behind the scenes, in order to better understand what you are doing. Otherwise, you would be just programming by coincidence.

I'll help you explore some of the parts of the DOM API that are usually abstracted away behind a little neat interface in your library of choice. Lets kick off with AJAX.

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
});

How do we write that with native browser-level toothless JavaScript?

We could start by looking it up on MDN. XMLHttpRequest is right on one count. It's for performing requests. But they can manipulate any data, not just XML. They also aren't limited to just the HTTP protocol.

XMLHttpRequest is what makes AJAX sprinkle magic all over rich internet applications nowadays. They are, admitedly, 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 try this in a pen I made here. Before we get into what I actually did in the pen, we should go over the snippet I wrote here, making sure we didn't miss anything.

The .onreadystatechange handler will fire every time xhr.readyState changes, but the only state that's really relevant is 4, a magic number that denotes an XHR request is complete, whatever the outcome was.

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, when 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 the 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 it should be enough to get you going.

Next up, events!

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' JS. 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 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 corner cases are in your wheelhouse, 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.

Moving along.

Event Delegation

What the heck is event delegation?, how am I even supposed to know what it is?

This is the 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.

Now, onto the meat.

raw-meat.jpg

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 for that.

Lets 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. 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 don't particularly understand the internals of Sizzle, but you might want to 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.getElementById, c.getElementsByTagName, c.getElementsByClassName, or one of the querySelector functions. It also fixes inconsistencies in IE8, and 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.

Everyone knows how to add nodes to the DOM, so I won't waste my time on that. Instead, I'll 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.

Given that we've been talking about the DOM for a while, let me introduce you to the dark side of the DOM.

Shadow DOM

A couple of years ago I got introduced to the shadow DOM. I had no idea it existed. You probably don't, either.

In short, the shadow DOM is a part of the DOM that's inaccessible for the most part. JavaScript acts as if there's nothing there, and so does CSS. There are a few browser-specific shadow DOM elements you can style (on certain properties), but interaction with the shadow DOM is very carefully limited in general.

If you've gotten this far, and happen to be looking for a job, this link might help you in your search.

A follow-up to this article can be found here: Getting Over jQuery

Comments(2)

Bagus Javas

Very useful article, Thank you very much! Let me read more posts

Nicolas Bevacqua

Glad you found it useful! Thanks for your feedback