Prototype Selector对象学习

复制代码 代码如下:

  function $$() {

  return Selector.findChildElements(document, $A(arguments));

  }

  这个类可以分成三个部分:第一个部分就是根据不同的浏览器,判断使用什么DOM操作方法。其中操作IE就是用普通的getElementBy* 系列方法;FF是document.evaluate;Opera和Safari是selectorsAPI。第二部分是对外提供的基本函数,像findElements,match等,Element对象里面的很多方法就是直接调用这个对象里面的方法。第三部分就是XPath等一些查询DOM的匹配标准,比如什么的字符串代表的意思是查找first-child,什么的字符串代表的是查询nth-child。

  由于这个对象里面的方法很多,就不给出所有的源码了,其实我自己也仅仅看懂了一些方法的代码而已。这里根据浏览器的不同用一个简单的例子走一遍进行DOM选择的流程。在这个过程中给出需要的源代码,并加以说明。

  具体的例子如下:

  

复制代码 代码如下:

  <div id="parent2">

  <div id="navbar">

  <a id="n1"></a>

  <a></a>

  </div>

  <div id="sidebar">

  <a id="s1"></a>

  <a></a>

  </div>

  </div>

  <script type="text/javascript"><!--

  $$('#navbar a', '#sidebar a')

  // --></script>

  下面以FF为例进行说明,流程如下:

  

复制代码 代码如下:

  /*先找到$$方法,上面已经给出了,在这个方法里面将调用Selector的findChildElements方法,并且第一个参数为document,剩下参数为DOM查询字符串的数组*/

  findChildElements: function(element, expressions) {

  //这里先调用split处理了一下字符串数组,判断是否合法,并且删除了空格

  expressions = Selector.split(expressions.join(','));

  //handlers里面包含了对DOM节点处理的一些方法,像concat,unique等

  var results = [], h = Selector.handlers;

  //逐个处理查询表达式

  for (var i = 0, l = expressions.length, selector; i < l; i++) {

  //新建Selector

  selector = new Selector(expressions[i].strip());

  //把查询到的节点连接到results里面

  h.concat(results, selector.findElements(element));

  }

  //如果找到的节点数大于一,把重复节点过滤掉

  return (l > 1) ? h.unique(results) : results;

  }

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

  //Selector.split方法:

  split: function(expression) {

  var expressions = [];

  expression.scan(/(([\w#:.~>+()\s-]+|\*|\[.*?\])+)\s*(,|$)/, function(m) {

  //alert(m[1]);

  expressions.push(m[1].strip());

  });

  return expressions;

  }

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

  //Selector.handlers对象

  handlers: {

  concat: function(a, b) {

  for (var i = 0, node; node = b[i]; i++)

  a.push(node);

  return a;

  },

  //...省略一些方法

  unique: function(nodes) {

  if (nodes.length == 0) return nodes;

  var results = [], n;

  for (var i = 0, l = nodes.length; i < l; i++)

  if (typeof (n = nodes[i])._countedByPrototype == 'undefined') {

  n._countedByPrototype = Prototype.emptyFunction;

  results.push(Element.extend(n));

  }

  return Selector.handlers.unmark(results);

  },

  //下面转向新建Selector对象过程!!

  

复制代码 代码如下:

  //先看Selector的初始化部分

  //可以看出初始化部分就是判断要用什么方法操作DOM,下面看一个这几个方法

  var Selector = Class.create({

  initialize: function(expression) {

  this.expression = expression.strip();

  if (this.shouldUseSelectorsAPI()) {

  this.mode = 'selectorsAPI';

  } else if (this.shouldUseXPath()) {

  this.mode = 'xpath';

  this.compileXPathMatcher();

  } else {

  this.mode = "normal";

  this.compileMatcher();

  }

  }

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

  //XPath,FF支持此种方法

  shouldUseXPath: (function() {

  //下面检查浏览器是否有BUG,具体这个BUG是怎么回事,我在网上也没搜到。大概意思就是检查一下能否正确找到某个节点的个数

  var IS_DESCENDANT_SELECTOR_BUGGY = (function(){

  var isBuggy = false;

  if (document.evaluate && window.XPathResult) {

  var el = document.createElement('div');

  el.innerHTML = '<ul><li></li></ul><div><ul><li></li></ul></div>';

  //这里的local-name()的意思就是去掉命名空间进行查找

  var xpath = ".//*[local-name()='ul' or local-name()='UL']" +

  "//*[local-name()='li' or local-name()='LI']";

  //document.evaluate是核心的DOM查询方法,具体的使用可以到网上搜

  var result = document.evaluate(xpath, el, null,

  XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

  isBuggy = (result.snapshotLength !== 2);

  el = null;

  }

  return isBuggy;

  })();

  return function() {

  //返回的方法中判断是否支持此种DOM操作。

  if (!Prototype.BrowserFeatures.XPath) return false;

  var e = this.expression;

  //这里可以看到Safari不支持-of-type表达式和empty表达式的操作

  if (Prototype.Browser.WebKit &&

  (e.include("-of-type") || e.include(":empty")))

  return false;

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

  return false;

  if (IS_DESCENDANT_SELECTOR_BUGGY) return false;

  return true;

  }

  })(),

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

  //Sarafi和opera支持此种方法

  shouldUseSelectorsAPI: function() {

  if (!Prototype.BrowserFeatures.SelectorsAPI) return false;

  //这里判断是否支持大小写敏感查找

  if (Selector.CASE_INSENSITIVE_CLASS_NAMES) return false;

  if (!Selector._div) Selector._div = new Element('div');

  //检查一下在空div里面进行查询是否会抛出异常

  try {

  Selector._div.querySelector(this.expression);

  } catch(e) {

  return false;

  }

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

  //Selector.CASE_INSENSITIVE_CLASS_NAMES属性

  /*document.compatMode用来判断当前浏览器采用的渲染方式。

  当document.compatMode等于BackCompat时,浏览器客户区宽度是document.body.clientWidth;

  当document.compatMode等于CSS1Compat时,浏览器客户区宽度是document.documentElement.clientWidth。*/

  if (Prototype.BrowserFeatures.SelectorsAPI &&

  document.compatMode === 'BackCompat') {

  Selector.CASE_INSENSITIVE_CLASS_NAMES = (function(){

  var div = document.createElement('div'),

  span = document.createElement('span');

  div.id = "prototype_test_id";

  span.className = 'Test';

  div.appendChild(span);

  var isIgnored = (div.querySelector('#prototype_test_id .test') !== null);

  div = span = null;

  return isIgnored;

  })();

  }

  return true;

  },

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

  //如果这两个都不是就用document.getElement(s)By*系列方法进行处理,貌似IE8开始支持SelectorAPI了,其余版本IE就只能用普通的方法进行DOM查询了

  //下面转向FF支持的shouldUseXPath方法!!!

  

复制代码 代码如下:

  //当判断要用XPath进行查询时,就开始调用compileXPathMatcher方法了

  compileXPathMatcher: function() {

  //底下给出patterns,和xpath

  var e = this.expression, ps = Selector.patterns,

  x = Selector.xpath, le, m, len = ps.length, name;

  //判断是否缓存了查询字符串e

  if (Selector._cache[e]) {

  this.xpath = Selector._cache[e]; return;

  }

  // './/*'表示在当前节点下查询所有节点 不懂得可以去网上看一下XPath的表示方法

  this.matcher = ['.//*'];

  //这里的le防止无限循环查找,那个正则表达式匹配除单个空格符之外的所有字符

  while (e && le != e && (/\S/).test(e)) {

  le = e;

  //逐个查找pattern

  for (var i = 0; i<len; i++) {

  //这里的name就是pattern里面对象的name属性

  name = ps[i].name;

  //这里查看表达式是否匹配这个pattern的正则表达式

  if (m = e.match(ps[i].re)) {

  /*

  注意这里,下面的xpath里面有的是方法,有的是字符串,所以这里需要判断一下,字符串的话,需要调用Template的evaluate方法,替换里面的#{...}字符串;是方法的话,那就传入正确的参数调用方法

  */

  this.matcher.push(Object.isFunction(x[name]) ? x[name](m) :

  new Template(x[name]).evaluate(m));

  //把匹配的部分去掉,继续下面的字符串匹配

  e = e.replace(m[0], '');

  break;

  }

  }

  }

  //把所有的匹配的xpath表达式连接起来,组成最终的xpath查询字符串

  this.xpath = this.matcher.join('');

  //放到缓存中

  Selector._cache[this.expression] = this.xpath;

  },

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

  //这些patterns就是判断查询字符串到底是要查找什么,根据相应的整个表达式来判断,譬如字符串'#navbar'根据patterns匹配,那么就是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|([^'"][^

  \]]*?)))?\]/ }

  ],

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

  /*当找到pattern之后,在用对应的name找到相应的查询字符串的xpath表示形式。比如上面的id,对应的就是id字符串,在compileXPathMatcher里面会判断xpath是字符串还是方法,是方法则会传进来相应的参数进行调用*/

  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}']",

  //...省略一些方法

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

  //下面进入Selector的findElements方法!!

  

复制代码 代码如下:

  findElements: function(root) {

  //判断root是否null,为null则设置成document

  root = root || document;

  var e = this.expression, results;

  //判断是用哪种模式操作DOM,在FF下是xpath

  switch (this.mode) {

  case 'selectorsAPI':

  if (root !== document) {

  var oldId = root.id, id = $(root).identify();

  id = id.replace(/[\.:]/g, "\\$0");

  e = "#" + id + " " + e;

  }

  results = $A(root.querySelectorAll(e)).map(Element.extend);

  root.id = oldId;

  return results;

  case 'xpath':

  //下面看一下_getElementsByXPath方法

  return document._getElementsByXPath(this.xpath, root);

  default:

  return this.matcher(root);

  }

  },

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

  //这个方法其实就是把查找到的节点放到results里,并且返回,这里用到了document.evaluate,下面给出了这个方法详细解释的网址

  if (Prototype.BrowserFeatures.XPath) {

  document._getElementsByXPath = function(expression, parentElement) {

  var results = [];

  var query = document.evaluate(expression, $(parentElement) || document,

  null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);

  for (var i = 0, length = query.snapshotLength; i < length; i++)

  results.push(Element.extend(query.snapshotItem(i)));

  return results;

  };

  }

  /*

  下面这个网址是document.evaluate的方法解释:https://developer.mozilla.org/cn/DOM/document.evaluate

  */

  下面使用给出的例子连续起来解释一下:

  首先$$里面调用findChildElements方法,expressions被设置为['#navbar a','#siderbar a']

  下面调用:selector = new Selector(expressions[i].strip());新建一个Selector对象,调用initialize方法,也就是判断用什么DOM API,由于是FF,所以是this.shouldUseXPath(),然后调用compileXPathMatcher()

  然后compileXPathMatcher()里面的 var e = this.expression,把e设置成'#navbar a',然后进入while循环,遍历patterns,检查查询字符串的匹配模式,这里根据pattern的正则表达式,找到{ name: 'id', re: /^#([\w\-\*]+)(\b|$)/ },,所以name为id,当m = e.match(ps[i].re)匹配之后,m被设置成一个数组,其中m[0]就是整个匹配的字符串'#navbar',m[1]就是匹配的第一个分组字符串'navbar'

  接下来判断Object.isFunction(x[name]),由于id对应的是字符串,所以执行new Template(x[name]).evaluate(m)),字符串:id: "[@id='#{1}']",中的#{1}被替换成m[1],即'navbar',最后把结果放到this.matcher中

  然后通过把第一个匹配的字符串删除,e变成了' a',这里有一个空格!接下来继续进行匹配

  这次匹配到的是:{ name: 'descendant', re: /^\s/ },然后找到xpath中对应的descendant项:descendant: "//*",然后把这个字符串放到this.matcher中,去掉空格e只剩下字符'a'了,继续匹配

  这词匹配到的是:{ name: 'tagName', re: /^\s*(\*|[\w\-]+)(\b|$)?/ },然后找到tagName对应的xpath项,

  tagName: function(m) {

  if (m[1] == '*') return '';

  return "[local-name()='" + m[1].toLowerCase() +

  "' or local-name()='" + m[1].toUpperCase() + "']";

  }

  是个方法,所以会调用x[name](m),而m[1]='a',返回下面的那串字符,然后在放到this.matcher里,这次e为空串,while的第一个条件不满足,退出循环,把this.matcher数组连接成一个xpath字符串: .//*[@id='navbar']//*[local-name()='a' or local-name()='A']

  在初始化完Selector后,执行Selector的实例方法findElements,这里直接调用:document._getElementsByXPath(this.xpath, root);

  在_getElementsByXPath方法里执行真正的DOM查询方法document.evaluate,最后返回结果

  以上就是整个查询DOM在FF下的流程!

  在IE下和Opera,safari下流程是一样的,只不过执行的具体方法略有不同,有兴趣可以自己研究研究,那些复杂的DOM选择操作就不举例子了。这里构造的流程是非常值得学习的,包括通过pattern模式匹配进行xpath的生成,把那些patterns,xpath等提出来。

  可以看出来,写一个兼容所有浏览器的框架真是不容易!学习学习!