这一部分我们继续讲解如何通过Searcher类实现节点的搜索。
搜索器
Searcher类使用分词器生产的规则来搜索DOM,搜索的实现基于Firefox的工作原理。看一下上篇提到的一个原则:选择器的最后一部分(最右边部分)称为关键选择器。浏览器首先通过关键选择器过滤出所有符合规则的元素集合,然后再通过其他规则进行过滤(从右到左)。
搜索器通过一个搜索跟节点和一个规则数组进行实例化。关键选择器就是规则数组的最后一项。每一个规则有一个值和一个选择类型,选择类型决定了使用什么样的DOM搜索方法进行搜索。现在我只实现了通过类和ID选择的选择类型,把它存在了fingMap里。
find = {
byId: function(root, id) {
return [root.getElementById(id)];
},
byNodeName: function(root, tagName) {
var i, results = [], nodes = root.getElementsByTagName(tagName);
for (i = 0; i < nodes.length; i++) {
results.push(nodes[i]);
}
return results;
},
byClassName: function(root, className) {
var i, results = [], nodes = root.getElementsByTagName('*');
for (i = 0; i < nodes.length; i++) {
if (nodes[i].className.match('\\b' + className + '\\b')) {
results.push(nodes[i]);
}
}
return results;
}
};
findMap = {
'id': function(root, selector) {
selector = selector.split('#')[1];
return find.byId(root, selector);
},
'name and id': function(root, selector) {
var matches = selector.split('#'), name, id;
name = matches[0];
id = matches[1];
return filter.byAttr(find.byId(root, id), 'nodeName', name.toUpperCase());
}
// ...
};
如果流量器支持getElementsByClassName方法,byClassName会默认是使用该方法。Searcher类通过find方法实现了一个对选择器引擎的抽象接口。
Searcher.prototype.find = function(token) {
if (!findMap[token.finder]) {
throw new InvalidFinder('Invalid finder: ' + token.finder);
}
return findMap[token.finder](this.root, token.identity);
};
如果搜索规则不存在会抛出一个异常。算法最核心的部分是matchesToken,该方法用来判断一个节点是否符合给出规则。Searcher.prototype.matchesToken = function(element, token) {
if (!matchMap[token.finder]) {
throw new InvalidFinder('Invalid matcher: ' + token.finder);
}
return matchMap[token.finder](element, token.identity);
};
在matchesAllRules方法里,每一个规则(token)都会和指定节点列表进行匹配,while循环用来迭代整个节点树。while ((ancestor = ancestor.parentNode) && token) {
if (this.matchesToken(ancestor, token)) {
token = tokens.pop();
}
}
如果循环结束时数组中已经没有规则,说明该元素匹配了所有了规则。通过这种方式,搜索DOM变得非常简单。
Searcher.prototype.parse = function() {
// Find all elements with the key selector
var i, element, elements = this.find(this.key_selector), results = [];
// Traverse upwards from each element to see if it matches all of the rules
for (i = 0; i < elements.length; i++) {
element = elements[i];
if (this.matchesAllRules(element)) {
results.push(element);
}
}
return results;
};
我们以符合关键选择器的元素集合开始,访问他们的祖先节点(ancestors)看它们是否匹配所有选择器规则。正如我上一篇所说的,这不是一个最快的选择器引擎,但是它更容易理解和继承。
实现接口
我们已经有搜索节点和解析选择器的工具了,下面要做的就是把它们封装成一个可用的接口。我决定提供分词器的接口,就像Sly提供对解析结果的访问一样。公开的接口如下:
dom.tokenize = function(selector) {
var tokenizer = new Tokenizer(selector);
return tokenizer;
};
dom.get = function(selector) {
var tokens = dom.tokenize(selector).tokens,
searcher = new Searcher(document, tokens);
return searcher.parse();
};
用户可以通过调用dom.get进行查询,你提供一个选择器,然后返回一个元素数组。现在返回的元素并没有像jQuery一样,被我们自定义的对象包装起来,但是我们将来会在需要时把这个功能加进来。
测试
我之前已经把测试写好了。和其他测试不同的是,这次的测试必须在浏览器环境中运行。虽然一个好的选择器引擎应该提供XML的搜索,但是为了保持代码的简单性,我们暂时先不考虑这方面的问题。ID选择器的测试如下:
given('a selector to search for', function() {
should('find with id', turing.dom.get('#dom-test')[0].id).isEqual('dom-test');
我也会用同样的方法测试我们的类和标签选择功能。我以前写过一个测试的开发框架,我们现在的项目测试都是基于该框架的基础上的。
下一篇
选择器还有许多要讲,我们还没有涉及缓存,我们可能要将这些先放一放。这部分结束之后,我们会讲一些工具库和事件等方面的东西。