这篇文章我会将
Sizzle
整个筛选元素的流程所有讲解一遍。从它是如何找出种子集seed
,又是如何将token
转换为筛选规则,再到是如何经过规则进行筛选的全部流程。这里我会经过一个例子来进行说明,由token
转换为筛选规则那里很是的绕,尤为是Sizzle
还有缓存的逻辑夹杂在其中,并且最复杂的实际上是缓存。我我的的描述可能并不能让人听得很明白,因此有兴趣的人,能够结合个人说明去看一下源码,我到如今也是只看懂了百分之八十多,缓存的相关代码我并无理解的特别透彻,因此这里我只给你们分析一下我本身所理解到的,整个选择元素的主流程。javascript
Sizzle
并非从左向右依次进行选择的,并非先选择出'.container'
而后再去找其下的input
。这样虽然看似合理,但实际上是很消耗时间的,由于根据DOM
树的结构越往下分支越多,因此Sizzle
会先在选择器的末尾找到一个种子集(也就是seed
),而后经过种子集一层一层往上判断,是否符合条件。vue
那么如何选择seed
呢?这就是select
函数干的事情了。java
这个函数,主要就作了两件事。node
tokenize
seed
一个选择字符串可能会存在多个关系选择器,好比body p>input:disabled
。若是使用这些关系选择器来做为分割,咱们能够获得几组选择器,seed就是在最后一组选择器中的元素选择器, ID选择器, 或者class选择器
,若是当最后一组选择器没有这三个选择器的话,那么就没有seed
。jquery
以上述例子为例,seed
就是整个document
中的全部input
。数组
若是在setDocument的时候, support.getElementsByClass = false得话,那么`seed`不包括class选择器 缓存
select = Sizzle.select = function(selector, context, results, seed) { var i, tokens, token, type, find, compiled = type selector === 'function' && selector, match = !seed && tokenize( (selector = compiled.selector || selector) ); results = results || []; // 这里指选择字符串没有逗号的状况, if (match.length === 1) { tokens = match[0] = match[0].slice(0); if (tokens.length > 2 && documentIsHTML && Expr.relative[tokens[1].type] ) { context = (Expr.find["ID"](token.matches[0] .replace(runescape, funescape), context) || [])[0]; if (!context) { return results; } else if (compiled) { context = context.parentNode } selector = selector.silce(tokens.shift().value.length); } i = matchExpr['needsContxt'].test(selector) ? 0 : tokens.length; // 这里开始找seed while (i--) { token = tokens[i]; // 从后向前, 若是碰到关系选择器了,那就不找了 if (Expr.relative[(type = token.type)]) { break; } // Expr.find 最多只有三个属性,这个是在setDocument的时候设置的 // TAG CLASS ID if ((find = Expr.find[type])) { if ( (seed = find( token.matches[0].replace(runescape, funescape), rsibling.test(tokens[0].type) && testContext(context.parentNode) || context ) ) ) { tokens.splice(i, 1); // 因为已经抽出了seed 因此要重组selector // 上面的例子跑到这里 selector就会变成 '.container [type=text]' selector = seed.length && toSelector(tokens); if (!selector) { push.apply(results, seed); return results } break; } } } } (compiled || compile(selecotr, match)) ( seed, context, !documentIsHTML, results, !context || rsibling.test(selector) && testContext(context.parentNode) || context ); // 注意: 这里并无return compile返回出来的闭包执行后的结果, 而是return 做为参数穿进去的results return results; } 复制代码
compile
其实并非生成规则的函数,它算是一个总入口,主要的功能是将生成的规则缓存,从缓存中查找是否已经有对应的规则,返回一个superMatch函数, superMatch函数是作筛选的函数。markdown
compile = Sizzle.compile = function(selector, match) { var i, setMatcher = [], elementMatchers = [], cached = compilerCache[selector + ' ']; if (!cache) { if (!match) { match = tokenize(selector); } i = match.length; // 注意: 这里的match是整个二维数组, 是整个一个选择组, 因此这里只循环一次 while(i--) { cached = matcherFromTokens(match[i]); // 在复杂的选择器的时候, 伪类函数会被标记, 这里就是判断是不是伪类 if (cached[expando]) { setMatchers.push(cached); } else { elementMatchers.push(cached); } } } // 缓存 cache = compilerCache( selector, // 这个函数返回superMatch函数 matcherFromGroupMatchers(elementMatchers, setMatchers) ) } 复制代码
matcherFromTokens
会经过token
生成规则,流程是这样的。它会先建立一个matchers
数组,并建立一个baseMathcer
函数,这个baseMatcher
通常状况都为true
。以后遍历整个token
,只要没有遇到关系操做符,就将对应的filter
函数推入matchers
中;当遇到了关系操做符,会先将已经在matchers
中的所有筛选函数,用elementMatcher
函数包裹在一块儿,再使用addCombinator
做为纽带返回一个函数,取代以前的matchers
。如此循环,直到将整个token所有遍历结束。addCombinator
主要的功能就是根据关系操做符来查找兄弟元素和父级元素。闭包
我会把matcherFromTokens
,elementMatcher
,addCombinator
这三个函数都放在下面。app
function matcherFromTokens(tokens) { var checkContext, matcher, j, len = tokens.length, // 判断是不是关系操做符开头 leadingRelative = Expr.relative[ tokens[0].type ], // 若是不是关系符开头, 默认就是父祖集关系 implicitRelative = leadingRelative || Expr.relative[' '], i = leadingRelative ? 1 : 0, // 这里就是baseMatcher // addCombinator中做为参数的fn 就是 filter matchContext = addCombinator(function(elem) { return elem === checkContext; }, implicitRelative, true), matchAnyContext = addCombinator(function(elem) { return indexOf(checkContext, elem) > -1; }, implicitRelative, true), // 这个就是最后规则的合集, 它先把baseMatcher放到了合集里面 // 通常状况 (!leadingRelative && (xml || context !== outermostContext))会返回true 从而不去执行下面的函数 matchers = [ function(elem, context, xml) { var ret = (!leadingRelative && (xml || context !== outermostContext)) || ( ( checkContext = context ).nodeType ? matchContext(elem, context, xml) : matchAnyContext(elem, context, xml) ); checkContext = null; return ret; } ]; // 正向遍历tokens for (; i < len; i++) { // 若是是关系符的话 if ((matcher = Expr.relative[tokens[i].type])) { // 先将以有的规则用elementMatcher包裹在一块儿, 再用addCombinator建立关联; // 生成的新的matcher代替原来所有的matcher matchers = [addCombinator(elementMatcher(matchers), matcher)]; // 若是是 TAG ATTR PESUDO ID CLASS CHILD } else { matcher = Expr.filter[tokens[i].type].apply(null, toekns[i].matches); // 若是是伪类, 这里我尝试了不少选择器可是都没有进入到这个if里面 // 感受得是特别复杂的选择器了 // 由于一直没试出来, 因此就没搞懂这里究竟是干啥的 if (matcher[expando]) { j = ++i; for (; j < len; j++) { if (Expr.relative[tokens[j].type]) { break; } } return setMatcher( i > 1 && elementMatcher(matchers), i > 1 && toSelector( tokens .slice(0, i - 1) .concat({value: tokens[i - 2].type === ' ' ? '*' : ''}) ).replace(rtrim, '$1'), matcher, i < j && matcherFromTokens(tokens.slice(i, j)), j < len && matcherFromTokens((tokens = tokens.slice(j))), j < len && toSelector(tokens) ); } matchers.push(matcher); } } // 最后再用elementMatcher裹一层, 返回一个函数 return elementMatcher(matchers); } function addCombinator(matcher, combinator, base) { var dir = combinator.dir, skip = combinator.next, key = skip || dir, checkNonElements = base && key === 'parentNode', doneName = done++; // 若是是 > + 这两个关系符 return combinator.first ? // 检查最近的父级或者兄弟元素 function(elem, context, xml) { // 这个while循环elem是持续赋值的 // 这里就是为何说是纽带的缘由了 // 在这里循环以后, 找到的新元素放到以后的matcher里面, 构成了经过seed一级一级向上查找的逻辑 while(elem = elem[dir]) { // 当遇到元素节点的时候 if (elem.nodeType === 1 || checkNonElements) { return matcher(elem, context, xml); } } return false; } : // 检查所有父级或者兄弟元素 function(elem, context, xml) { var oldCahce, uniqueCache, outerCache, newCache = [dirruns, doneName]; if (xml) { while ((elem = elem[dir])) { if (elem.nodeType === 1 || checkNonElments) { if (matcher(elem, context, xml)) { return true; } } } } else { while ((elem = elem[dir])) { // 这一块都是缓存 // 缓存才是最让人看不懂的 // 这一块,我也是没看的特别懂, 若是有人理解这里, 能够告知一下 // 蟹蟹 if (elem.nodeType === 1 || checkNonElements) { outerCache = elem[expando] || (elem[expando] = {}); uniqueCache = outerCache[elem.uniqueID] || (outerCache[elem.uniqueID] = {} ); if (skip && skip === elem.nodeName.toLowerCase()) { elem = elem[dir] || elem; } else if ( (oldCache = uniqueCache[key]) && oldCache[0] === dirruns && oldCache[1] === doneName) { return (newCache[2] = oldCache[2]); } else { // 这里是不走缓存的 上面两个if 应该都是从缓存中拿值 uniqueCache[key] = newCache; if ( (newCache[2] = matcher(elem, context, xml)) ) { return true } } } } } return false; } } // 这个方法就是把一堆matacher 揉成一个 function elementMatcher(matchers) { return matchers.length > 1 ? function(elem, context, xml) { var i = matchers.length; // 注意这里是i-- 说明这里是倒叙的 // 这就是像剥洋葱同样, 一层一层判断规则 while (i--) { //只要有一个不知足, 直接返回false if (!matchers[i](elem, context, xml)) { return false; } return true; } } : matchers[0]; } 复制代码
在跑完了matcherFromTokens
,咱们再回过头来继续看compile
,当compile
的所有的matcherFromTokens
都跑完之后,就只剩返回作缓存和返回matcherFromGroupMatchers
了。matcherFromGroupMatchers
函数返回superMatcher
函数,superMatcher
函数使用来遍历seed
,经过以前matcherFromTokes
运行得到的规则,对seed
进行筛选。
function matcherFromGroupsMatchers(elementMatchers, setMatchers) { var bySet = setMatchers.length > 0, byElement = elementMatchers.length > 0, superMatcher = function(seed, context, xml, results, outermost) { var elem, j, matcher, matchedCount = 0, i = '0', unmatched = seed && [], setMatched = [], contextBackup = outermostContext, // 若是没有seed 那么就拿文档所有的元素当作seed elems = seed || byElement && Expr.find["TAG"]('*', outermost), // 缓存用 dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), len = elems.length; if (outermost) { // 这个outermostContext会在baseMatcher的时候用做判断 outermostContext = context == document || context || outermost; } for (; i !== len && (elem = elems[i] != null); i++) { if(byElement && elem) { j = 0; if (!context && elem.ownerDoucment != document) { setDocumet(elem); xml = !documentIsHtml; } // elementMatches会出现多个的状况就是有逗号的状况 // 这个时候只要知足一组规则就能够把当前的元素推到结果集中 // 我们的例子只有一组规则 while ( (matcher = elementMatches[j++]) ) { if (matcher(elem, context || document, xml)) { results.push(elem); break; } } // 缓存 if (outermost) { dirruns = dirrunsUnique; } } // 没有被匹配的那些元素 if (bySet) { if ((elem = !matcher && elem)) { matchedCount--; } if (seed) { unmatched.push(elem); } } } matchedCount += i; // 这里的逻辑我并不太太懂, 由于我尝试的例子中, 并无走到这里的 // 这应该也是复杂选择器才会出现, 我试过:not(:not)的嵌套, 也没走到这里 // 但愿有懂的人 能讲解一下 // 这里若是没走for循环的话, 那么i 是字符串'0' 而matchedCount是数字0 // 再包括matchedCount会-- 有可能即便走了for循环 也会致使会不相等 if (bySet && i !== matchedCount) { j = 0; while((matcher = setMatchers[j++])) { matcher(unmatched, setMatched, context, xml); } if (seed) { if (matchedCount > 0) { while (i--) { if ( !(unmatched[i] | setMatched[i]) ) { setMatched[i] = pop.call(results); } } } setMatched = condense(setMatched); } push.apply(results, setMatched); if (outermost && !seed && setMatched.length > 0 && (matchedCount + setMatchers.length) > -1) { //排序 Sizzle.uniqueSort(results); } } if (outermost) { dirruns = dirrunsUnique; outermoustContext = contextBackup; } // 这里虽然return的是unmatched 可是results才是最终的结果, 在select函数中最后return的是做为参数的result return unmatched; } return bySet ? // marFunction 就是给参数的函数打expando标记的 markFunction(superMatcher) : superMatcher; } 复制代码
看Sizzle
大概看了2个月, 在2020年以前把大概流程所有都看通了,算是过年了。在最后这段查找中,Sizzle用了大量的闭包,大量的柯里化函数,为了就是保证所有的filter
函数入参,都为elem, context, xml
。这是我看的第一个库,看完了真的收获不少,最开始由于看看司徒大大的书,一时兴起想把Sizzle
看完,期间也以为太难了想放弃,可是最后磕磕绊绊终因而看下来了。此次看完了等把JavaScript框架设计
都看完,再把jquery源码撸了,再撸vue,而后再过年。哈哈哈哈哈。