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
getElementByIdwhen applicable — fastest for single-element lookup. - Class selectors: Prefer
getElementsByClassNamefor better performance over general queries. - Tag selectors: Use
getElementsByTagNamefor direct tag name matches. - Complex selectors: Fall back to
querySelectorAllfor 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
Zinstance. - String selectors:
- If it looks like HTML (
<div>), parse it viafragment. - If a context is provided, search within it using
.find(). - Otherwise, query globally using
qsa.
- If it looks like HTML (
- Functions: Treat as DOM-ready callbacks, equivalent to
$(document).ready(fn). - Z instances: Return unchanged — avoids duplicaiton.
- Arrays or DOM nodes: Wrap into a
Zcollection.
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>. ThewrapMapensures proper hierarchy during creation. - Attribute application: If additional properties are passed (e.g.,
{id: 'test'}), they’re applied using appropriate Zepto methods orattr().
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.