官术网_书友最值得收藏!

  • jQuery Design Patterns
  • Thodoris Greasidis
  • 5654字
  • 2021-07-16 12:52:26

Introducing the Observer Pattern

The key concept of the Observer Pattern is that there is an object, often referred to as the observable or the subject, whose internal state changes during its lifetime. There are also several other objects, referred as the observers, that want to be notified in the event that the state of the observable/subject changes, in order to execute some operations.

The observers may need to be notified about any kind of state change of the observable or only specific types of changes. In the most common implementation, the observable maintains a list with its observers and notifies them when an appropriate state change occurs. In case a state change occurs to the observable, it iterates through the list of observers that are interested for that type of state change and executes a specific method that they have defined.

According to the definition of the Observer Pattern and the reference implementation in Computer Science books, the observers are described as objects that implement a well-known programming interface, in most cases, specific to each observable they are interested in. In the case of a state change, the observable will execute the well-known method of each observer as it is defined in the programming interface.

Note

For more information on how the Observer Pattern is used in traditional, object-oriented programming, you can visit http://www.oodesign.com/observer-pattern.html.

In the web stack, the Observer Pattern often uses plain anonymous callback functions as observers instead of objects with well-known methods. An equivalent result, as defined by the Observer Pattern, can be achieved since the callback function keeps references to the variables of the environment that it was defined in—a pattern commonly referenced as a Closure. The main benefit of using the Observer Pattern over callbacks as invocation or initialization parameters is that the Observer Pattern can support several independent handlers on a single target.

Note

For more information on closures, you can visit https://developer.mozilla.org/en-US/docs/Web/JavaScript/Closures.

Tip

Defining a simple callback

A callback can be defined as a function that is passed as an argument to another function/method or is assigned to a property of an object and expected to be executed at some later point of time. In this way, the piece of code that was handed our callback will invoke or call it, propagating the results of an operation or event back to the context where the callback was defined.

Since the pattern of registering functions as observers has proven to be more flexible and straightforward to program, it can be found in programming languages outside the web stack as well. Other programming languages provide an equivalent functionality through language features or special objects such as subroutines, lambda expressions, blocks, and function pointers. For example, Python also defines functions as first-class objects such as JavaScript, enabling them to be used as callbacks, while C# defines Delegates as a special object type in order to achieve the same result.

The Observer Pattern is an integral part of developing web interfaces that respond to user actions, and every web developer has used it to some degree, even without noticing it. This is because the first thing that a web developer needs to do while creating a rich user interface is to add event listeners to page elements and define how the browser should respond to them.

This is traditionally achieved by using the EventTarget.addEventListener() method on the page elements that we need to listen to for events such as a "click", and providing a callback function with the code that needs to be executed when that event occurs. It is worth mentioning that in order to support older versions of Internet Explorer, testing for the existence of EventTarget.attachEvent(), and using that instead, is required.

Note

For more information on the addEventListener() and attachEvent() methods, you can visit https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener and https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/attachEvent.

How it is used by jQuery

The jQuery library heavily uses the Observer Pattern in several parts of its implementation, either directly by using the addEventListener method or creating its own abstraction over it. Moreover, jQuery offers a series of abstractions and convenient methods to make working with the Observer Pattern easier on the web and also uses some of them internally to implement other methods as well.

The jQuery on method

The jQuery.fn.on() method is the central jQuery method for attaching event handlers to elements, providing an easy way to adopt the Observer Pattern, while keeping our code easy to read and reason. It attaches the requested event handler over all the elements of a composite jQuery collection object returned by the $() function.

Searching for jQuery.fn.on in jQuery's Source Viewer (which is available at http://james.padolsey.com/jquery), or directly searching jQuery's source code for on: function (the first character is a tab), will lead us to the method's definition, which counts 67 lines of code. Actually, the first 55 lines of the internal on function are just handling all the different ways that the jQuery.fn.on() method can be invoked; near its end, we can see that it actually uses the internal method jQuery.event.add():

jQuery.fn.extend({
  on: function( types, selector, data, fn ) {
    return on( this, types, selector, data, fn );
  }
});

function on( elem, types, selector, data, fn, one ) {

  /* 55 lines of code handling the method overloads */
  return elem.each( function() {
    jQuery.event.add( this, types, fn, data, selector );
  } );
}

The jQuery.event object is the one-place stop for event handling in jQuery and its implementation counts around 443 lines of code. It holds several helper functions for managing events such as add, dispatch, fix, handlers, remove, simulate, and trigger. All these functions are used internally by jQuery itself wherever the Observer Pattern appears or managing events is required.

Searching for jQuery.event.add in jQuery's Source Viewer or jQuery.event = directly in jQuery's source code, will lead us to the relatively long implementation of the helper function that counts around 107 lines of code in jQuery v2.2.0. The following code snippet shows a trimmed down version of that method, where some code related to the technical implementation of jQuery and not related to the Observer Pattern has been removed for clarity:

add: function( elem, types, handler, data, selector ) { 
    /* ... 4 lines of code ... */
        elemData = dataPriv.get( elem ); 
    /* ... 13 lines of code ... */

    // Make sure that the handler has a unique ID, 
    // used to find/remove it later 
 if ( !handler.guid ) { 
 handler.guid = jQuery.guid++; 
 } 

    // Init the element's event structure and main handler, 
    // if this is the first 
 if ( !( events = elemData.events ) ) { 
 events = elemData.events = {}; 
 } 
    /* ... 9 lines of code ... */ 

    // Handle multiple events separated by a space 
    types = ( types || "" ).match( rnotwhite ) || [ "" ]; 
    t = types.length; 
    while ( t-- ) { 
        /* ... 30 lines of code ... */ 

        // Init the event handler queue if we're the first 
        if ( !( handlers = events[ type ] ) ) { 
 handlers = events[ type ] = []; 
            handlers.delegateCount = 0; 

            // Only use addEventListener if the special events handler
            // returns false 
            if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
 if ( elem.addEventListener ) { 
 elem.addEventListener( type, eventHandle ); 
 } 
            } 
        }

        /* ... 9 lines of code ... */ 

        // Add to the element's handler list, delegates in front 
 if ( selector ) { 
 handlers.splice( handlers.delegateCount++, 0, handleObj ); 
 } else { 
 handlers.push( handleObj ); 
 }
        /* ... 3 lines of code ... */
    } 
}

Now, let's see how the Observer Pattern is implemented by jQuery.event.add(), by referring to the preceding highlighted code.

The handler variable in the arguments of the jQuery.event.add() method stores the function that was originally passed as an argument to the jQuery.fn.on() method. We can refer to this function as our observer function, since it is executed when the appropriate event fires on the element that it was attached to.

In the first highlighted code area, jQuery creates and assigns a guid property to the observer function that is stored in the handler variable. Keep in mind that assigning properties to functions is possible in JavaScript, since functions are first-class objects. The jQuery.guid++ statement is executed right after the assignment of the old value and is required since jQuery.guid is a page-wide counter used by jQuery and jQuery plugins internally. The guid property on the observer function is used as a way to identify and locate the observer function inside the observer list that jQuery has for each element. For example, it is used by the jQuery.fn.off() method to locate and remove an observer function from the observer list associated with an element.

Tip

jQuery.guid is a page-wide counter that is used by the plugins and jQuery itself as a centralized way to retrieve unique integer IDs. It is often used to assign unique IDs to elements, objects, and functions, in order to make it easier to locate them in collections. It is the responsibility of each implementer that retrieves and uses the current value of jQuery.guid to also increase the property value (by one) after each use. Otherwise, and since this is a page-wide counter that is used by both jQuery plugins and jQuery themselves for identification, the page will probably face malfunctions that are hard to debug.

In the second and third highlighted code areas, jQuery initializes an array to hold the observer lists for each inpidual event that may fire on that element. One thing to note in the second highlighted code area is that the observer lists found in the elemData variable are not a property on the actual DOM element. As shown in the dataPriv.get( elem ) statement, near the start of the jQuery.event.add() method, jQuery uses separate mapping objects to hold the associations between DOM elements and their observer lists. By using this data cache mechanism, jQuery is able to avoid polluting the DOM elements with the extra properties that are needed by its implementation.

Note

You can easily locate the data cache mechanism implementation in the source code of jQuery by searching for function Data(). This will bring you to the constructor function of the Data class that is also followed by the implementation of the class methods that are defined in the Data.prototype object. For more information, you can visit http://api.jquery.com/data.

The next highlighted code area is where jQuery checks whether the EventTarget.addEventListener() method is actually available for that element and then uses it to add the event listener to the element. In the final highlighted code area, jQuery adds the observer function to its internal list, which holds all the observers of the same event type that are attached to that specific element.

Note

Depending on the version you are using, you might get different results to some degree. The most recent stable jQuery version released and used as reference while writing this book was v2.2.0.

In case you need to provide support for older browsers, for example, Internet Explorer lower than version 9, then you should use the v1.x versions of jQuery. The latest version as of the writing of this book was v1.12.0, which offers the exact same API as the v2.2.x versions, but also has the required code to work on older browsers.

In order to cover the implementation inconsistencies of older browsers, the implementation of jQuery.event.add() in jQuery v1.x is a bit longer and more complex. One of the reasons for this is because jQuery also needs to test whether EventTarget.addEventListener() is actually available in the browser that it is running and try to use EventTarget.attachEvent() if this is not the case.

As we saw in the preceding code, the jQuery implementation follows the operation model that the Observer Pattern describes, but it also incorporates some implementation tricks in order to make it work more efficiently with the APIs available to web browsers.

The document-ready observer

Another convenient method that jQuery offers, which is widely used by developers, is the $.fn.ready() method. This method accepts a function parameter and executes it only after the DOM tree of the page has been fully loaded. Such a thing can be useful in case your code is not loaded last in the page and you don't want to block the initial page render, or the elements that it needs to manipulate are defined later than its own <script> tag.

Note

Keep in mind that the $.fn.ready() method works slightly differently than the window.onload callback and the "load" event of the page, which wait until all the resources of the page are loaded. For more information, you can visit http://api.jquery.com/ready.

The following code demonstrates the most common way to use the $.fn.ready() method:

$(document).ready(function() {
    /* this code will execute only after the page has been fully loaded */ 
})

If we try to locate the implementation of jQuery.fn.ready, we will see that it actually uses jQuery.ready.promise internally to work:

jQuery.fn.ready = function( fn ) { 
  // Add the callback 
  jQuery.ready.promise().done( fn ); 

  return this; 
};
/* … a lot lines of code in between */
jQuery.ready.promise = function( obj ) { 
  if ( !readyList ) { 

    readyList = jQuery.Deferred(); 


    // Catch cases where $(document).ready() is called
    // after the browser event has already occurred.
    // Support: IE9-10 only
    // Older IE sometimes signals "interactive" too soon
    if ( document.readyState === "complete" || ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) {
      // Handle it asynchronously to allow ... to delay ready 
      window.setTimeout( jQuery.ready ); 

    } else { 
      // Use the handy event callback 
 document.addEventListener( "DOMContentLoaded", completed ); 

      // A fallback to window.onload, that will always work 
 window.addEventListener( "load", completed ); 
    } 
  } 
  return readyList.promise( obj ); 
};

As you can see in the preceding highlighted code areas of the implementation, jQuery uses addEventListener to observe when the DOMContentLoaded event is fired on the document object. Moreover, to ensure that it will work across a wide range of browsers, it also observes for the load event to be fired on the window object.

The jQuery library also provides shorter methods to add the above functionality in your code. Since the aforementioned implementation does not actually need a reference to the document, we can instead just write $().ready(function() {/* ... */ }). There also exists an overload of the $() function that achieves the same result, which is used like $(function() {/* ... */ }). These two alternative ways to use jQuery.fn.ready have been heavily criticized among developers, since they commonly lead to misunderstandings. The second, shorter version in particular can lead to confusion, since it looks like an Immediately Invoked Function Expression (IIFE), a pattern that JavaScript developers use heavily and have learned to recognize. In fact, it only differs by one character ($) and as a result, its use is not suggested before a discussion with the rest of your developer team.

Note

The $.fn.ready() method is also characterized as a method that provides an easy way to implement the Lazy Initialization/Execution Pattern in our code. The core concept of this pattern is to postpone the execution of a piece of code or load a remote resource at a later point of time. For example, we can wait for the page to be fully loaded until we add our observers or wait for a certain event to happen before downloading a web resource.

Demonstrate a sample use case

In order to see the Observer Pattern in action, we will create an example showcasing a skeleton implementation of a dashboard. In our example, the user will be able to add information boxes to his dashboard related to some sample items and categories that are available for selection on the header.

Our example will have three predefined categories for our items: Products, Sales, and Advertisements. Each of these categories will have a series of related items that will appear in the area right below the category selector. The user will be able to select the desired category by using a drop-down selector and this will change the visible selection items of the dashboard.

Our dashboard will initially contain a hint information box about the dashboard usage. Whenever a user clicks on one of the category items, a new information box will appear in our three-column layout dashboard. In the preceding image, the user has added two new information boxes for Product B and Product D by clicking on the associated buttons.

The user will also be able to dismiss any of these information boxes by clicking on a red close button on the top-right of each information box. In the preceding image, the user dismissed the Product D information box, then added information boxes for the Advertisement 3 and later the 1st, 2nd, and 3rd week items of the Sales category.

By just reading the above description, we can easily isolate all the user interactions that are required for the implementation of our dashboard. We will need to add observers for each one of these user interactions and write code inside the callback functions that execute the appropriate DOM manipulations.

In detail, our code will need to:

  • Observe changes done to the currently selected element and respond to such event by hiding or revealing the appropriate items
  • Observe the clicks on each item button and respond by adding a new information box
  • Observe the clicks on the close button of each information box and respond by removing it from the page

Now let's proceed and review the HTML, CSS, and JavaScript code required for the preceding example. Let's start with the HTML code and for reference, let's say that we saved it in a file named Dashboard Example.html, as follows:

<!DOCTYPE html> 
<html> 
  <head> 
    <title>Dashboard Example</title> 
    <link rel="stylesheet" type="text/css" href="dashboard-example.css"> 
  </head> 
  <body> 
    <h1 id="pageHeader">Dashboard Example</h1> 

    <p class="dashboardContainer"> 
      <section class="dashboardCategories"> 
        <select id="categoriesSelector"> 
          <option value="0" selected>Products</option> 
          <option value="1">Sales</option> 
          <option value="2">Advertisements</option> 
        </select> 
        <section class="dashboardCategory"> 
          <button>Product A</button> 
          <button>Product B</button> 
          <button>Product C</button> 
          <button>Product D</button> 
          <button>Product E</button> 
        </section> 
        <section class="dashboardCategory hidden"> 
          <button>1st week</button> 
          <button>2nd week</button> 
          <button>3rd week</button> 
          <button>4th week</button> 
        </section> 
        <section class="dashboardCategory hidden"> 
          <button>Advertisement 1</button> 
          <button>Advertisement 2</button> 
          <button>Advertisement 3</button> 
        </section> 
        <p class="clear"></p> 
      </section> 

      <section class="boxContainer"> 
        <p class="boxsizer"> 
          <article class="box"> 
            <header class="boxHeader"> 
              Hint! 
              <button class="boxCloseButton">&#10006;</button> 
            </header> 
            Press the buttons above to add information boxes... 
          </article> 
        </p> 
      </section> 
      <p class="clear"></p> 
    </p> 

    <script type="text/javascript" src="jquery.js"></script> 
    <script type="text/javascript" src="dashboard-example.js">
    </script> 
  </body> 
</html>

In the preceding HTML, we placed all our dashboard-related elements inside a <p> element with the dashboardContainer CSS class . This will enable us to have a centric starting point to search for our dashboard's elements and also scope our CSS. Inside it, we define two <section> elements in order to pide the dashboard into logical areas using some HTML5 semantic elements.

The first <section> with the dashboardCategories class is used to hold the categories selector of our dashboard. Inside it, we have a <select> element with the ID categoriesSelector that is used to filter the visible category items and three subsections with the dashboardCategory class that are used to wrap the <button> elements that will populate the dashboard with information boxes when clicked. Two of them also have the hidden class so that only the first one is visible when the page loads by matching the initially selected option (<option>) of the category selector. Also, at the end of the first section, we also added a <p> with the clear class that, as we saw in the first chapter, will be used to clear the floated <button> elements.

The second <section> with the boxContainer class is used to hold the information boxes of our dashboard. Initially, it contains only one with a hint about how to use the dashboard. We use a <p> element with the boxsizer class to set the box dimensions and an HTML5 <article> element with the box class to add the required border padding and shadow, similar to the box elements from the first chapter.

Each information box, besides its content, also contains a <header> element with the boxHeader class and a <button> element with the boxCloseButton class that, when clicked, removes the information box that contains it. We also used the &#10006; HTML character code as the button's content in order to get a better-looking "x" mark and avoid using a separate image for that purpose.

Lastly, since the information boxes are also floated, we also need a <p> with the clear class at the end of the boxContainer.

In the <head> of the preceding HTML, we also reference a CSS file named as dashboard-example.css with the following content:

.dashboardCategories { 
    margin-bottom: 10px; 
} 

.dashboardCategories select, 
.dashboardCategories button { 
    display: block; 
    width: 200px; 
    padding: 5px 3px; 
    border: 1px solid #333; 
    margin: 3px 5px; 
    border-radius: 3px; 
    background-color: #FFF; 
    text-align: center; 
    box-shadow: 0 1px 1px #777; 
    cursor: pointer; 
} 

.dashboardCategories select:hover, 
.dashboardCategories button:hover { 
    background-color: #DDD; 
} 

.dashboardCategories button { 
    float: left; 
} 

.box { 
    padding: 7px 10px; 
    border: solid 1px #333; 
    margin: 5px 3px; 
    box-shadow: 0 1px 2px #777; 
} 

.boxsizer { 
    float: left; 
    width: 33.33%; 
} 

.boxHeader { 
    padding: 3px 10px;
    margin: -7px -10px 7px;
    background-color: #AAA; 
    box-shadow: 0 1px 1px #999; 
} 

.boxCloseButton { 
    float: right; 
    height: 20px; 
    width: 20px; 
    padding: 0; 
    border: 1px solid #000; 
    border-radius: 3px; 
    background-color: red; 
    font-weight: bold; 
    text-align: center; 
    color: #FFF; 
    cursor: pointer; 
} 

.clear { clear: both; } 
.hidden { display: none; }

As you can see in our CSS file, first of all we add some space below the element with the dashboardCategories class and also define the same styling for the <select> element and the buttons inside it. In order to differentiate it from the default browser styling, we add some padding, a border with rounded corners, a different background color when hovering the mouse pointer, and some space in between them. We also define that our <select> element should be displayed alone in its row as a block and that the category item buttons should float next to each other. We again use the boxsizer and box CSS classes, as we did in Chapter 1, A Refresher on jQuery and the Composite Pattern; the first one to create a three-column layout and the second one to actually provide the styling of an information box. We continue by defining the boxHeader class that is applied to the <header> elements of our information boxes, and define some padding, a grey background color, a light shadow, and also some negative margins so that it counterbalances the effect of the box's paddings and places itself next to its border.

To complete the styling of the information boxes, we also define the boxCloseButton CSS class that (i) floats the box's close buttons to the upper-right corner inside the box <header>, (ii) defines a 20px width and height, (iii) overrides the default browser's <button> styling to zero padding, and (iv) adds a single-pixel black border with rounded corners and a red background color. Lastly, like in Chapter 1, A Refresher on jQuery and the Composite Pattern we define the clear utility CSS class to prevent the element from being placed next to the previous floating elements and also define the hidden class as a convenient way of hiding elements of the page.

In our HTML file, we reference the jQuery library itself and also a JavaScript file named as dashboard-example.js that contains our dashboard implementation. Following the best practices of creating performant web pages, we have placed them right before the </body> tag, in order to avoid delaying the initial page rendering:

$(document).ready(function() { 

    $('#categoriesSelector').change(function() { 
        var $selector = $(this); 
        var selectedIndex = +$selector.val(); 
        var $dashboardCategories = $('.dashboardCategory'); 
        var $selectedItem = $dashboardCategories.eq(selectedIndex).show(); 
        $dashboardCategories.not($selectedItem).hide();
    }); 

    function setupBoxCloseButton($box) { 
        $box.find('.boxCloseButton').click(function() { 
            $(this).closest('.boxsizer').remove(); 
        }); 
    } 

    // make the close button of the hint box work 
    setupBoxCloseButton($('.box')); 

    $('.dashboardCategory button').on('click', function() { 
        var $button = $(this); 
        var boxHtml = '<p class="boxsizer"><article class="box">' + 
                '<header class="boxHeader">' + 
                    $button.text() + 
                    '<button class="boxCloseButton">&#10006;' + 
                    '</button>' + 
                '</header>' + 
                'Information box regarding ' + $button.text() + 
            '</article></p>'; 
        $('.boxContainer').append(boxHtml); 
        setupBoxCloseButton($('.box:last-child')); 
    });

}); 

We have placed all our code inside a $(document).ready() call, in order to delay its execution until the DOM tree of the page is fully loaded. This would be absolutely required if we placed our code in the <head> element, but it is also a best practice that is good to follow in any case.

We first add an observer for the change event on the categoriesSelector element using the `$.fn.change()` method, which is actually a shorthand method for the $.fn.on('change', /* … */) method. In jQuery, the value of the this keyword inside a function that is used as an observer holds a reference to the DOM element that the event was fired. This applies to all jQuery methods that register observers, from the core $.fn.on() to the $.fn.change() and $.fn.click() convenient methods. So we use the $() function to make a jQuery object with the <select> element and store it in the $selector variable. Then, we use $selector.val() to retrieve the value of the selected <option> and cast it to a numeric value by using the + operator. Right after this, we retrieve the <section> elements of dashboardCategory and cache the result to the $dashboardCategories variable. Then, we proceed by finding and revealing the category whose position is equal to the value of the selectedIndex variable and also store the resulting jQuery object to the $selectedItem variable. Finally, we are using the $selectedItem variable with the $.fn.not() method to retrieve and hide all the category elements, except from the one we just revealed.

In the next code section, we define the setupBoxCloseButton function that will be used to initialize the functionality of the close button. It expects a jQuery object with the box elements as a parameter, and for each of them, searches their descendants for the boxCloseButton CSS class that we use on the close buttons. Using $.fn.click(), which is a convenient method for $.fn.on('click', /* fn */), we register an anonymous function to be executed whenever a click event is fired that uses the $.fn.closest() method to find the first ancestor element with the boxsizer class and removes it from the page. Right after this, we call this function once for the box elements that already existed in the page at the time when the page was loaded. In this case, the box element with the usage hint.

Note

An extra thing to keep in mind when using the $.fn.closest() method is that it begins testing the given selector from the current element of the jQuery collection before proceeding with its ancestor elements. For more information, you can visit its documentation at http://api.jquery.com/closest.

In the final code section, we use the $.fn.on() method to add an observer for the click event on each of the category buttons. In this case, inside the anonymous observer function, we use the this keyword, which holds the DOM element of the <button> that was clicked, and use the $() method to create a jQuery object and cache its reference in the $button variable. Right after this, we retrieve the button's text content using the $.fn.text() method and along with it, construct the HTML code for the information box. For the close button, we use the &#10006 HTML character code that will be rendered as a prettier "X" icon. The template we created is based on the HTML code of the initially visible hint box; for the needs of this chapter's example, we use plain string concatenation. Lastly, we append the generated HTML code for our box to the boxContainer, and since we expect it to be the last element, we use the $() function to find it and provide it as a parameter to the setupBoxCloseButton.

How it is compared with event attributes

Before the EventTarget.addEventListener() was defined in the DOM Level 2 Events specification, the event listeners were registered either by using the event attributes that are available for HTML elements or the element event properties that are available for DOM nodes.

Note

For more information on the DOM Level 2 Event specification and event attributes, you can visit http://www.w3.org/TR/DOM-Level-2-Events and https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Event_attributes, respectively.

The event attributes are a set of attributes that are available to HTML elements and provide a declarative way of defining pieces of JavaScript code (preferably function calls) that should be executed when a specific event is triggered on that element. Because of their declarative nature and how simply they can be used, this is often the first way that new developers get introduced to events in web development.

If we used event attributes in the above example, then the HTML code for the close buttons in the information boxes will look as follows:

<article class="box"> 
    <header class="boxHeader"> 
        Hint! 
 <button onclick="closeInfoBox();" 
                class="boxCloseButton">&#10006;</button> 
    </header> 
    Press the buttons above to add information boxes... 
</article>

Also, we should change the template that is used to create new information boxes and expose the closeInfoBox function on the window object, in order for it to be accessible from the HTML event attribute:

window.closeInfoBox = function() { 
    $(this).closest('.boxsizer').remove(); 
};

Some of the disadvantages of using event attributes over the Observer Pattern are:

  • It makes it harder to define multiple separate actions that have to be executed when an event fires on an element
  • It makes the HTML code of the page bigger and less readable
  • It is against the separation of concerns principle, since it adds JavaScript code inside our HTML, possibly making a bug harder to track and fix
  • Most of the time, it leads to the functions being called in the event attribute getting exposed to the global window object, thereby "polluting" the global namespace

Using the element event properties would not require any changes to our HTML, keeping all the implementation in our JavaScript files. The changes required in our setupBoxCloseButton function will make it look as follows:

function setupBoxCloseButton($box) { 
    var $closeButtons = $box.find('.boxCloseButton'); 
    for (var i = 0; i < $closeButtons.length; i++) { 
        $closeButtons[i].onclick = function() { 
 this.onclick = null; 
            $(this).closest('.boxsizer').remove(); 
        }; 
    } 
}

Note that, for convenience, we are still using jQuery for DOM manipulations, but the resulting code still has some of the aforementioned disadvantages. More importantly, in order to avoid memory leaks, we are also required to remove the function assigned to the onclick property before removing the element from the page, if it contains references to the DOM element that it is applied on.

Using the tools that today's browsers offer, we can even match the convenience that the declarative nature of event attributes offers. In the following image, you can see how the Firefox developer tools provide us with helpful feedback when we use them to inspect a page element that has an event listener attached:

As you can see in the preceding image, all the elements that have observers attached also have an ev sign right next to them, which when clicked, displays a dialog showing all the event listeners that are currently attached. To make our developing experience even better, we can directly see the file and the line that these handlers were defined in. Moreover, we can click on them in order to expand and reveal their code, or click on the sign in front of them to navigate to their source and add breakpoints.

One of the biggest benefits of using the Observer Pattern over event attributes is clearly visible in the case where we need to take more than one action when a certain event happens. Suppose that we also need to add a new feature in our example dashboard, which would prevent a user from accidentally double-clicking a category item button and adding the same information box twice to the dashboard. The new implementation should ideally be completely independent from the existing one. Using the Observer Pattern, all we need to do is add the following code that observes for button clicks and disables that button for 700 milliseconds:

$(document).ready(function() { 
  $('.dashboardCategory button').on('click', function() { 
    var $button = $(this); 
    $button.prop('disabled', true); 
    
    setTimeout(function() { 
      $button.prop('disabled', false); 
    }, 700); 
  }); 
});

The preceding code is indeed completely independent from the basic implementation and we could place it inside the same or a different JS file and load it to our page. This would be more difficult when using event attributes, since it would require us to define both actions at the same time inside the same event handler function; as a result, it would strongly couple the two independent actions.

Avoid memory leaks

As we saw earlier, there are some strong advantages of using the Observer Pattern to handle events on a web page. When using the EventTarget.addEventListener() method to add an observer to an element, we also need to keep in mind that in order to avoid memory leaks, we also have to call the EventTarget.removeEventListener() method before removing such elements from the page so that the observers are also removed.

Note

For more information on removing event listeners from elements, you can visit https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener, or for the jQuery equivalent method, visit http://api.jquery.com/off/.

The jQuery library developers understood that such an implementation concern could easily be forgotten or not handled properly, thereby making the adoption of the Observer Pattern look more complex, so they decided to encapsulate the appropriate handling inside the jQuery.event implementation. As a result, when using any event handling jQuery method, such as the core $.fn.on() or any of the convenient methods such as $.fn.click() or $.fn.change(), the observer functions are tracked by jQuery itself and are properly unregistered if we later decide to remove the element from the page. As we saw earlier in the implementation of jQuery.event, jQuery stores a reference to the observers of each element in a separate mapping object. Every time we a use a jQuery method that removes DOM elements from the page, it first makes sure to remove any observers attached to those elements or any of the descendant elements, by checking the mapping object. As a result, the example code we used earlier is not causing memory leaks even though we are not using any method that explicitly removes the observers we add to the created elements.

Tip

Be careful when mixing jQuery and plain DOM manipulations

Even though all jQuery methods keep you safe from memory leaks caused from observers that are never unregistered, keep in mind it can't protect you if you remove elements using plain methods from the DOM API. If methods such as Element.remove() and Element.removeChild() are used and the removed elements or their descendants have observers attached, then they are not going to be unregistered automatically. The same applies when assigning to the Element.innerHTML property.

主站蜘蛛池模板: 墨竹工卡县| 菏泽市| 江津市| 巧家县| 永新县| 瑞昌市| 车险| 滕州市| 巴林左旗| 库尔勒市| 沂源县| 常德市| 滨海县| 通山县| 衡东县| 安泽县| 定日县| 衢州市| 安多县| 女性| 清水县| 宜黄县| 县级市| 特克斯县| 金平| 邳州市| 锦州市| 运城市| 嘉定区| 南宫市| 娄烦县| 神池县| 和田市| 崇阳县| 沁水县| 莫力| 德令哈市| 德江县| 永定县| 阳曲县| 寻甸|