js框架开发之旅--选择器三

上一篇我们讨论了如何实现一个选择器的引擎。选择器引擎使用基于正则表达式的分词和扫描器,把选择器拆分成可执行的部分。

这一部分我们继续讲解如何通过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');
我也会用同样的方法测试我们的类和标签选择功能。

我以前写过一个测试的开发框架,我们现在的项目测试都是基于该框架的基础上的。


下一篇

选择器还有许多要讲,我们还没有涉及缓存,我们可能要将这些先放一放。这部分结束之后,我们会讲一些工具库和事件等方面的东西。


牧客网--让自由职业成为一个靠谱的工作


发布了16 篇原创文章 · 获赞 87 · 访问量 5万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章