SoFunction
Updated on 2025-04-10

Prototype Selector object learning

Copy the codeThe code is as follows:

function $$() {
return (document, $A(arguments));
}

This class can be divided into three parts: the first part is to determine which DOM operation method is used according to different browsers. Among them, IE is to use the ordinary getElementBy* series method; FF is; Opera and Safari are selectorsAPI. The second part is the basic functions provided to the outside world, such as findElements, match, etc. Many methods in the Element object are to directly call the methods in this object. The third part is some matching standards for querying DOM such as XPath, such as what string means to look for first-child, and what string means to look for nth-child.

Since there are many methods in this object, I will not give all the source code. In fact, I have only understood some method code. Here we use a simple example to go through the process of DOM selection according to the different browsers. In this process, the required source code is given and explained.

Specific examples are as follows:
Copy the codeThe code is as follows:

<div >
<div >
<a ></a>
<a></a>
</div>
<div >
<a ></a>
<a></a>
</div>
</div>

<script type="text/javascript"><!--
        $$('#navbar a', '#sidebar a')
// --></script>

The following is an example of FF, and the process is as follows:
Copy the codeThe code is as follows:

/*First find the $$ method, which has been given above. In this method, the findChildElements method of Selector will be called, and the first parameter is document, and the remaining parameter is an array of DOM query string*/

findChildElements: function(element, expressions) {
// Here we first call split to process the string array, determine whether it is legal, and delete the spaces
expressions = ((','));
//handlers contains some methods for processing DOM nodes, such as concat, unique, etc.
var results = [], h = ;
// Process query expressions one by one
for (var i = 0, l = , selector; i < l; i++) {
// Create a new Selector
selector = new Selector(expressions[i].strip());
//Connect the query node into results
(results, (element));
}
//If the number of nodes found is greater than one, filter out the repeated nodes
return (l > 1) ? (results) : results;
}

//===================================================

//method:
split: function(expression) {
var expressions = [];
(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {
        //alert(m[1]);
(m[1].strip());
});
return expressions;
}

//===================================================

//Object
handlers: {
concat: function(a, b) {
for (var i = 0, node; node = b[i]; i++)
(node);
return a;
},
//...Omit some methods
unique: function(nodes) {
if ( == 0) return nodes;
var results = [], n;
for (var i = 0, l = ; i < l; i++)
if (typeof (n = nodes[i])._countedByPrototype == 'undefined') {
n._countedByPrototype = ;
((n));
}
return (results);
},

//The following is a process of creating a new Selector object! !

Copy the codeThe code is as follows:

//Look at the initialization part of Selector first
//You can see that the initialization part is to determine what method to use to operate the DOM. Let's see these methods below
var Selector = ({
initialize: function(expression) {
= ();

if (()) {
= 'selectorsAPI';
} else if (()) {
= 'xpath';
();
} else {
= "normal";
();
}

}

//===================================================

//XPath, FF supports this method
shouldUseXPath: (function() {

//Check the browser for any bugs. I haven't found it online. It roughly means checking whether the number of nodes can be found correctly
var IS_DESCENDANT_SELECTOR_BUGGY = (function(){
var isBuggy = false;
if ( && ) {
var el = ('div');
= '<ul><li></li></ul><div><ul><li></li></ul></div>';
//The local-name() here means to remove the namespace and search
var xpath = ".//*[local-name()='ul' or local-name()='UL']" +
"//*[local-name()='li' or local-name()='LI']";
//It is the core DOM query method. For specific use, you can search online
var result = (xpath, el, null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

isBuggy = ( !== 2);
el = null;
}
return isBuggy;
})();

return function() {
//The return method determines whether this DOM operation is supported.
if (!) return false;

var e = ;
// Here you can see that Safari does not support the operations of -of-type expressions and empty expressions
if ( &&
(("-of-type") || (":empty")))
return false;

if ((/(\[[\w-]*?:|:checked)/).test(e))
return false;

if (IS_DESCENDANT_SELECTOR_BUGGY) return false;

return true;
}

})(),

//===================================================

//Sarafi and opera support this method
shouldUseSelectorsAPI: function() {
if (!) return false;
//This is to determine whether case-sensitive search is supported
if (Selector.CASE_INSENSITIVE_CLASS_NAMES) return false;

if (!Selector._div) Selector._div = new Element('div');
//Check whether querying in an empty div will throw an exception
try {
Selector._div.querySelector();
} catch(e) {
return false;
}

//===================================================

//Selector.CASE_INSENSITIVE_CLASS_NAMES attribute
/* Used to determine the rendering method currently used by the browser.
When equal to BackCompat, the browser client area width is;
When equal to CSS1Compat, the browser client area width is. */

if ( &&
=== 'BackCompat') {
Selector.CASE_INSENSITIVE_CLASS_NAMES = (function(){
var div = ('div'),
span = ('span');

= "prototype_test_id";
= 'Test';
(span);
var isIgnored = (('#prototype_test_id .test') !== null);
div = span = null;
return isIgnored;
})();
}

return true;
},

//===================================================

//If neither of these two methods are processed using the (s)By* series method, it seems that IE8 has begun to support SelectorAPI, and the rest of the IE versions can only use ordinary methods to query DOM.

//The following is to turn to the shouldUseXPath method supported by FF!!!

Copy the codeThe code is as follows:

//When it is determined that XPath is to be used for querying, start calling the compileXPathMatcher method

compileXPathMatcher: function() {
//Give patterns and xpath below
var e = , ps = ,
x = , le, m, len = , name;

//Judge whether the query string e is cached
if (Selector._cache[e]) {
= Selector._cache[e]; return;
}
// './/*' means querying all nodes under the current node. If you don't know, you can go online to see the expression method of XPath
= ['.//*'];
//The le here prevents infinite loop searches, and that regular expression matches all characters except a single space character
while (e && le != e && (/\S/).test(e)) {
le = e;
//Look for pattern one by one
for (var i = 0; i<len; i++) {
//The name here is the name attribute of the object in the pattern
name = ps[i].name;
// Check here to see if the expression matches the regular expression of this pattern
if (m = (ps[i].re)) {
/*
Note that there are methods and strings in the xpath below, so you need to judge here that for strings, you need to call the evaluate method of Template to replace the #{...} string; if it is a method, then pass in the correct parameters to call the method
*/
((x[name]) ? x[name](m) :
new Template(x[name]).evaluate(m));
// Remove the matching part and continue to match the following string
e = (m[0], '');

break;
}
}

}
//Connect all matching xpath expressions to form the final xpath query string
= ('');
//Put it in cache
Selector._cache[] = ;
},
//==============================================

//These patterns are to determine what the query string is looking for, and judge based on the corresponding entire expression. For example, if the string '#navbar' matches according to patterns, then it is id
patterns: [
{ name: 'laterSibling', re: /^\s*~\s*/ },
{ name: 'child', re: /^\s*>\s*/ },
{ name: 'adjacent', re: /^\s*\+\s*/ },
{ name: 'descendant', re: /^\s/ },
{ name: 'tagName', re: /^\s*(\*|[\w\-]+)(\b|$)?/ },
{ name: 'id', re: /^#([\w\-\*]+)(\b|$)/ },
{ name: 'className', re: /^\.([\w\-\*]+)(\b|$)/ },
{ name: 'pseudo', re:
/^:((first|last|nth|nth-last|only)(-child|-of-type)|empty|checked|(en|d
is)abled|not)(\((.*?)\))?(\b|$|(?=\s|[:+~>]))/ },
{ name: 'attrPresence', re: /^\[((?:[\w-]+:)?[\w-]+)\]/ },
{ name: 'attr', re:
/\[((?:[\w-]*:)?[\w-]+)\s*(?:([!^$*~|]?=)\s*((['"])([^\4]*?)\4|([^'"][^
\]]*?)))?\]/ }
],

//==============================================

/*After finding the pattern, the xpath representation of the corresponding query string is found with the corresponding name. For example, the id above corresponds to the id string. In compileXPathMatcher, we will determine whether xpath is a string or a method. If it is a method, the corresponding parameters will be passed in for call*/
xpath: {
descendant: "//*",
child: "/*",
adjacent: "/following-sibling::*[1]",
laterSibling: '/following-sibling::*',
tagName: function(m) {
if (m[1] == '*') return '';
return "[local-name()='" + m[1].toLowerCase() +
"' or local-name()='" + m[1].toUpperCase() + "']";
},
className: "[contains(concat(' ', @class, ' '), ' #{1} ')]",
id: "[@id='#{1}']",
//...Omit some methods

//==============================================

//Let’s enter the findElements method of Selector! !

Copy the codeThe code is as follows:

findElements: function(root) {
//Judge whether root is null. If it is null, set to document
root = root || document;
var e = , results;
//Judge which mode to use to operate the DOM, under FF, it is xpath
switch () {
case 'selectorsAPI':

if (root !== document) {
var oldId = , id = $(root).identify();
id = (/[\.:]/g, "\\$0");
e = "#" + id + " " + e;

}
results = $A((e)).map();
= oldId;

return results;
case 'xpath':
//Look at the _getElementsByXPath method below
return document._getElementsByXPath(, root);
default:
return (root);
}
},

//===========================================

//This method is actually to put the found node in results and return it. It is used here. The URL for the detailed explanation of this method is given below.
if () {
document._getElementsByXPath = function(expression, parentElement) {
var results = [];
var query = (expression, $(parentElement) || document,
null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
for (var i = 0, length = ; i < length; i++)
(((i)));
return results;
};
}

/*
The following URL is the method explanation: /cn/DOM/
*/

The following uses the given example to explain in succession:

First, findChildElements method is called in $$, and expressions are set to ['#navbar a','#siderbar a']

The following call is: selector = new Selector(expressions[i].strip()); Create a new Selector object, call the initialize method, that is, determine which DOM API to use. Since it is FF, it is (), and then call compileXPathMatcher()

Then, in compileXPathMatcher() var e = , set e to '#navbar a', then enter the while loop, traverse patterns, and check the matching pattern of the query string. Here, according to the regular expression of pattern, find { name: 'id', re: /^#([\w\-\*]+)(\b|$)/ }, so name is id. After m = (ps[i].re) match, m is set to an array, where m[0] is the entire matching string '#navbar', and m[1] is the first matching grouping string 'navbar'.

Next, judge (x[name]). Since the id corresponds to a string, execute new Template(x[name]).evaluate(m)). String: id: "[@id='#{1}']", the #{1} in "m[1] is replaced by m[1], that is, 'navbar', and finally put the result in

Then by deleting the first matching string, e becomes 'a', there is a space here! Continue to match next

This time the match is: { name: 'descendant', re: /^\s/ }, and then find the corresponding descendant item in xpath: descendant: "//*", then put this string in, remove the space e and only the character 'a', and continue to match

This word matches: { name: 'tagName', re: /^\s*(\*|[\w\-]+)(\b|$)?/ }, and then find the xpath item corresponding to tagName.

tagName: function(m) {
if (m[1] == '*') return '';
return "[local-name()='" + m[1].toLowerCase() +
"' or local-name()='" + m[1].toUpperCase() + "']";
}

It is a method, so x[name](m) will be called, and m[1]='a', returning the string of characters below, and then put in. This time e is an empty string. The first condition of while is not satisfied, exits the loop and concatenates the array into an xpath string: .//*[@id='navbar']//*[local-name()='a' or local-name()='A']

After initializing the Selector, execute the Selector instance method findElements, and directly call it here: document._getElementsByXPath(, root);

Execute the real DOM query method in the _getElementsByXPath method and finally return the result

The above is the entire process of querying DOM under FF!

The processes under IE and Opera and safari are the same, but the specific methods of execution are slightly different. If you are interested, you can study them yourself. I won’t give examples of those complex DOM selection operations. The process constructed here is very worth learning, including generating xpath through pattern matching, and proposing patterns, xpath, etc.

It can be seen that it is really not easy to write a framework that is compatible with all browsers! Learn and learn!