Understanding Zepto's $ Function Implementation

Introduction

After laying the groundwork in previous discussions, we now dive into the core of Zepto — the $ function. This symbol is central to using Zepto, serving as the entry point for DOM selection, manipulation, and event handling. In this article, we'll dissect how Zepto implements $, exploring its internal logic and performance optimizations.

Source Version

The analysis here is based on Zepto version 1.2.0.

DOM Selection with zepto.qsa

One of the primary uses of $ is selecting elements from the DOM. This functionality relies heavily on zepto.qsa, a utility that intelligently chooses the most efficient native method for querying elements.

Code

zepto.qsa = function(element, selector) {
  var found,
      maybeId = selector[0] === '#',
      maybeClass = !maybeId && selector[0] === '.',
      nameOnly = maybeId || maybeClass ? selector.slice(1) : selector,
      isSimpleSelector = /^[\w-]*$/.test(nameOnly);

  return (element.getElementById && isSimpleSelector && maybeId) ?
    (found = element.getElementById(nameOnly)) ? [found] : [] :
    (element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11) ? [] :
    slice.call(
      isSimpleSelector && !maybeId && element.getElementsByClassName ?
        maybeClass ?
          element.getElementsByClassName(nameOnly) :
          element.getElementsByTagName(selector) :
        element.querySelectorAll(selector)
    );
};

Key Variables

  • maybeId: Checks if the selector starts with #, indicating an ID query.
  • maybeClass: Determines if it’s a class selector (starts with .).
  • nameOnly: Strips the leading # or . for cleaner matching.
  • isSimpleSelector: Uses /^[\w-]*$/ to verify if the selector is a simple token (e.g., myclass, not .parent .child).

Optimization Strategy

Rather than relying solely on querySelectorAll, Zepto prioritizes faster native methods when possible:

  • ID selectors: Use getElementById when applicable — fastest for single-element lookup.
  • Class selectors: Prefer getElementsByClassName for better performance over general queries.
  • Tag selectors: Use getElementsByTagName for direct tag name matches.
  • Complex selectors: Fall back to querySelectorAll for everything else.

This layered approach ensures optimal speed, especially important in older browsers where querySelectorAll can be slower.

Node Type Validation

The check element.nodeType !== 1 && element.nodeType !== 9 && element.nodeType !== 11 filters out invalid contexts:

  • Node.ELEMENT_NODE (1)
  • Node.DOCUMENT_NODE (9)
  • Node.DOCUMENT_FRAGMENT_NODE (11)

If the context isn't one of these, the function returns an empty array to prevent errors.

Converting NodeLists to Arrays

The use of slice.call(...) converts live collections like NodeList into standard arrays, enabling array methods such as map, forEach, etc., which are essential for chaining operations in Zepto.

The Z Constructor and zepto.Z

The $ function doesn’t return raw DOM nodes — it returns instances of the Z constructor, which enables method chaining and consistent API behavior.

Implementation

function Z(elements, selector) {
  var len = elements ? elements.length : 0;
  for (var i = 0; i < len; i++) {
    this[i] = elements[i];
  }
  this.length = len;
  this.selector = selector || '';
}

zepto.Z = function(dom, sel) {
  return new Z(dom, sel);
};

The Z function mimics an array by assigning each DOM node to a numeric index and setting the length property. It also stores the original selector string for debugging and reuse.

Type Checking with isZ

zepto.isZ = function(obj) {
  return obj instanceof zepto.Z;
};

This helper allows Zepto to detect whether a value is already a wrapped collection, avoiding redundant processing.

The Core: zepto.init Function

The actual work behind $ happens in zepto.init, which handles various input types and routes them appropriately.

Main Logic

zepto.init = function(selector, context) {
  var dom;

  // No argument
  if (!selector) return zepto.Z();

  // String input
  else if (typeof selector === 'string') {
    selector = selector.trim();
    if (selector[0] === '<' && /^\s*<(\w+|!)[^>]*>/.test(selector)) {
      dom = zepto.fragment(selector, RegExp.$1, context);
      selector = null;
    } else if (context !== undefined) {
      return $(context).find(selector);
    } else {
      dom = zepto.qsa(document, selector);
    }
  }

  // Function: document ready
  else if (typeof selector === 'function') {
    return $(document).ready(selector);
  }

  // Already a Z instance
  else if (zepto.isZ(selector)) {
    return selector;
  }

  // Array or plain object
  else {
    if (Array.isArray(selector)) {
      dom = selector.filter(Boolean);
    } else if (typeof selector === 'object') {
      dom = [selector];
      selector = null;
    } else if (typeof selector === 'string' && /^\s*<(\w+|!)[^>]*>/.test(selector)) {
      dom = zepto.fragment(selector, RegExp.$1, context);
      selector = null;
    } else if (context !== undefined) {
      return $(context).find(selector);
    } else {
      dom = zepto.qsa(document, selector);
    }
  }

  return zepto.Z(dom, selector);
};

Handling Different Input Types

  • No arguments: Returns an empty Z instance.
  • String selectors:
    • If it looks like HTML (<div>), parse it via fragment.
    • If a context is provided, search within it using .find().
    • Otherwise, query globally using qsa.
  • Functions: Treat as DOM-ready callbacks, equivalent to $(document).ready(fn).
  • Z instances: Return unchanged — avoids duplicaiton.
  • Arrays or DOM nodes: Wrap into a Z collection.

HTML Parsing: zepto.fragment

This function converts HTML strings into DOM node arrays.

Implementation

zepto.fragment = function(html, name, props) {
  var dom, container, nodes;
  var singleTagExp = /^<(\w+)\s*\/?>(?:<\/\1>|)$/;

  // Fast path for single tags
  if (singleTagExp.test(html)) {
    dom = $(document.createElement(RegExp.$1));
  }

  if (!dom) {
    var wrapMap = {
      tr: document.createElement('tbody'),
      tbody: document.createElement('table'),
      thead: document.createElement('table'),
      tfoot: document.createElement('table'),
      td: document.createElement('tr'),
      th: document.createElement('tr'),
      '*': document.createElement('div')
    };

    // Expand self-closing tags like <p /> → <p></p>
    html = html.replace(/<!(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, '<$1></$2>');

    var tagName = fragmentRE.test(html) && RegExp.$1;
    name = name || tagName;
    name = wrapMap[name] ? name : '*';
    container = wrapMap[name];

    container.innerHTML = '' + html;
    dom = slice.call(container.childNodes);
    
    // Remove nodes after extracting
    while (container.firstChild) {
      container.removeChild(container.firstChild);
    }
  }

  // Apply properties if given
  if (props && typeof props === 'object') {
    nodes = $(dom);
    $.each(props, function(key, val) {
      if (['val', 'css', 'html', 'text', 'data', 'width', 'height', 'offset'].indexOf(key) >= 0) {
        nodes[key](val);
      } else {
        nodes.attr(key, val);
      }
    });
  }

  return dom;
};

How It Works

  • Single tag optimization: For inputs like <div/>, directly create the element without full parsing.
  • Self-closing tag expansion: Converts <p class="x"/> into valid structures so the browser parses them correctly.
  • Container wrapping: Certain elements like <tr> must be inside <table> and <tbody>. The wrapMap ensures proper hierarchy during creation.
  • Attribute application: If additional properties are passed (e.g., {id: 'test'}), they’re applied using appropriate Zepto methods or attr().

Conclusion

The $ function in Zepto is far more than syntactic sugar — it's a smart dispatcher that normalizes diverse inputs into a unified, chainable intreface. By leveraging fast native APIs, minimizing overhead, and ensuring cross-browser consistency, Zepto delivers a lightweight yet powerful alternative to larger libraries.

Tags: zepto javascript dom-manipulation query-selector source-code-analysis

Posted on Wed, 09 Sep 2026 16:30:06 +0000 by ready2drum