Jump To …

04-data.js

jQuery Annotated Source.

Home | Previous Chapter | Next Chapter

Chapter 4: Data

var rbrace = /^(?:\{.*\}|\[.*\])$/,
  rmultiDash = /([a-z])([A-Z])/g;

jQuery.extend({
  cache: {},

Please use with caution

  uuid: 0,

Unique for each copy of jQuery on the page Non-digits removed to match rinlinejQuery

  expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ),

The following elements throw uncatchable exceptions if you attempt to add expando properties to them.

  noData: {
    "embed": true,

Ban all objects except for Flash (which handle expandos)

    "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",
    "applet": true
  },

  hasData: function( elem ) {
    elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ];

    return !!elem && !isEmptyDataObject( elem );
  },

  data: function( elem, name, data, pvt /* Internal Use Only */ ) {
    if ( !jQuery.acceptData( elem ) ) {
      return;
    }

    var internalKey = jQuery.expando, getByName = typeof name === "string", thisCache,

We have to handle DOM nodes and JS objects differently because IE6-7 can't GC object references properly across the DOM-JS boundary

      isNode = elem.nodeType,

Only DOM nodes need the global jQuery cache; JS object data is attached directly to the object so GC can occur automatically

      cache = isNode ? jQuery.cache : elem,

Only defining an ID for JS objects if its cache already exists allows the code to shortcut on the same path as a DOM node with no cache

      id = isNode ? elem[ jQuery.expando ] : elem[ jQuery.expando ] && jQuery.expando;

Avoid doing any more work than we need to when trying to get data on an object that has no data at all

    if ( (!id || (pvt && id && !cache[ id ][ internalKey ])) && getByName && data === undefined ) {
      return;
    }

    if ( !id ) {

Only DOM nodes need a new unique ID for each element since their data ends up in the global cache

      if ( isNode ) {
        elem[ jQuery.expando ] = id = ++jQuery.uuid;
      } else {
        id = jQuery.expando;
      }
    }

    if ( !cache[ id ] ) {
      cache[ id ] = {};

TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery metadata on plain JS objects when the object is serialized using JSON.stringify

      if ( !isNode ) {
        cache[ id ].toJSON = jQuery.noop;
      }
    }

An object can be passed to jQuery.data instead of a key/value pair; this gets shallow copied over onto the existing cache

    if ( typeof name === "object" || typeof name === "function" ) {
      if ( pvt ) {
        cache[ id ][ internalKey ] = jQuery.extend(cache[ id ][ internalKey ], name);
      } else {
        cache[ id ] = jQuery.extend(cache[ id ], name);
      }
    }

    thisCache = cache[ id ];

Internal jQuery data is stored in a separate object inside the object's data cache in order to avoid key collisions between internal data and user-defined data

    if ( pvt ) {
      if ( !thisCache[ internalKey ] ) {
        thisCache[ internalKey ] = {};
      }

      thisCache = thisCache[ internalKey ];
    }

    if ( data !== undefined ) {
      thisCache[ jQuery.camelCase( name ) ] = data;
    }

TODO: This is a hack for 1.5 ONLY. It will be removed in 1.6. Users should not attempt to inspect the internal events object using jQuery.data, as this internal data object is undocumented and subject to change.

    if ( name === "events" && !thisCache[name] ) {
      return thisCache[ internalKey ] && thisCache[ internalKey ].events;
    }

    return getByName ? 

Check for both converted-to-camel and non-converted data property names

      thisCache[ jQuery.camelCase( name ) ] || thisCache[ name ] :
      thisCache;
  },

  removeData: function( elem, name, pvt /* Internal Use Only */ ) {
    if ( !jQuery.acceptData( elem ) ) {
      return;
    }

    var internalKey = jQuery.expando, isNode = elem.nodeType,

See jQuery.data for more information

      cache = isNode ? jQuery.cache : elem,

See jQuery.data for more information

      id = isNode ? elem[ jQuery.expando ] : jQuery.expando;

If there is already no cache entry for this object, there is no purpose in continuing

    if ( !cache[ id ] ) {
      return;
    }

    if ( name ) {
      var thisCache = pvt ? cache[ id ][ internalKey ] : cache[ id ];

      if ( thisCache ) {
        delete thisCache[ name ];

If there is no data left in the cache, we want to continue and let the cache object itself get destroyed

        if ( !isEmptyDataObject(thisCache) ) {
          return;
        }
      }
    }

See jQuery.data for more information

    if ( pvt ) {
      delete cache[ id ][ internalKey ];

Don't destroy the parent cache unless the internal data object had been the only thing left in it

      if ( !isEmptyDataObject(cache[ id ]) ) {
        return;
      }
    }

    var internalCache = cache[ id ][ internalKey ];

Browsers that fail expando deletion also refuse to delete expandos on the window, but it will allow it on all other JS objects; other browsers don't care

    if ( jQuery.support.deleteExpando || cache != window ) {
      delete cache[ id ];
    } else {
      cache[ id ] = null;
    }

We destroyed the entire user cache at once because it's faster than iterating through each key, but we need to continue to persist internal data if it existed

    if ( internalCache ) {
      cache[ id ] = {};

TODO: This is a hack for 1.5 ONLY. Avoids exposing jQuery metadata on plain JS objects when the object is serialized using JSON.stringify

      if ( !isNode ) {
        cache[ id ].toJSON = jQuery.noop;
      }

      cache[ id ][ internalKey ] = internalCache;

Otherwise, we need to eliminate the expando on the node to avoid false lookups in the cache for entries that no longer exist

    } else if ( isNode ) {

IE does not allow us to delete expando properties from nodes, nor does it have a removeAttribute function on Document nodes; we must handle all of these cases

      if ( jQuery.support.deleteExpando ) {
        delete elem[ jQuery.expando ];
      } else if ( elem.removeAttribute ) {
        elem.removeAttribute( jQuery.expando );
      } else {
        elem[ jQuery.expando ] = null;
      }
    }
  },

For internal use only.

  _data: function( elem, name, data ) {
    return jQuery.data( elem, name, data, true );
  },

A method for determining if a DOM node can handle the data expando

  acceptData: function( elem ) {
    if ( elem.nodeName ) {
      var match = jQuery.noData[ elem.nodeName.toLowerCase() ];

      if ( match ) {
        return !(match === true || elem.getAttribute("classid") !== match);
      }
    }

    return true;
  }
});

jQuery.fn.extend({
  data: function( key, value ) {
    var data = null;

    if ( typeof key === "undefined" ) {
      if ( this.length ) {
        data = jQuery.data( this[0] );

        if ( this[0].nodeType === 1 ) {
          var attr = this[0].attributes, name;
          for ( var i = 0, l = attr.length; i < l; i++ ) {
            name = attr[i].name;

            if ( name.indexOf( "data-" ) === 0 ) {
              name = jQuery.camelCase( name.substring(5) );

              dataAttr( this[0], name, data[ name ] );
            }
          }
        }
      }

      return data;

    } else if ( typeof key === "object" ) {
      return this.each(function() {
        jQuery.data( this, key );
      });
    }

    var parts = key.split(".");
    parts[1] = parts[1] ? "." + parts[1] : "";

    if ( value === undefined ) {
      data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]);

Try to fetch any internally stored data first

      if ( data === undefined && this.length ) {
        data = jQuery.data( this[0], key );
        data = dataAttr( this[0], key, data );
      }

      return data === undefined && parts[1] ?
        this.data( parts[0] ) :
        data;

    } else {
      return this.each(function() {
        var $this = jQuery( this ),
          args = [ parts[0], value ];

        $this.triggerHandler( "setData" + parts[1] + "!", args );
        jQuery.data( this, key, value );
        $this.triggerHandler( "changeData" + parts[1] + "!", args );
      });
    }
  },

  removeData: function( key ) {
    return this.each(function() {
      jQuery.removeData( this, key );
    });
  }
});

function dataAttr( elem, key, data ) {

If nothing was found internally, try to fetch any data from the HTML5 data-* attribute

  if ( data === undefined && elem.nodeType === 1 ) {
    var name = "data-" + key.replace( rmultiDash, "$1-$2" ).toLowerCase();

    data = elem.getAttribute( name );

    if ( typeof data === "string" ) {
      try {
        data = data === "true" ? true :
        data === "false" ? false :
        data === "null" ? null :
        !jQuery.isNaN( data ) ? parseFloat( data ) :
          rbrace.test( data ) ? jQuery.parseJSON( data ) :
          data;
      } catch( e ) {}

Make sure we set the data so it isn't changed later

      jQuery.data( elem, key, data );

    } else {
      data = undefined;
    }
  }

  return data;
}

TODO: This is a hack for 1.5 ONLY to allow objects with a single toJSON property to be considered empty objects; this property always exists in order to make sure JSON.stringify does not expose internal metadata

function isEmptyDataObject( obj ) {
  for ( var name in obj ) {
    if ( name !== "toJSON" ) {
      return false;
    }
  }

  return true;
}