前言
已经写了两篇文章了,我的思路就是按照整个函数的运行顺序进行分析,那么这次就轮到
tokenize和Sizzle的主方法了。我还是会按照原来的方式,先把这两个函数用到的一些内部的属性或者方法先放在最前面,然后在进行分析这两个函数,一些已经出现过的函数或者变量包括之前的正则,我就不重复列举了,不清楚的同学可以去我之前的文章去查。Sizzle源码虽然看了很长时间了,但是进度真的好慢,好多东西需要打断点一点点的看,有一些兼容也不是很清楚,要换浏览器去测试。OK,牢骚发到这里,开始撸源码。
用到的全局变量与方法
这里的全局指的是IIFE中的作用域
变量
tokenCache = createCachetokenize预编译之后的缓存,下一次如果遇到相同的选择器,直接返回结果nonnativeSelectorCache = createCache这个原生的querySelectorAll无法返回结果的选择器字符串的缓存,下次如果出现直接走Sizzle的方法,不再走原生方法Expr.prefilter这东西是由于像attribute pesuod child的正则,捕获了太多的捕获组,要对这个捕获组进行二次加工。Expr.cacheLength常量,数字,控制缓存的数量,值是50。
方法
createCache这个方法就是创建缓存
function createCache() {
var keys = [];
function cache(key, value) {
//Array.prototype.push返回的是push之后当前数组的长度
if (keys.push(key) > Expr.cacheLength) {
//Array.prototype.shift返回的是被shift出去的值
delete cache[keys.shift()];
}
return ( cache[key + ' '] = value );
}
return cache;
}
push
push = arr.push;
slice = arr.slice;
try {
push.apply(
(arr = slice.call(preferredDoc.childNodes)),
preferredDoc.childNodes
);
arr[preferredDoc.childNodes.length].nodeType
} catch(e) {
push = { apply : arr.length ?
function (target, els) {
pushNative.apply(target, slice.call(els))
} :
function (target, els) {
var j = traget.length,
i = 0;
while((traget[i++] = els[i++])){}
traget.legth = j - 1;
}
}
}
Expr.prefilter
Expr = {
// ...
prefilter: {
//参数就是被attribute的正则 match 到的数组
/***$1 属性名
$2 运算符
$3 - $5 都是属性值
$3 双引号
$4 单引号
$5 没引号
***/
'ATTR': function(match) {
// 转码
match[1] = match[1].replace(runescape, funescape);
// 不管是从哪里捕获的都统一放到$3
match[3] = (match[3] || match[4] || match[5] || '').replace(runescape, funescape);
// 如果是 '~=' 的话, 需要留空格
if (match[2] === '~=') {
match[3] = " " + match[3] + " ";
}
return match.slice(0, 4);
},
/***
$1 (only|first|last|nth|nth-last)
$2 (child|of-type)
$3 括号中的全部内容
$4 even odd 或者 表达式2n+1 中的2n
$5 2n的正负 +2n -2n 中的 + -
$6 n的倍数 2n 中的 2
$7 运算符 + -
$8 最后一个数 1
***/
'CHILD': function(match) {
match[1] = match[1].toLowerCase();
if (match[1].slice(0, 3) === 'nth') {
// nth得有参数
if(!match[3]) {
Siizle.error(match[0]);
}
match[4] = +(match[4] ?
match[5] + (match[6] || 1) :
2 * (match[3] === 'even' || match[3] === 'odd'));
match[5] = +((match[7] + match[8]) || match[3] === 'odd');
// 除了nth的其余的没有括号中的值
} else if (match[3]) {
Sizzle.error(match[0]);
}
return match;
},
/***
$1 伪类名
$2 括号中的全部内容
$3 在引号中的值
$4 单引号的值
$5 双引号的值
$6 没有引号的值
***/
'PESUDO': function(match) {
var excess,
// $6没有值而$2有值的情况: :not(:nth-child(2))
unquoted = !match[6] && match[2];
if (matchExpr['CHILD'].test(match[0])) {
return null;
}
// 如果是引号中的内容
if (match[3]) {
match[2] = match[4] || match[5] || '';
// 如果括号中的内容还是个伪类
} else if (unquoted && repseudo.test(unquoted) &&
//递归调用tokenize
(excess = tokenize(unquoted, true)) &&
// 留最近的一个()
// excess是个负数
(excess = unquoted.indexOf(")", unquoted.length - excess) - unquoted.length)) {
match[0] = match[0].slice(0, excess);
match[2] = unquoted.slice(0, excess);
}
return match.slice(0, 3);
}
}
// ...
}
- testContext 检测一个节点作为Sizzle上下文的有效性
function testContext( context ) {
return context && typeof context.getElementsByTagName !== "undefined" && context;
}
tokenize
预编译
Sizzle在1.7之后加入了预编译的思想,其实大家都这么说,我就也跟着这么说了。我理解的预编译其实就是将所输入的东西,通过某种规则,进行转换,转换成另一种格式。这也有另一种说辞,也就是AST,我想这个词大家应该更清楚一点,不同AST是什么的同学可以去看一下这篇文章。
Sizzle中的预编译就是tokenize函数,它将各种选择器按照转换成了一个对象,举个简单的例子
var selector = 'a#link > p, .expando';
// 会转换成一下的样子
var tSelector = [
[
{
type: 'TAG',
value: 'a',
match: ['a']
},
{
type: 'ID',
value: '#link',
match: ['link']
},
{
type: '>'
value: '>'
},
{
type: 'TAG',
value: 'p',
match: ['p']
}
],
[
{
type: 'CLASS',
value: '.expando',
match: ['expando']
}
]
]
函数源码
tokenize = Sizzle.tokenize = function(selector, parseOnly) {
var matched, match, tokens, type,
soFar, groups, preFilters,
cached = tokenCache[selector + ' '];
// 有没有缓存,如果有缓存 那么直接返回就ok
if (cached) {
// 如果是parseOnly的话, 缓存里面存的都是匹配上的字符,
// 所以如果有的话, 那么不会有剩余的字符串所以返回0
// 在返回的时候 会返回一个缓存的浅复制.
return parseOnly ? 0 : cached.silce(0)
}
soFar = selector;
groups = [];
preFilters = Expr.preFilter;
while (soFar) {
// 如果是第一次进入, 或者是匹配到了逗号
if (!matched || (match = rcomma.exec(soFar))) {
// 如果是逗号的话 把逗号去了
if (match) {
soFar = soFar.slice(match[0].length) || soFar;
}
groups.push( (tokens = []) );
}
matched = false;
// 如果是关系选择器
if ( (match = rcombinators.exec(soFar))) {
matched = match.shift();
tokens.push({
value: matched,
type: match[0].replace(rtrim, " ")
});
soFar = soFar.slice(0, matched.length);
}
// 循环匹配
// TAG CLASS ID ATTR CHILD PESUDO
for (type in Expr.filter) {
if (match = matchExpr[type] && (!preFilter[type] || (match = preFilter[type](match)))) {
matched = match.shift();
tokens.push({
value: matched,
type: type,
matches: match
});
soFar = soFar.slice(matched.length);
}
}
if (!matched) {
break;
}
}
// 这里也是, 如果是parseOnly的话就返回没匹配到的剩下的字符串的长度
// 如果不是的话, 就要看有没有剩下的字符串
// 如果有, 那说明字符串不合法 直接报错
// 如果没有的话 先存缓存, 然后再return一个副本出去
return paresOnly ?
soFar.length :
soFar ?
Sizzle.error(selector) :
tokenCache(selector, groups).slice(0);
}
这里会出现一种情况,比如选择字符串是':not(:nth-child(2))'这样的,进入了tokenize,这个时候到PESUDO,那么就要走preFilter['PESUDO']方法,这个字符串被匹配的时候是$2有值而$6没值的情况,所以会再次走tokenize函数(paresOnly = true),形成递归。
Sizzle函数
Sizzle函数就是咱们调用选择器走的第一个函数,前两篇文章说的方法都是在引入Sizzle时运行的。该方法会判断字符串是否是简单的class tag id。比如p,#id,.container。或者是否可以直接使用querySelectorAll,如果都不可以的话,就进入select方法。
函数源码
function Sizzle(selector, context, results, seed) {
var m, i, elem, nid, match, groups, newSelector,
newContext = context && context.ownerDocument,
// 如果不传context, 那么默认就是9
nodeType = context ? context.nodeType : 9;
results = results || [];
// 如果选择器是空, 或者选择器不是字符串, 又或者 context的节点类型不是 element document documetfragment之中的任何一个的话 直接返回[]
if (typeof selector != 'string' || !selector ||
nodeType !== 1 && nodeType !== 9 && noedeType !== 11) {
return results;
}
if (!seed) {
// 设置一次document但是一般都是直接返回了
setDocument(context);
context = context || doucment;
if (documentIsHtml) {
//如果是简单的选择id tag class这种情况
if (nodeTpe !== 11 && (match = rquickExpr.exec(selector))) {
//ID
if (m = match[1]) {
// 是document节点
if (nodeType === 9) {
if ( (elem = context.getElementById(m)) ) {
if (elem.id === m) {
results.push(elem);
return results;
}
} else {
return results;
}
// 如果是元素节点
// 元素节点是没有getElememtById这个方法的只能通过document选,选完元素之后判断是不是被当前元素节点包含
} else {
if (newContext && (elem = newContext.getElementById(m)) &&
contains(context, elem) &&
elem.id === m) {
results.push(elem);
return results;
}
}
// 标签选择
} else if (match[2]) {
// 由于rquickExpr不会匹配到(*),所以不需要考虑兼容问题, 直接返回所有匹配到的元素就可以了
push.apply(results, context,getElementByTagName(selector));
// class 选择
} else if ((m = match[3]) && support.getElementsByClassName &&
context.getElementsByClassName) {
push.apply(results, context,getElementsByClassName(m));
return results;
}
}
// 如果支持querySelectorAll, 并且这个字符串并不是之前出现过的无法用原生方法匹配的字符串, 并且没有兼容问题中没有这类的话
if (support.qsa &&
!nonnativeSelevtorCache[selector + ' '] &&
(!rbuggyQSA || !rbuggyQSA.test(selector)) &&
// 排除object对象
(nodeType!==1 || context.nodeName.toLowerCase() !== 'object')) {
newSelector = selector;
newContext = context;
// querySelector有一个问题,如果不是document.querySelectorAll而是element.querySelectorAll的话
// 当选择字符串出现了关系选择符也就是 '> ~ + ,' 这些的话,选择出来的结果会有出入
// 解决这个问题的方法就是给当前的element加一个id 再在原本的选择字符串的最前面添加这个id
// 通过doucment.querySelectorAll选择元素
if (nodeType === 1 &&
(rdescend.test(selector) || rcombinators.test(selector))) {
// 如果是兄弟选择器的话 那么要给其父集添加id
newContext = rsibling.test(selector) && testContext(context.parentNode) ||
context;
// 这里这个判断不是很清楚 如果有人知道是什么意思的话希望能解答一下蟹蟹
// 以下是英文注释
// We can use :scope instead of the ID hack if the browser
// supports it & if we're not changing the context.
if (newContext !== context || !support.scope) {
if ((nid = context.getAttribute("id"))) {
nid = nid.replace(rcssescape, fcssescape);
} else {
context.setAttribute('id', (nid = expando));
}
}
groups = tokenize(selector);
i = groups.length;
while(i--) {
groups[i] = (nid? '#' + nid : ':scope') + ' ' +
toSelector(groups[i]);
}
newSelector.groups.join(',');
}
try {
push.apply(results,
newContext.querySelectorAll(newSelector)
);
return results;
} catch(e) {
// 推入原生方法不能解析的字符串缓存
nonnatvieSelectorCache(selector, true);
} finally {
if (nid === expando) {
context.removeAttribute('id');
}
}
}
}
}
// 所有不行的都走select方法
return select(selector.replace(rtrim, '$1'), context, results, seed);
}
总结
到此Sizzle的前置函数应该就都看完了, 在之后应该就是如何选择元素了。下一章应该会写select方法吧,就这样。