jQuery-1.9.1源码分析系列(二)jQuery选择器

时间:2021-01-21 03:55:51

1.选择器结构


jQuery的选择器根据源码可以分为几块

          init: function( selector, context, rootjQuery ) {
            ...
// HANDLE: $(""), $(null), $(undefined), $(false)
...
// Handle HTML strings
if ( typeof selector === "string" ) {
  ...
// HANDLE: $(DOMElement)
} else if ( selector.nodeType ) {
  ...
// HANDLE: $(function)
} else if ( jQuery.isFunction( selector ) ) {
  ...
} //HANDLE: $($...)
if ( selector.selector !== undefined ) {
  ...
} return jQuery.makeArray( selector, this );
}

可以看到,jQuery接受的参数方式也就这么几个(""/null/undefined/false)、(string, context, rootjQuery)、(DOMElement)、(function)、($...)。除了第一个参数是string的情况比较复杂以外,其他情况都比较简单。

$(""/null/undefined/false)直接返回即可

  // HANDLE: $(""), $(null), $(undefined), $(false)
  if ( !selector ) {
    return this;
  }

$(DOMElement)将DOM对象转化为伪数组返回即可  

  // HANDLE: $(DOMElement)
  else if ( selector.nodeType ) {
    this.context = this[0] = selector;
    this.length = 1;
    return this;
  }

$(function)等同于jQuery.ready(function)

  // HANDLE: $(function)
  } else if ( jQuery.isFunction( selector ) ) {
    return rootjQuery.ready( selector );
  }

$($...)将参数执行结果(也是一个jQuery实例)包装到this上返回

  //HANDLE: $($...)
  if ( selector.selector !== undefined ) {
    this.selector = selector.selector;
    this.context = selector.context;
  }   return jQuery.makeArray( selector, this );

接下来重点:当jQuery传递的第一个参数是字符串的时候

2. jQuery选择器核心—选择器是字符串$(String[,xxx])


首先必须要必备的几个正则知识:


test方法:

  RegExpObject.test(string)用于检测一个字符串是否匹配某个模式。

  如果字符串 string 中含有与 RegExpObject 匹配的文本,则返回 true,否则返回 false。

match方法:

  stringObject.match(searchvalue | regexp)可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。该方法类似 indexOf() 和 lastIndexOf(),但是它返回指定的值,而不是字符串的位置。

  match方法将检索字符串 stringObject,以找到一个或多个与 regexp 匹配的文本。这个方法的行为在很大程度上有赖于 regexp 是否具有标志 g。

  如果 regexp 没有标志 g,那么 match() 方法就只能在 stringObject 中执行一次匹配。如果没有找到任何匹配的文本, match() 将返回 null。否则,它将返回一个数组,其中存放了与它找到的匹配文本有关的信息。该数组的第 0 个元素存放的是匹配文本,而其余的元素存放的是与正则表达式的子表达式匹配的文本。除了这些常规的数组元素之外,返回的数组还含有两个对象属性。index 属性声明的是匹配文本的起始字符在 stringObject 中的位置,input 属性声明的是对 stringObject 的引用。

  如果 regexp 具有标志 g,则 match() 方法将执行全局检索,找到 stringObject 中的所有匹配子字符串。若没有找到任何匹配的子串,则返回 null。如果找到了一个或多个匹配子串,则返回一个数组。不过全局匹配返回的数组的内容与前者大不相同,它的数组元素中存放的是 stringObject 中所有的匹配子串,而且也没有 index 属性或 input 属性。

  注意:在全局检索模式下,match() 即不提供与子表达式匹配的文本的信息,也不声明每个匹配子串的位置。如果您需要这些全局检索的信息,可以使用 RegExp.exec()。

exec方法:

  RegExpObject.exec(string) 返回一个数组,其中存放匹配的结果。如果未找到匹配,则返回值为 null。

  此数组的第 0 个元素是与正则表达式相匹配的文本,第 1 个元素是与 RegExpObject 的第 1 个子表达式相匹配的文本(如果有的话),第 2 个元素是与 RegExpObject 的第 2 个子表达式相匹配的文本(如果有的话),以此类推。除了数组元素和 length 属性之外,exec() 方法还返回两个属性。index 属性声明的是匹配文本的第一个字符的位置。input 属性则存放的是被检索的字符串 string。我们可以看得出,在调用非全局的 RegExp 对象的 exec() 方法时,返回的数组与调用方法 String.match() 返回的数组是相同的。

  但是,当 RegExpObject 是一个全局正则表达式时,exec() 的行为就稍微复杂一些。它会在 RegExpObject 的 lastIndex 属性指定的字符处开始检索字符串 string。当 exec() 找到了与表达式相匹配的文本时,在匹配后,它将把 RegExpObject 的 lastIndex 属性设置为匹配文本的最后一个字符的下一个位置。这就是说,您可以通过反复调用 exec() 方法来遍历字符串中的所有匹配文本。当 exec() 再也找不到匹配的文本时,它将返回 null,并把 lastIndex 属性重置为 0。

  重要事项:如果在一个字符串中完成了一次模式匹配之后要开始检索新的字符串,就必须手动地把 lastIndex 属性重置为 0。

  提示:请注意,无论 RegExpObject 是否是全局模式,exec() 都会把完整的细节添加到它返回的数组中。这就是 exec() 与 String.match() 的不同之处,后者在全局模式下返回的信息要少得多。因此我们可以这么说,在循环中反复地调用 exec() 方法是唯一一种获得全局模式的完整模式匹配信息的方法。

  eg:

  var str = "Visit W3School sfsffs test W3School W3School";
  var patt = new RegExp(/(W3)(School)/);
  var result = patt.exec(str);// ["W3School", "W3", "School"]
  result.index;//
  result.input;// "Visit W3School sfsffs test W3School W3School"   patt = new RegExp(/(W3)(School)/g);
  result = patt.exec(str);// ["W3School", "W3", "School"]
  result.index;//
  patt.lastIndex;//
  result = patt.exec(str);// ["W3School", "W3", "School"]
  result.index;//
  patt.lastIndex;//

jQuery选择器第一个参数为字符串的源码分析


注:stringObject.charAt(index)方法可返回指定位置的字符

先整体浏览源码在细细分析

  var match, elem;
if ( selector.charAt(0) === "<" && selector.charAt( selector.length - 1 ) === ">" && selector.length >= 3 ) {
    // 匹配html标签
    match = [ null, selector, null ];
  } else {
    //rquickExpr = /^(?:(<[\w\W]+>)[^>]*|#([\w-]*))$/,
    //匹配html标签+字符串(可以是空字符)或#+字符串(可以是空字符)
    match = rquickExpr.exec( selector );
  }   //匹配的html或确保没有为选择器为#id的情况下指定上下文
  //因为当选择器为#id的情况下,match = ["#id", undefined, "id"],match[1]是undefined
  if ( match && (match[1] || !context) ) {
    // HANDLE: $(html) -> $(array);选择器是html字符串,将字符串解析成DOM元素数组返回
    if ( match[1] ) {
      ...
    }
    // HANDLE: $(#id);选择器是ID
    else {
      ...
    }   // HANDLE: $(expr, $(...));选择器的context没有或是jQuery对象的情况,比如$("p")、$("p",$(".test")):查找$(".test")下的p标签元素
  } else if ( !context || context.jquery ) {
    return ( context || rootjQuery ).find( selector );
  // HANDLE: $(expr, context);选择器的context为真实的上下文环境,比如$("p",".test"):查找class .test下的p标签元素,等价于$(context).find(expr)
  } else {
    return this.constructor( context ).find( selector );
  }

其中使用到find函数我们会在后面去解析他,现在我们只需要知道他是用来查找下级节点即可。接下来主要分析选择器为id和html字符串的情况

a.选择器为html字符串


首先将html字符串解析成DOM节点集合,然后将它和当前jQuery实例合并

  jQuery.merge( this, jQuery.parseHTML(
    match[1],
    context && context.nodeType ? context.ownerDocument || context : document,
    true
  ) );

  比如:$("<span><p></p></span><div></div>")合并结果为

  jQuery-1.9.1源码分析系列(二)jQuery选择器

然后判断html字符串是否是一个单独的标签并且后一个参数是否是对象(props对象)。如果满足条件,将对象的属性(property)添加到标签属性(attribute)上。特别需要注意的是如果props对象的某个属性名称(假设为fncName)是刚才搜集的DOM集合对象(实际上就是jQuery对象)的函数名的情况下,将props对象的fncName属性的值作为参数传递给DOM集合对象中的fncName函数并执行之

  //rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/
  // HANDLE: $(html, props)
  if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
    for ( match in context ) {
      // context的属性是函数的话执行之
      if ( jQuery.isFunction( this[ match ] ) ) {
        this[ match ]( context[ match ] );       //其他情况设置t as attributes
      } else {
        this.attr( match, context[ match ] );
      }
    }
  }

最终返回处理过的DOM集合对象

  return this;

完整源码

  // HANDLE: $(html) -> $(array)

  if ( match[1] ) {
    context = context instanceof jQuery ? context[0] : context;     jQuery.merge( this, jQuery.parseHTML(
      match[1],
      context && context.nodeType ? context.ownerDocument || context : document,
      true
    ) );     // HANDLE: $(html, props)
    if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) {
      for ( match in context ) {
        if ( jQuery.isFunction( this[ match ] ) ) {
          this[ match ]( context[ match ] );         }else {
          this.attr( match, context[ match ] );
        }
      }
    }
    return this;
  }

b.选择器为ID


如果选择器为ID,则直接使用原生的document.getElementById直接获取即可。

唯一需要做的是兼容为难题。Blackberry 4.6返回的节点可能已经不再DOM上了(缓存导致),所以需要判断该节点是否存在并且其父节点也存在;

  if ( elem && elem.parentNode )

IE和Opera有时候会使用name属性代替ID属性,返回的结果可能不是我们期望的,这个时候需要做兼容。使用find查找来替代

  if ( elem.id !== match[2] ) {
    return rootjQuery.find( selector );
  }

将兼容后得到的节点元素添加到this上并返回。

完整源码:

  // HANDLE: $(#id)

  else {
    elem = document.getElementById( match[2] );     // Blackberry 4.6 返回的节点可能已经不再文档中#6963
    if ( elem && elem.parentNode ) {
      // IE 和Opera有时使用name替代ID去查询导致结果不对的处理
      if ( elem.id !== match[2] ) {
        return rootjQuery.find( selector );
      }       this.length = 1;
      this[0] = elem;
    }     this.context = document;
    this.selector = selector;
    return this;
  }

  如果觉得本文不错,请点击右下方【推荐】!