Monday 10 October 2011

Learning JavaScript: From jQuery up

Being someone who's gone from puzzling over DOM scripting to fairly advanced JavaScript I've a decent amount to write about learning it.

Starting with jQuery

jQuery is amazing. It's syntax is so clear and well thought out that you can write JavaScript by coding something that's pretty close to how you would explain what you want to do. For example:

$('img.thumbnail').mouseover(function () { $(this).css('border', 'solid 2px red'); });

Writing code like this is easily readable and, knowing the quality of jQuery's internals, pretty efficient. The issue I have with it is less to do with using jQuery and more to do with its style.

Early in my learning I wrote a lot of code like you see above but the more I had to write, the less I thought that style was the best approach. When the problem you are trying to solve passes a certain complexity, using the above style can create duplication of resources and repetition of actions.

A different approach

The best solution is to start looking at what JavaScript as a language can do to solve these problems.

Let's look at some actual real-world code as a way to look for some solutions. Below is a simple accordian, written using jQuery for most things with a bit of structure for organisation.

// hide all accordion content divs
$('div.accordionContent').hide();

// when you click a tab
$('a.accordionTab').click(function () {
       // 1. Selection of active element every time you click
       var $currentActive = $('a.accordionTab').filter('.active');
 
       // if this tab is active, close it's content div
       if ($currentActive[0] === this) {
              close($(this));
       } else {
            if ($currentActive.length > 0) {
                  close($currentActive);
            }
            open($(this));
       }
       return false;
});

// mark the tab as inactive and hide it's content div
close = function ($el) {
       // 2. DOM traversal & element selection every time this function is run
       $el.removeClass('active')
              .parent()
              .find('.accordionContent')
              .slideUp();
};

open = function ($el) {
       // 2. DOM traversal & element selection every time this function is run
       $el.addClass('active')
              .parent()
              .find('.accordionContent')
              .slideDown();
};

The main problems are, as listed above

  1. Finding the current active tab every time the click event runs through element selection.
  2. Every time open or close are run, it causes DOM traversal and element selection.

The code also runs in the same scope as any other script in the document (which can lead to variables & functions being overwritten or used by accident) when it should really be contained in a single place.

Solutions

  1. Put the whole thing in an object and store that in 1 variable*.
  2. Do your selections once and store the result in variables. That includes selection by DOM traversal.

* This variable should really be stored in a namespace when we are at the production stage.

Pray explain

OK, so in an effort to make this a bit clearer I've stuck the code on Github. Download it now (clone it if you know how to use git, or click the Downloads button and select the .zip).

**Update** Having figured out Git hub pages the code is now more easily accessible here

It doesn't need to be accessed via a server, just open the .html files in your browser and we'll work our way through, starting with base_pattern.html.

base_pattern.html

The JavaScript (js/pattern.js) here is a base pattern with this structure:

All code is contained in one object stored in the pattern variable. That object has a single method called init that you call when you have an element you want to add behaviour to.

Inside pattern is a constructor called Constr.

Constr = function (elm) {
    ...
Every time you run pattern's init method it uses Constr to create an object for each element matched to hold its behaviours.

init : function (context) {
       // 4. Searches are always performed within a context
       if (context === 'undefined') {
           context = document.body;
       }

       // 5. For each matching element, create an object using the Constr constructor 
       $('.accordion', context).each(function () {
           new Constr(this);
       });
}

Notice how searches are always performed inside an context element, even if this is document.body. This means that you can run init, not just on a whole document but also on a sub-section of one (if you replace a sub-section with AJAX for example).

Apart from that the structure we started with is mainly the same. We're still attaching an event to each accordion tab and the logic inside that is using open and close methods to control the accordion content areas.

The main difference is that, thinking a bit more programatically, we are setting all our variables at the top of Constr, including those that hold element selections.

var $elm = $(elm),
    $tabs = $elm.find('.accordionTab'),
    tabIdx = $tabs.length,
    $contentAreas = $elm.find('.accordionContent'),
    activeIdx = $tabs.index($tabs.filter('.'+ activeClass)),
    that = this,
    onClick;

By wrapping everything in pattern we also create a closed scope that means we can define what we like safely.

If you open your browser's Developer tools (in Chrome, Safari or IE9, Firebug in Firefox or Dragonfly in Opera) and type pattern you'll be able to see and inspect the pattern object.

One last efficiency

The pattern is quite nice now. The structure is a nice mapping of the logic that makes the accordion work, variables are all stored and re-used and changes to DOM elements in open and close are just to properties of their jQuery wrappers; no DOM traversal or selection is needed.

It's a bit personal but the last thing that's bugging me now is that at the top of onClick the idx variable is set each time by jQuery looping through the $tabs object which feels a bit inefficient.

onClick = function () {
            var idx = $tabs.index(this);

We are creating an onClick function for all tabs so it would make more sense to give each of these functions access to the index of that tab in $tabs. It is possible to use closure to do this so let's have a go.

base_pattern_with_closure.html

So in the JavaScript for this page (js/pattern_with_closure.html) let's have a look at the new onClick.

// This function uses closure to create a function with access to the idx at the point it is called
onClick = function (idx) {
       // capture each index using a closure
       return function (eventObj) {
              if(activeIdx !== null) {
                     that.close();
              }
              if (idx === activeIdx) {
                     activeIdx = null;
              } else {
                     activeIdx = idx;
                     that.open();
              }
       };
};

So now rather than onClick being a variable containing a function to run on the click event, it now is like a factory, returning a function to do this.

This makes more sense if we look at it's use.

// for each tab, bind a function to its click event which has access to the tab's index
while (tabIdx--) {
       $tabs.eq(tabIdx).bind('click', onClick(tabIdx));
}

The onClick function is now run at the point we bind the event and the function it returns is what fires on that event, not onClick.

When the function it returns is created (at the event binding stage), onClick sends it a single parameter called idx which is the index of the tab in $tabs that was clicked. idx only exists at the point onClick runs but, thanks to closure, the internal function will always have access to it.

Because we use closure we are effectively pushing the effort onto scope resolution rather than looping through an array.

More info

I'm not exaggerating when I say it took me almost a year to 'get' closure after I first came across it. By contrast I once explained it to a colleague (with a lot of experience of heavy programming) and they got it straight away. Depending on your speed of understanding here's a few links to help:

What else?

In the rest of the examples I've tried to explore the different options you have when approaching the problem in this way (see index.html). I'd be very interested in any suggested changes to these examples or to other options so if you can think of any, let me know (or just fork the repositry :).

No comments:

Post a Comment